########################################################################
# Copyright (C) 2010-2021 VMware, Inc.                                 #
# All Rights Reserved                                                  #
########################################################################

"""This module contains classes necessary to parse, extract, and validate
   VIB archives and VIB XML meta-data.
"""

# Python library modules
import datetime
import logging
import operator
import os
import re
import sys
import tarfile
import stat
import shutil
import subprocess
import tempfile

# esximage modules
from . import AcceptanceLevels
from . import CHECKSUM_EXEMPT_PAYLOADS
from . import Errors
from . import Version
from .ConfigSchema import ConfigSchema, ConfigSchemaSoftwareTag
from .Utils import ArFile
from .Utils import EsxGzip
from .Utils import VmTar
from .Utils import XmlUtils
from .Utils.HashedStream import HashedStream, HashError
from .Utils.Misc import isString
from .Utils import PathUtils

PAYLOAD_READ_CHUNKSIZE = 1024 * 1024

CONFIG_SCHEMA_DIR = "usr/lib/vmware/configmanager/schemas"

# VibSign module is lazy loaded in AcceptanceLevels.
from .AcceptanceLevels import loadVibSign

# Use the XmlUtils module to find ElementTree.
etree = XmlUtils.FindElementTree()

SCHEMADIR = XmlUtils.GetSchemaDir()

EXTENSION_ORIG_DESC = ".orig"
EXTENSION_ORIG_SIG = ".sig"

logger = logging.getLogger(__name__)

class HwPlatform(object):
   """A class for representing a hardware platform.
         Attributes (read-only Properties):
            * vendor - A string representing the hardware vendor.
            * model  - A string representing the hardware model.
   """
   def __init__(self, vendor='', model=''):
      """Class constructor.
            Parameters:
               * vendor - An optional string value to assign to the vendor
                          attribute.
               * model  - An optional string value to assign to the model
                          attribute.
      """
      self._vendor = vendor
      self._model = model

   def __hash__(self):
      # We want to support adding this to a set(), so we must be hashable.
      # Incidentally, this is also why our attributes are made available as
      # read-only properties. -RS
      return hash((self._vendor, self._model))

   def __eq__(self, other):
      mine = (self._vendor, self._model)
      theirs = (other._vendor, other._model)
      return mine == theirs

   def __ne__(self, other):
      return not self.__eq__(other)

   def __str__(self):
      return '-'.join(x for x in (self._vendor, self._model) if x)

   vendor = property(lambda self: self._vendor)
   model  = property(lambda self: self._model)

   @classmethod
   def FromXml(cls, xml):
      """Creates an HwPlatform instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new HwPlatform object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = etree.fromstring(xml)
         except Exception as e:
            raise ValueError("Could not parse HwPlatform XML data: %s." % e)

      vendor = xml.get("vendor", "").strip()
      if not vendor:
         raise ValueError("hwplatform must specify vendor.")

      model = xml.get("model", "").strip()
      return cls(vendor, model)

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("hwplatform")
      xml.set("vendor", self.vendor)
      if self.model:
         xml.set("model", self.model)
      return xml

   def MatchProblem(self, other):
      '''Determine if this HWPlatform object matches another.
         Parameters:
            * other - Another instance of HWPlatform.
         Return:
            * None  - If self.vendor and self.model match the hardware
                      platform 'other'. Note that if
                      self.vendor is '', it will match any value of
                      other.vendor. If self.model is '', it will match any
                      value of other.model.
            * problem - a tuple (attr, thisval, otherval), if there is a mis-
                        match.  If self.vendor != other.vendor, then attr will
                        be "vendor"; if the models don't match then attr will be
                        "model".  thisval contains the vendor or model value from
                        this instance of HwPlatform, while otherval contains the
                        vendor or model value from other.
      '''
      if self._vendor and self._vendor != other.vendor:
         return ("vendor", self._vendor, other._vendor)
      if self._model and self._model != other.model:
         return ("model", self._model, other.model)
      return None

class SoftwarePlatform(object):
   """Class representing a software platform.
      Attributes:
         version       - version of the platform requirement, for example
                         7.0 / 7.* / 7.0.* .
         locale        - locale of the platform, can be empty.
         productLineID - product, typically embeddedEsx / esxio.
   """
   # XXX: this differs from schema: VUM test bulletins can have version "e.x.p",
   # NSX-T has version like "7.*.*", EAM test bulletins can have version "*.*".
   VERSION_REGEX = r'^((\d+\.(\*|\d+)(\.(\*|\d+))?)|(e\.x\.p)|(\*\.\*))$'
   PRODUCT_REGEX = r'^(embeddedEsx|esx|esxio)$'

   PRODUCT_EMBEDDEDESX = 'embeddedEsx'
   PRODUCT_ESX = 'esx'
   PRODUCT_ESXIO = 'esxio'

   def __init__(self, version, locale, productLineID):
      """Class constructor.
      """
      self.SetVersion(version)
      self.SetLocale(locale)
      self.SetProduct(productLineID)

   def __str__(self):
      return ('<SoftwarePlatform version=%s locale=%s productLineID=%s>'
              % (self._version, self._locale, self._productLineID))

   def __eq__(self, other):
      return (self._version == other._version and
              self._locale == other._locale and
              self._productLineID == other._productLineID)

   def __ne__(self, other):
      return (self._version != other._version or
              self._locale != other._locale or
              self._productLineID != other._productLineID)

   def SetVersion(self, version):
      """Verify and set version.
      """
      if not isinstance(version, str) or not version:
         raise ValueError('Platform version must be a non-empty string')
      elif not re.match(self.VERSION_REGEX, version):
         raise ValueError("Invalid platform version '%s'" % version)
      else:
         self._version = version

   def SetLocale(self, locale):
      """Verify and set locale.
      """
      if not isinstance(locale, str):
         raise ValueError('Platform locale must be a string')
      self._locale = locale

   def SetProduct(self, productLineID):
      """Verify and set productLineID.
      """
      if not isinstance(productLineID, str) or not productLineID:
         raise ValueError('Platform productLineID must be a non-empty string')
      elif not re.match(self.PRODUCT_REGEX, productLineID):
         logger.warning("Invalid platform productLineID '%s'" % productLineID)
      self._productLineID = productLineID

   version = property(lambda self: self._version, SetVersion)
   locale = property(lambda self: self._locale, SetLocale)
   productLineID = property(lambda self: self._productLineID, SetProduct)

   @classmethod
   def FromXml(cls, xml):
      """Creates an instance from XML. Input should be an instance of
         ElementTree.
      """
      return cls(xml.get('version'), xml.get('locale'),
                 xml.get('productLineID'))

   def ToXml(self, isVmwareXml=False):
      """Serialize the object to XML. If isVmwareXml is set, tag name will
         be swPlatform instead of softwarePlatform.
      """
      tagName = 'swPlatform' if isVmwareXml else 'softwarePlatform'
      elem = etree.Element(tagName,
         attrib={
            'version': self._version,
            'locale': self._locale,
            'productLineID': self._productLineID,
         })
      return elem

class VibRelation(object):
   """Describes a relationship between two VIBs, such as a conflict,
      requirement, or replacement.
   """

   RELATIONS = ("<<", "<=", "=", ">=", ">>")

   RELRE = r'\s?(<[<=]|=|>[>=])\s?'
   relre = re.compile(RELRE)

   def __init__(self, name, relation = None, version = None, implicit = False):
      """Class consctructor.
            Parameters:
               * name     - The name of the relation.
               * relation - An operator describing whether the relation matches
                            only particular versions. If specified, must be one
                            of "<<", "<=", "=", ">=" or ">>". ("<<" and ">>"
                            indicate less than and greater than.) If this
                            parameter is specified, the version parameter must
                            also be specified.
               * version  - An instance of Version.VibVersion used for
                            comparison based on the relation operator. If this
                            parameter is specified, version must also be
                            specified.
      """
      self.name = name
      self.relation = relation
      self.version = version
      self.implicit = implicit
      if relation is not None:
         self.id = "%s %s %s" % (name, relation, str(version))
      else:
         self.id = name

   def __eq__(self, other):
      """Determines if this object describes the same relationship as another
         VibRelation object.
            Parameters:
               * other - An instance of VibRelation.
            Returns: True or False, depending on whether the objects define the
                     same relationship or not.
      """
      return (self.name == other.name and
              self.relation == other.relation and
              self.version == other.version)

   def __ne__(self, other):
      return not self.__eq__(other)

   __str__ = lambda self: str(self.id)

   def matchesprovide(self, provide):
      """Returns True if the provide matches this object."""
      if self.name != provide.name:
         return False
      if not self.relation:
         return True
      if not provide.version:
         return False

      # If our relation only specifies version, then we don't compare release.
      compare = lambda x, y: (x > y) - (x < y)
      if self.version.release:
         rc = compare(provide.version, self.version)
      else:
         rc = compare(provide.version.version, self.version.version)

      if self.relation == "<<":
         return rc == -1
      if self.relation == "<=":
         return rc == -1 or rc == 0
      if self.relation == "=":
         return rc == 0
      if self.relation == ">=":
         return rc == 0 or rc == 1
      if self.relation == ">>":
         return rc == 1

   @classmethod
   def FromString(cls, relstr):
      """Creates a VibRelation instance from a string of the form
         '<name> [(<</<=/=/>=/>>) (version)]'.
         Returns: A new VibRelation object.
         Raises:
            * ValueError - If an illegal operator is in the string, or operator
                           was found but version was not found
      """
      parts = cls.relre.split(relstr)
      partslen = len(parts)
      if partslen == 1:
         return cls(parts[0].strip())
      elif partslen < 3 or not parts[2].strip():
         raise ValueError("Relation found but version not found")
      elif partslen > 3:
         raise ValueError("Illegal operator or more than one operator used")
      return cls(parts[0].strip(), parts[1],
                 Version.VibVersion.fromstring(parts[2].strip()))

   @classmethod
   def FromXml(cls, xml):
      """Creates a VibRelation instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new VibRelation object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = etree.fromstring(xml)
         except Exception as e:
            raise ValueError("Could not parse VIB relation XML data: %s." % e)

      name = xml.get("name")
      if not name:
         raise ValueError("name attribute is missing or empty.")
      relation = xml.get("relation")
      if relation  and relation not in cls.RELATIONS:
         raise ValueError("Invalid relation value: %s." % relation)
      version = xml.get("version")
      if relation and not version:
         raise ValueError("relation attribute exists and is non-empty, but "
                          "version attribute is missing or empty.")
      if version and not relation:
         raise ValueError("version attribute exists and is non-empty, but "
                          "relation attribute is missing or empty.")
      if version:
         try:
            version = Version.VibVersion.fromstring(version)
         except Exception as e:
            raise ValueError("version attribute has invalid value '%s'."
                             % version)

      return cls(name, relation, version)

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("constraint")
      xml.set("name", self.name)
      if self.relation and self.version:
         xml.set("relation", self.relation)
         xml.set("version", str(self.version))
      return xml


class VibProvide(object):
   "Describes a feature provided by a VIB."
   def __init__(self, name, version = None, implicit = False):
      """Class consctructor.
            Parameters:
               * name     - The name of the provided feature.
               * version  - An optional instance of Version.VibVersion.
               * implicit - The object represents an implicit provide using the
                            package name and version. (Defaults to False.)
      """
      self.name = name
      self.version = version
      self.implicit = implicit
      if version is not None:
         self.id = "%s = %s" % (name, str(version.versionstring))
      else:
         self.id = name

   def __eq__(self, other):
      """Determines if this object describes the same provide as another
         VibProvide object.
            Parameters:
               * other - An instance of Provide.
            Returns: True or False, depending on whether the objects define the
                     same provide or not.
      """
      return (self.name == other.name and
              self.version == other.version)

   def __ne__(self, other):
      return not self.__eq__(other)

   __str__ = lambda self: self.id

   @classmethod
   def FromString(cls, provstr):
      """Creates a VibProvide instance from a string of the form:
         '<provide-name> [= <provide-version>]'
         Returns: A new VibProvide object.
      """
      if '=' in provstr:
         name, ver = provstr.split('=')
         return cls(name.strip(), Version.VibVersion.fromstring(ver.strip()))
      else:
         return cls(provstr.strip())

   @classmethod
   def FromXml(cls, xml):
      """Creates a VibProvide instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new VibProvide object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = etree.fromstring(xml)
         except Exception as e:
            raise ValueError("Could not parse VIB provide XML data: %s." % e)

      name = xml.get("name")
      if not name:
         raise ValueError("name attribute is missing or empty.")
      version = xml.get("version")
      if version:
         try:
            version = Version.VibVersion.fromstring(version)
         except Exception as e:
            raise ValueError("version attribute has invalid value '%s'."
                             % version)
      return cls(name, version)

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("provide")
      xml.set("name", self.name)
      if self.version:
         xml.set("version", str(self.version))
      return xml


class Checksum(object):
   """Represents a checksum.
      Attributes:
         * checksumtype - A string describing the algorithm used to compute
                          the checksum.
         * checksum     - The checksum data itself
         * verifyprocess - The type of decompression or other post-processing
                          that needs to be done on the payload prior to
                          computing the checksum.  "" means the checksum can
                          be computed directly on the payload.
   """
   VERIFY_PROCESSES = ("", "gunzip", "txt-mle")

   def __init__(self, checksumtype="sha-256", checksum='',
                verifyprocess=""):
      self.checksumtype = checksumtype
      self.checksum = checksum
      if verifyprocess not in self.VERIFY_PROCESSES:
         raise AssertionError
      self.verifyprocess = verifyprocess

   def __str__(self):
      return '<checksum type %s: %s>' % (self.checksumtype, self.checksum)

   __repr__ = __str__

   def __eq__(self, other):
      return (self.checksumtype == other.checksumtype and
              self.checksum == other.checksum and
              self.verifyprocess == other.verifyprocess)

   def __ne__(self, other):
      return not self.__eq__(other)

   @classmethod
   def FromXml(cls, xml):
      """Creates a Checksum instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new Checksum object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = etree.fromstring(xml)
         except Exception as e:
            raise ValueError("Could not parse checksum XML data: %s." % e)

      checksumtype = xml.get("checksum-type", "").strip()
      if not checksumtype:
         raise ValueError("checksum-type attribute was not found")

      verifyprocess = xml.get("verify-process", "").strip()
      checksum = xml.text.strip() if xml.text else ""

      return cls(checksumtype, checksum, verifyprocess)

   def ToXml(self):
      """Serializes a Checksum object to XML.
         Returns: a etree.Element object.
      """
      elem = etree.Element("checksum")
      elem.set("checksum-type", self.checksumtype)
      if self.verifyprocess:
         elem.set("verify-process", self.verifyprocess)
      elem.text = self.checksum
      return elem


class BaseVib(object):
   """The BaseVib class manages VIB metadata and does not contain methods
      or properties that work with .vib files or that are specific to any type
      of VIB. It contains properties and methods common to any type of VIB.

      VIB objects are intended to be hashable based on a subset of properties.

      Class Variables:
         * VIB_VERSION        - A constant string for the version of the
                                current VIB Specification
         * TYPE_BOOTBANK      - A constant representing a VIB type containing
                                ESXi bootbank payloads
         * VIB_SCHEMA         - The default path of an XML schema that can be
                                used to verify that VIB XML data is valid.
         * TYPE_LOCKER        - A constant representing a VIB type containing
                                files for product locker.

      Attributes:
         * id                - A string giving the unique ID of the VIB.
                               Read-only.
         * vibtype           - One of TYPE_BOOTBANK or TYPE_LOCKER, specifying the
                               payload type of the VIB.
                               Can only be set by the constructor.
         * name              - A user-friendly string giving the name of the
                               VIB. Not required to be unique.
         * version           - An instance of Version, specifying the version
                               information for the VIB.
         * versionstr        - Read-only string representation of version
         * vendor            - A string representing the vendor or publisher
                               of this VIB
         * summary           - A string giving a user-friendly, one-line
                               summary of the VIB contents.
         * description       - A string giving a user-friendly, detailed
                               description of the software in the VIB.
         * releasedate       - A float value giving the date and time the VIB
                               was built or released (as returned by
                               time.time()). May also be None if unknown.
         * urls              - A dict with each key being a unique, friendly identifier
                               for a KB article or other in depth information
                               about this VIB or the issues it is resolving, and
                               the corresponding value being the URL string to the
                               KB article or information.
         * depends           - A list of VibRelations instances, specifying
                               dependency information for the VIB.
         * conflicts         - A list of VibRelations instances, specifying
                               constraints for VIBs that conflict with this VIB
         * replaces          - A list of VibRelations instances, specifying
                               VIBs that this VIB replaces.
         * provides          - A list of Provides constraint instances, specify-
                               ing virtual packages or capabilities of this VIB
                               that other VIBs may have a dependency on.
         * compatibleWith    - A list of VibRelations instances, specifying
                               entities that this VIB is compatible with, but
                               which this VIB is not dependent on for install.
         * swtags            - A list of software tags that may be used for
                               information or filtering.
         * relativepath      - A path relative to the location of metadata.zip
                               giving the location of the VIB.
         * packedsize        - The number of bytes of the entire VIB archive
         * checksum          - The MD5/SHA checksum of the VIB archive
         * scanned           - A boolean indicating whether this VIB has been
                               scanned against the host or not.
         * installdate       - A float value giving the date and time the VIB
                               was installed (as returned by time.time()). May
                               also be None if VIB was not installed, or the
                               value is otherwise not applicable.
   """
   VIB_VERSION = '2.0.0'

   TYPE_BOOTBANK = 'bootbank'
   TYPE_META = 'meta'
   TYPE_LOCKER = 'locker'
   vibtypes = (TYPE_BOOTBANK, TYPE_META, TYPE_LOCKER)

   _vib_classes = dict()

   VIB_SCHEMA = os.path.join(SCHEMADIR, "vib20-metadata.rng")

   # These ATTRS must be equal between two VIBs that are being merged. If any
   # are unequal, this will result in an exception when calling MergeVib().
   ATTRS_TO_VERIFY = ('vibtype', 'name', 'version', 'vendor')
   # When merging two VIBs, the following attributes will be taken from the VIB
   # with the newer releasedate. However, if two VIBs have the same releasedate
   # but non-matching attributes, this will still cause an exception.
   ATTRS_TO_COPY = ('summary', 'description', 'releasedate', 'urls', 'depends',
                    'conflicts', 'replaces', 'provides', 'compatibleWith',
                    'swtags')

   def __init__(self, **kwargs):
      """Class constructor.
            Parameters:
               * kwargs - A list of keyword arguments used to populate the
                          VIB's attributes.
            Returns: A new VIB object.
            Raises:
               * TypeError  - On any unrecognized keyword argument.
               * ValueError - Illegal value passed for a valid keyword (e.g.,
                              wrong type).
      """
      # NOTE: please update ATTRS_TO_COPY if new attr is being added.
      #       Subclasses need to update ATTRS_TO_COPY if they want their
      #       attributes being copied in MergeVib operation.
      self._vibtype          = kwargs.pop('vibtype', self.TYPE_BOOTBANK)
      self.name              = kwargs.pop('name', '')
      self.version           = kwargs.pop('version', Version.VibVersion(""))
      self.vendor            = kwargs.pop('vendor', '')
      self.summary           = kwargs.pop('summary', '')
      self.description       = kwargs.pop('description', '')
      self.releasedate       = kwargs.pop('releasedate', None)
      self.urls              = kwargs.pop('urls', {})
      self.depends           = kwargs.pop('depends', list())
      self.conflicts         = kwargs.pop('conflicts', list())
      self.replaces          = kwargs.pop('replaces', list())
      self.provides          = kwargs.pop('provides', list())
      self.compatibleWith    = kwargs.pop('compatibleWith', list())
      self.swtags            = kwargs.pop('swtags', list())
      self.relativepath      = kwargs.pop('relativepath', '')
      self.packedsize        = kwargs.pop('packedsize', 0)
      self.checksum          = kwargs.pop('checksum', Checksum())
      self._scanned          = kwargs.pop('scanned', False)
      self.installdate       = kwargs.pop('installdate', None)
      self.remotelocations   = kwargs.pop('remotelocations', list())

      if kwargs:
         badkws = ', '.join("'%s'" % kw for kw in kwargs)
         raise TypeError("Unrecognized keyword argument(s): %s." % badkws)

      # Always add implicit provide for self.name, self.version.
      provide = VibProvide(self.name, self.version, implicit=True)
      if provide not in self.provides:
         self.provides.append(provide)

      # Always add implicit replaces.
      replace = VibRelation(self.name, "<<", self.version, implicit=True)
      if replace not in self.replaces:
         self.replaces.append(replace)

      # _signeddesctext is used to store the following
      # 1) The descriptor that gets signed.
      # 2) The original signed descriptor from the vib database
      self._signeddesctext = ''

      # _pkcs7 is used to store a list of signature of _signeddesctext,
      # each could be one of the following
      # 1) The signature from the vib database
      # 2) The newly generated signature for a vib
      self._pkcs7 = []

   def setvibtype(self, value):
      if value in self.vibtypes:
         self._vibtype = value
      else:
         raise ValueError(value + " is not a valid VIB type")

   vibtype = property(lambda self: self._vibtype, setvibtype)
   id      = property(lambda self: '%s_%s_%s_%s' % (self.vendor, self._vibtype,
                                 self.name, str(self.version)))
   versionstr = property(lambda self: str(self.version))
   __str__ = lambda self: "%s-%s" % (self.name, self.version)

   def GetSignature(self):
      '''Returns a byte string that contains all signatures joined by new lines.
      '''
      return b'\n'.join(self._pkcs7)

   def GetOrigDescriptor(self):
      return self._signeddesctext

   def SetSignature(self, pkcs7):
      '''Take a string that contains all signatures joined by new lines,
         set self._pkcs7 to be a list of the signatures in bytes.
      '''
      if sys.version_info[0] >= 3 and isinstance(pkcs7, str):
         pkcs7 = pkcs7.encode()
      self._pkcs7 = self._getPkcs7List(pkcs7)

   def ClearSignature(self):
      '''Clear the list of signature to start fresh.
      '''
      del self._pkcs7[:]

   def SetOrigDescriptor(self, signeddesctext):
      self._signeddesctext = signeddesctext

   def _getPkcs7List(self, pkcs7):
      '''Convert a byte string containing multiple pkcs7 signatures to a list.
      '''
      PKCS7_HEADER = b'-----BEGIN PKCS7-----'
      indexes = [m.start() for m in re.finditer(PKCS7_HEADER, pkcs7)] + \
                [len(pkcs7)]
      return [pkcs7[indexes[i]:indexes[i + 1]].strip(b' \n')
              for i in range(len(indexes) - 1)]

   def MergeVib(self, other):
      '''Merges attributes between two VIBs.
            Parameters:
               * other - another VIB object to merge with.
            Returns: A new VIB object.
            Exceptions:
               ValueError - If merge is attempted between two VIBs with
                            non-matching values for any attribute in the
                            ATTRS_TO_VERIFY class variable, or if a merge is
                            attempted between two VIBs with the same
                            releasedate but different values for any attribute
                            in the ATTRS_TO_COPY class variable.
               TypeError  - If the two VIBs are not of the same class
      '''
      kwargs = {}

      if self.__class__.__name__ != other.__class__.__name__:
         msg = "Unsupported types to merge: '%s' and '%s'" % (
               self.__class__.__name__, other.__class__.__name__)
         raise TypeError(msg)

      for attr in self.ATTRS_TO_VERIFY:
         mine = getattr(self, attr)
         theirs = getattr(other, attr)
         if mine != theirs:
            msg = "VIBs %s and %s have unequal values of the '%s' attribute: " \
                  "'%s' != '%s'" % (self.id, other.id, attr, str(mine),
                                    str(theirs))
            raise ValueError(msg)

      if other.releasedate > self.releasedate:
         newer = other
         older = self
         kwargs['relativepath'] = newer.relativepath
         kwargs['remotelocations'] = newer.remotelocations
      elif other.releasedate < self.releasedate:
         newer = self
         older = other
         kwargs['relativepath'] = newer.relativepath
         kwargs['remotelocations'] = newer.remotelocations
      else:
         for attr in self.ATTRS_TO_COPY:
            mine, theirs = getattr(self, attr), getattr(other, attr)
            if mine != theirs:
               if attr == 'swplatforms' and (not mine or not theirs):
                  # PR 2783222: software platforms may be missing in the
                  # metadata VIB when the depot is made by older EPK/VC, set
                  # the value here according to the VIB descriptor. However,
                  # they cannot be unequal while both being non-empty.
                  self.swplatforms = mine or theirs
                  other.swplatforms = theirs or mine
                  continue
               raise ValueError("VIBs %s and %s have unequal values of the "
                                "'%s' attribute: '%s' != '%s'" % (self.id,
                                other.id, attr, getattr(self, attr),
                                getattr(other, attr)))
         newer = self
         older = other
         rls = list(set(self.remotelocations + other.remotelocations))
         kwargs['remotelocations'] = rls
         # When merging happens to VIBs in metadata.zip, make sure we keep
         # non-empty relativepath for remotelocation generation.
         kwargs['relativepath'] = newer.relativepath or older.relativepath

      for attr in self.ATTRS_TO_VERIFY + self.ATTRS_TO_COPY:
         kwargs[attr] = getattr(newer, attr)

      # Preserve installdate, even if VIB metadata has changed.
      installdates = [x for x in (newer.installdate, older.installdate)
                      if x is not None]
      if installdates:
         kwargs["installdate"] = max(installdates)

      return self.__class__(**kwargs)

   @classmethod
   def _XmlToKwargs(cls, xml):
      kwargs = dict()

      kwargs["vibtype"] = xml.findtext("type")

      for tag in ("name", "vendor", "summary", "description"):
         # lxml does not honor "default" argument to findtext. :-(
         kwargs[tag] = (xml.findtext(tag) or "").strip()

      if not kwargs["name"]:
         raise Errors.VibFormatError("Unknown", "Missing VIB name.")

      text = (xml.findtext("version") or "").strip()
      if text:
         try:
            kwargs["version"] = Version.VibVersion.fromstring(text)
            vibnv = "%s-%s" % (kwargs["name"], kwargs["version"])
         except Exception as e:
            msg = "VIB '%s' has invalid version '%s'." % (kwargs["name"], text)
            raise Errors.VibFormatError(kwargs["name"], msg)

      text = (xml.findtext("release-date") or "").strip()
      if text:
         try:
            kwargs["releasedate"] = XmlUtils.ParseXsdDateTime(text)
         except Exception as e:
            msg = "VIB '%s' has invalid release-date: %s." % (vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)

      # Part of the database; not used in the publish schema.
      text = (xml.findtext("installdate") or "").strip()
      if text:
         try:
            kwargs["installdate"] = XmlUtils.ParseXsdDateTime(text)
         except Exception as e:
            msg = "VIB '%s' has invalid installdate: %s." % (vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)

      urls = {}
      for elem in xml.findall('urls/url'):
         key = elem.get('key')
         if not key:
            msg = "VIB '%s' url '%s' has missing or invalid key" % \
               (vibnv, elem.text)
            raise Errors.VibFormatError(vibnv, msg)
         urls[key]  = elem.text.strip()
      kwargs["urls"] = urls

      for tag in ("depends", "conflicts", "replaces", "compatibleWith"):
         kwargs[tag] = list()
         for elem in xml.findall("relationships/" + tag + "/constraint"):
            try:
               kwargs[tag].append(VibRelation.FromXml(elem))
            except Exception as e:
               msg = "VIB '%s' has invalid %s: %s." % (vibnv, tag, e)
               raise Errors.VibFormatError(vibnv, msg)

      kwargs["provides"] = list()
      for elem in xml.findall("relationships/provides/provide"):
         try:
            kwargs["provides"].append(VibProvide.FromXml(elem))
         except Exception as e:
            msg = "VIB '%s' has invalid provide: %s." % (vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)

      kwargs["swtags"] = list()
      for elem in xml.findall("software-tags/tag"):
         text = elem.text.strip()
         kwargs["swtags"].append(text)

      kwargs["relativepath"] = (xml.findtext("relative-path") or '').strip()

      text = (xml.findtext("packed-size") or '0').strip()
      try:
         kwargs["packedsize"] = int(text)
      except Exception as e:
         msg = "VIB '%s' has invalid packed-size: %s" % (vibnv, e)
         raise Errors.VibFormatError(vibnv, msg)

      elem = xml.find("checksum")
      if elem is not None:
         try:
            kwargs["checksum"] = Checksum.FromXml(elem)
         except Exception as e:
            msg = "VIB '%s' has invalid checksum: %s" % (vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)

      return kwargs

   @classmethod
   def FromXml(cls, xml, origdesc = None, signature = None, validate = False, schema = VIB_SCHEMA, **kwargs):
      """Creates a BaseVIB instance from an ElementTree XML Node.
            Parameters:
               * xml       - Must be either an instance of ElementTree, or a
                             string of XML-formatted data.
               * origdesc  - The original descriptor data for the VIB
               * signature - The original signature data for the VIB
               * validate  - If True, XML will be validated against a schema. If
                             False, no validation will be done. Defaults to
                             True.
               * schema    - A file path giving the location of a VIB schema.
               * kwargs    - Initialize constructor arguments from keywords.
                             Primarily useful to provide required id or type
                             arguments when XML data is from a template.
            Returns: A new Vib object.
            Raises:
               * VibFormatError     - If the given XML is not valid XML, or
                                      does not contain required elements or
                                      attributes.
               * VibValidationError - If validation is True, and an error
                                      occurred during validation.
      """
      if not etree.iselement(xml):
         try:
            xml = etree.fromstring(xml)
         except Exception as e:
            msg = "Could not parse VIB XML data: %s." % e
            raise Errors.VibFormatError("Unknown", msg)

      name = xml.findtext("name") or ''
      if validate:
         res = cls.Validate(xml, schema)
         if not res:
            msg = "VIB (%s) XML data failed schema validation. Errors: %s" \
                  % (name, res.errorstrings)
            raise Errors.VibValidationError(None, res.errorstrings, msg)

      vibtype = xml.findtext("type")
      if not vibtype:
         vibtype = BaseVib.TYPE_BOOTBANK  # same default as constructor
      if vibtype not in cls.vibtypes:
         raise Errors.VibFormatError(name, "Invalid VIB type '%s'" % vibtype)

      vibclass = cls._vib_classes.get(vibtype, BaseVib)
      xmlkwargs = vibclass._XmlToKwargs(xml)

      if origdesc is not None:
         if not etree.iselement(origdesc):
            try:
               origxml = etree.fromstring(origdesc)
            except Exception as e:
               msg = "Could not parse original VIB XML data: %s." % e
               raise Errors.VibFormatError(name, msg)
         else:
            origxml = origdesc
         if validate:
            res = cls.Validate(origxml, schema)
            name = origxml.findtext("name") or ''
            if not res:
               msg = "Original VIB (%s) XML data failed schema validation. " \
                     "Errors: %s" % (name, res.errorstrings)
               raise Errors.VibValidationError(None, res.errorstrings, msg)

         # Use original as the source of args, except install date and checksum
         # that are present only in image database and metadata zip contexts
         # respectively.
         origkwargs = vibclass._XmlToKwargs(origxml)
         origkwargs['installdate'] = xmlkwargs.get('installdate', None)
         origkwargs['checksum'] = xmlkwargs.get('checksum', Checksum())
         kwargs.update(origkwargs)
      else:
         kwargs.update(xmlkwargs)

      retvib = vibclass(**kwargs)
      if signature is not None:
         retvib.SetSignature(signature)
      if origdesc is not None:
         retvib.SetOrigDescriptor(origdesc)
      return retvib

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("vib", {"version":"5.0"})

      elem = etree.SubElement(xml, "type").text = self._vibtype

      for tag in ("name", "version", "vendor", "summary", "description"):
         etree.SubElement(xml, tag).text = str(getattr(self, tag))

      elem = etree.SubElement(xml, "release-date")
      if self.releasedate is not None:
         elem.text = self.releasedate.isoformat()
      else:
         tz = XmlUtils.UtcInfo()
         elem.text = datetime.datetime.now(tz).isoformat()

      elem = etree.SubElement(xml, "urls")
      for key in sorted(self.urls.keys()):
         urlelem = etree.SubElement(elem, "url")
         urlelem.text = self.urls[key]
         urlelem.set("key", key)

      rel = etree.SubElement(xml, "relationships")
      for tag in ("depends", "conflicts", "replaces", "provides",
                  "compatibleWith"):
         elem = etree.SubElement(rel, tag)
         constraints = getattr(self, tag)
         for constraint in constraints:
            if not constraint.implicit:
               elem.append(constraint.ToXml())

      elem = etree.SubElement(xml, "software-tags")
      for tag in self.swtags:
         subelem = etree.SubElement(elem, "tag").text = tag

      if self.relativepath:
         etree.SubElement(xml, "relative-path").text = self.relativepath
      if self.packedsize:
         etree.SubElement(xml, "packed-size").text = str(self.packedsize)
      if self.checksum.checksum:
         xml.append(self.checksum.ToXml())
      if self.installdate is not None:
         elem = etree.SubElement(xml, "installdate")
         elem.text = self.installdate.isoformat()

      return xml

   def ToXmlString(self):
      try:
         return etree.tostring(self.ToXml(), pretty_print=True)
      except Exception:
         return etree.tostring(self.ToXml())

   @classmethod
   def Validate(cls, xml, schema = VIB_SCHEMA):
      """Validates an XML document against the given schema.
            Parameters:
               * xml    - An ElementTree instance, or a string containing XML
                          data.
               * schema - A file path giving the location of a VIB schema.
            Returns:
               A ValidationResult instance, which evaluates to
               True if the document is valid, False otherwise.
               errorstrings and errors properties can also be evaluated.
            Raises:
               * VibValidationError - If the schema cannot be loaded, if the
                                      schema is not valid, or if the
                                      ElementTree implementation does not
                                      support validation.
               * VibFormatError     - If the xml parameter is not valid XML.
      """
      try:
         schema_obj = XmlUtils.GetSchemaObj(schema)
      except Exception as e:
         raise Errors.VibValidationError(None, [], str(e))

      if not etree.iselement(xml):
         try:
            xml = etree.fromstring(xml)
         except Exception as e:
            msg = "Could not parse VIB XML data: %s." % e
            raise Errors.VibFormatError(msg, None)

      return XmlUtils.ValidateXml(xml, schema_obj)

   def GetRelativePath(self):
      """Returns a relative name for the vib in a depot tree
            Parameters:
               None
            Returns:
               A relative path
      """
      return os.path.join("vib20", self.name, self.id + ".vib")

class Payload(object):
   """The Payload class encapsulates a VIB payload of different types.

      Class Variables:
         * TYPE_VGZ                - Payload is vtar.gz format
         * TYPE_TGZ                - Payload is tar.gz format
         * TYPE_BOOT               - Payload is a boot kernel not mappable in
                                     VisorFS
         * TYPE_UPGRADE            - Payload is upgrade data for inclusion in
                                     upgrade ISO images.
         * TYPE_BOOT_COM32_BIOS    - Payload is a boot loader COM32 module.
         * TYPE_BOOT_ISO_BIOS      - Payload is an ISO boot loader.
         * TYPE_BOOT_PXE_BIOS      - Payload is a PXE boot loader.
         * TYPE_BOOT_LOADER_EFI    - Payload is an EFI boot loader.
         * TYPE_ELTORITO_IMAGE_EFI - Payload is an EFI FAT12 boot image.
         * TYPE_TEXT               - Payload is a file that's a plain text file.
         * TYPE_INSTALLER_VGZ      - Payload is a vtar.gz format payload that
                                     is only used for installation.

      Attributes:
         * name         - A name to identify the payload within a VIB
         * payloadtype  - Identifies the payload type, one of TYPE_*
         * bootorder    - A positive integer for ordering BOOT payloads
         * vfatname     - String, VFAT filename in 8.3 format
         * size         - An integer giving the payload size, in bytes.
         * fileobj      - File object that points to a local source file
                          for the payload
         * checksums    - A list of Checksum instances for the payload
   """
   TYPE_VGZ = "vgz"
   TYPE_TGZ = "tgz"
   TYPE_BOOT = "boot"
   TYPE_UPGRADE = "upgrade"
   TYPE_BOOT_COM32_BIOS = "boot_com32_bios"
   TYPE_BOOT_ISO_BIOS = "boot_iso_bios"
   TYPE_BOOT_PXE_BIOS = "boot_pxe_bios"
   TYPE_BOOT_LOADER_EFI = "boot_loader_efi"
   TYPE_ELTORITO_IMAGE_EFI = "eltorito_image_efi"
   TYPE_TEXT = "text"
   TYPE_INSTALLER_VGZ = "installer_vgz"
   # A special value used for ordering payload in front of other vgz payloads
   BOOT_ORDER_VGZ_BASE = 100

   PAYLOAD_TYPES = (TYPE_VGZ, TYPE_TGZ, TYPE_BOOT, TYPE_UPGRADE,
                    TYPE_BOOT_COM32_BIOS, TYPE_BOOT_ISO_BIOS,
                    TYPE_BOOT_PXE_BIOS, TYPE_BOOT_LOADER_EFI,
                    TYPE_ELTORITO_IMAGE_EFI, TYPE_TEXT, TYPE_INSTALLER_VGZ)
   GZIP_TYPES = (TYPE_VGZ, TYPE_TGZ, TYPE_BOOT, TYPE_INSTALLER_VGZ)
   NON_GZIP_TYPES = tuple(set(PAYLOAD_TYPES) - set(GZIP_TYPES))
   # VisorFS mountable tardisk types.
   TARDISK_TYPES = (TYPE_VGZ, TYPE_TGZ, TYPE_INSTALLER_VGZ)

   def __init__(self, name, payloadtype, bootorder=0, vfatname="",
                size=0, fileobj=None, checksums=None):
      if len(name) > 15:
         raise ValueError("Name '%s' exceeds 15 characters" % name)
      self.name = name
      if payloadtype not in self.PAYLOAD_TYPES:
         logger.warning("Payload type of '%s' is not supported." % payloadtype)
      self.payloadtype = payloadtype
      self.bootorder = bootorder
      self.vfatname = vfatname
      self.size = size
      self.fileobj = fileobj
      if checksums:
         self.checksums = checksums
      else:
         self.checksums = list()
      self.localname = ''

   def __eq__(self, other):
      # size and checksums (particularly the latter) are important for security.
      # Payloads with unequal size or checksums MUST NOT compare equally. This
      # is used in the BaseVib.MergeVib() method.
      mine = (self.name, self.payloadtype, self.bootorder,
              self.size,
              sorted(self.checksums, key=operator.attrgetter('checksum')))
      other = (other.name, other.payloadtype, other.bootorder,
               other.size,
               sorted(other.checksums, key=operator.attrgetter('checksum')))
      return mine == other

   def __ne__(self, other):
      return not self.__eq__(other)

   def __repr__(self):
      return "%s: %.3f KB" % (self.name, self.size / 1024.0)

   def GetPreferredChecksum(self, verifyprocess=''):
      """Returns the most secure checksum available for checking the payload
         for a given verify process.
         Currently, it will return the SHA-256 checksum if available.  If not,
         then it returns the SHA-1 checksum.  If neither of the above is
         available, returns the first checksum with the given verifyprocess.
            Parameters:
               * verifyprocess - only return a checksum of particular verify
                                 process, default is empty string, meaning
                                 no special process.
            Returns: A Checksum instance, or None if there is no checksum
                     matching the criteria.
      """
      algomap = dict()
      for checksum in self.checksums:
         if checksum.verifyprocess == verifyprocess:
            algomap[checksum.checksumtype] = checksum
      if "sha-256" in algomap:
         return algomap["sha-256"]
      elif "sha-1" in algomap:
         return algomap["sha-1"]
      else:
         checksums = list(algomap.values())
         return checksums[0] if checksums else None

   @classmethod
   def FromXml(cls, xml):
      """Creates a Payload instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new Payload object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = etree.fromstring(xml)
         except Exception as e:
            raise ValueError("Could not parse payload XML data: %s." % e)

      kwargs = dict()

      text = xml.get("name")
      kwargs["name"] = text.strip()
      length = len(kwargs["name"])
      if length < 1 or length > 15:
         raise ValueError("Invalid value '%s' for name." % text)

      text = xml.get("type")
      kwargs["payloadtype"] = text.strip()
      if kwargs["payloadtype"] not in cls.PAYLOAD_TYPES:
         logger.warning("Unrecognized value '%s' for payload type." % text)

      text = xml.get("boot-order", "0")
      try:
         kwargs["bootorder"] = int(text)
      except Exception:
         raise ValueError("Invalid value '%s' for boot-order." % text)

      kwargs["vfatname"] = xml.get("vfat-name", "").strip()

      text = xml.get("size", "0")
      try:
         kwargs["size"] = int(text)
      except:
         raise ValueError("Invalid value '%s' for size." % text)

      elems = xml.findall("checksum")
      checksums = list()
      for elem in elems:
         checksums.append(Checksum.FromXml(elem))
      kwargs["checksums"] = checksums

      return cls(**kwargs)

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("payload")
      xml.set("name", self.name)
      xml.set("type", self.payloadtype)
      if self.bootorder > 0:
         xml.set("boot-order", str(self.bootorder))
      if self.vfatname:
         xml.set("vfat-name", self.vfatname)
      xml.set("size", str(self.size))
      if self.checksums:
         for checksum in self.checksums:
            xml.append(checksum.ToXml())
      return xml


class MaintenanceMode(object):
   """This class represents maintenance mode attributes for a VIB.
         Attributes:
            * remove  - If True, maintenance mode must be enabled when updating
                        or removing the VIB.
            * install - If True, maintenance mode must be enabled on VIB
                        installation.
         Note: Upgrade is a case of removing an old version of a VIB and
               installing a new version. In such cases, the "remove"
               maintenance mode attribute of the VIB being removed should be
               evaluated, and the "install" maintenace mode attribute of the
               new VIB should be evaluated.
   """
   def __init__(self, remove = True, install = None):
      self.remove = remove
      if install is None:
         self.install = remove
      else:
         self.install = install

   def __str__(self):
      return "remove/update: %s, installation: %s" % (self.remove, self.install)

   def __eq__(self, other):
      return self.remove == other.remove and self.install == other.install

   def __ne__(self, other):
      return not self.__eq__(other)

   @classmethod
   def FromXml(cls, xml):
      """Creates a MaintenanceMode instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new VibProvide object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = etree.fromstring(xml)
         except Exception as e:
            raise ValueError("Could not parse maintenance mode XML data: %s." %
                             e)

      install = None
      remove = True
      if xml.text:
         try:
            remove = XmlUtils.ParseXsdBoolean(xml.text)
         except Exception as e:
            raise ValueError(str(e))

      text = xml.get("on-install")
      if text:
         try:
            install = XmlUtils.ParseXsdBoolean(text)
         except Exception as e:
            raise ValueError(str(e))

      text = xml.get("on-remove")
      if text:
         try:
            remove = XmlUtils.ParseXsdBoolean(text)
         except Exception as e:
            raise ValueError(str(e))

      return cls(remove, install)

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("maintenance-mode")
      if self.install == self.remove:
         xml.text = self.remove and "true" or "false"
      else:
         xml.set("on-install", self.install and "true" or "false")
         xml.set("on-remove", self.remove and "true" or "false")
      return xml

#
# NOTE: UW and Mem Resource Constraints have been descoped from MN.
# However this class is still kept for OP implementation.
#
class ResourceConstraints(object):
   """A class representing resource pool constraints for userworld executables
      in a VIB.
      Attributes:
         * schedgrouppath    - A string, representing the scheduler group path
                               to which the memory & CPU constraints should be
                               applied.
      For more details on the attributes, please see section 2.1.20 of the VIB
      2.0 functional specification, "Memory and Resource Usage".
      #bora/apps/esxupdate/doc/vib20-spec.tex
      http://vmweb.vmware.com/~evan/MNDesign/vib20-spec.pdf
   """
   XML_TAGS = ("sched-group-path",
               "mem-size-limit-mb",
               )
   XML_TEXT_TAGS = ("sched-group-path",)

   def __init__(self, **kwargs):
      self.memsizelimitmb      = kwargs.pop("memsizelimitmb",      70)
      self.schedgrouppath      = kwargs.pop("schedgrouppath", "")

      if kwargs:
         badkws = ', '.join("'%s'" % kw for kw in kwargs)
         raise TypeError("Unrecognized keyword argument(s): %s." % badkws)

   def __eq__(self, other):
      for attr in (attr.replace("-", "") for attr in self.XML_TAGS):
         if getattr(self, attr) != getattr(other, attr):
            return False
      return True

   def __ne__(self, other):
      return not self.__eq__(other)

   @classmethod
   def FromXml(cls, xml):
      """Creates a ResourceConstraints instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new ResourceConstraints object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = etree.fromstring(xml)
         except Exception as e:
            raise ValueError("Could not parse resource constraint XML data: "
                             "%s." % e)

      kwargs = dict()
      for tag in cls.XML_TAGS:
         text = xml.findtext(tag)
         if text is not None:
            kw = tag.replace("-", "")
            try:
               if tag in cls.XML_TEXT_TAGS:
                  kwargs[kw] = text.strip()
               else:
                  kwargs[kw] = int(text)
            except Exception:
               raise ValueError("Invalid value '%s' for %s." % (text, tag))

      return cls(**kwargs)

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("resources")
      for tag in self.XML_TAGS:
         value = getattr(self, tag.replace("-", ""))
         etree.SubElement(xml, tag).text = str(value)

      return xml


class ArFileVib(BaseVib):
   """The ArFileVib class manages VIBs based on AR archives, and
      contains properties and methods for handling components of .vib
      files.  It may be used to cosntruct VIB archives -- see the
      WriteVibFile() method for more details.

      Class Variables:

      Attributes:
         * maintenancemode   - A boolean indicating whether the host must be in
                               maintenance mode before installing the VIB.
         * hwplatforms       - A set of HWPlatform instances, indicating the
                               hardware platforms for which the VIB is intended.
         * acceptancelevel   - A number representing the trust or acceptance
                               level of a VIB.  Not settable.
         * filelist          - A list containing the full paths of all files
                               in the (v)tar payload.
         * liveinstallok     - A boolean indicating whether live installation
                               is supported.
         * liveremoveok      - A boolean indicating whether live removal is
                               supported.
         * cimomrestart      - A boolean indicating whether cimom should be
                               restarted after a live install/removal.
         * statelessready    - A boolean indicating whether this VIB is ready
                               for stateless deployment.
         * overlay           - A boolean indicating whether this VIB is allowed
                               to overlay files from a non-overlay VIB
         * payloads          - A list of Payload instances, listing all the
                               payloads contained in this VIB.
         * resources         - An instance of ResourceConstraints.
         * signed            - Boolean property, True if VIB is signed
   """
   ACCEPTANCE_COMMUNITY = 'community'
   ACCEPTANCE_PARTNER = 'partner'
   ACCEPTANCE_ACCEPTED = 'accepted'
   ACCEPTANCE_CERTIFIED = 'certified'
   ACCEPTANCE_LEVELS = (ACCEPTANCE_COMMUNITY, ACCEPTANCE_PARTNER,
                        ACCEPTANCE_ACCEPTED, ACCEPTANCE_CERTIFIED,
                        # Temporary additions for compatibility:
                        'signed', 'unsigned')

   # Map acceptance level values to the value used in the config store
   # acceptance_level schema.
   TO_CS_ACCEPTANCE = {
      ACCEPTANCE_CERTIFIED: "VMWARE_CERTIFIED",
      ACCEPTANCE_ACCEPTED: "VMWARE_ACCEPTED",
      ACCEPTANCE_PARTNER: "PARTNER_SUPPORTED",
      ACCEPTANCE_COMMUNITY: "COMMUNITY_SUPPORTED"
   }
   FROM_CS_ACCEPTANCE = dict((v, k) for (k, v) in list(TO_CS_ACCEPTANCE.items()))

   ATTRS_TO_VERIFY = BaseVib.ATTRS_TO_VERIFY + ('acceptancelevel', 'filelist',
         'overlay', 'payloads', 'resources')
   ATTRS_TO_COPY = BaseVib.ATTRS_TO_COPY +  ('maintenancemode', 'hwplatforms',
         'swplatforms', 'liveinstallok', 'liveremoveok')

   def __init__(self, **kwargs):
      self.maintenancemode = kwargs.pop('maintenancemode', MaintenanceMode())
      self.hwplatforms     = kwargs.pop('hwplatforms', set())
      self.acceptancelevel = kwargs.pop('acceptancelevel', "community")
      self.filelist        = kwargs.pop('filelist', set())
      self.liveinstallok   = kwargs.pop('liveinstallok', False)
      self.liveremoveok    = kwargs.pop('liveremoveok', False)
      self.cimomrestart    = kwargs.pop('cimomrestart', False)
      self.statelessready  = kwargs.pop('statelessready', False)
      self.overlay         = kwargs.pop('overlay', False)
      self.payloads        = kwargs.pop('payloads', list())
      self.resources       = kwargs.pop('resources', ResourceConstraints())

      self.swplatforms = list()
      for p in kwargs.pop('swplatforms', list()):
         if isinstance(p, SoftwarePlatform):
            self.swplatforms.append(p)
         else:
            # Devkit scons build passes in tuples.
            self.swplatforms.append(SoftwarePlatform(*p))

      BaseVib.__init__(self, **kwargs)

      # We rely on self.payloads being sorted in order for comparison with
      # other VIB objects.
      self.payloads.sort(key = operator.attrgetter("name"))

      self._fileorigin = None
      self._arfile = None

   def __eq__(self, other):
      for attr in self.ATTRS_TO_VERIFY:
         if getattr(self, attr) != getattr(other, attr):
            return False

      return True

   def __ne__(self, other):
      return not self.__eq__(other)

   signed = property(lambda self: bool(self._pkcs7))

   @property
   def hasConfigSchema(self):
      """Returns if the VIB has a config schema.
      """
      return self.GetConfigSchemaTag() is not None

   def MergeVib(self, other):
      '''Merges attributes between two VIBs.
         BaseVib.MergeVib() will do most of the work, only statelessready
         attribute specific to ArFileVib needs to be merged.
      '''
      retvib = BaseVib.MergeVib(self, other)
      # Set statelessready tag to the 'or' operation of the two
      # input vibs. See PR 1290763.
      attr = 'statelessready'
      retvib.statelessready = getattr(self, attr) or getattr(other, attr)
      return retvib

   @classmethod
   def _XmlToKwargs(cls, xml):
      '''Converts XML to kwargs for the constructor.
         With signed ArFile VIB, caller of this method (FromXml) should be
         using the original descriptor for all attributes except installdate.
         This way tampering of the descriptor will not take effect. Tampering
         of the original descriptor can be revealed with signature check.
      '''
      kwargs = BaseVib._XmlToKwargs(xml)

      # Although, if we've gotten this far without a name and version,
      # something is probably wrong.
      vibnv = "%s-%s" % (kwargs.get("name", "(unknown)"),
                         kwargs.get("version", "(unknown)"))

      elem = xml.find("system-requires/maintenance-mode")
      if elem is not None:
         try:
            kwargs["maintenancemode"] = MaintenanceMode.FromXml(elem)
         except Exception as e:
            msg = "VIB '%s' has invalid maintenance mode: %s" % (vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)

      hwplatforms = set()
      for elem in xml.findall("system-requires/hwplatform"):
         try:
            hwplatforms.add(HwPlatform.FromXml(elem))
         except Exception as e:
            msg = "VIB '%s' has invalid hwplatform: %s" %(vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)
      if len(hwplatforms):
         kwargs["hwplatforms"] = hwplatforms

      swPlatforms = list()
      for elem in xml.findall('system-requires/softwarePlatform'):
         try:
            swPlatforms.append(SoftwarePlatform.FromXml(elem))
         except Exception as e:
            msg = ("VIB '%s' has invalid softwarePlatform: %s"
                   % (vibnv, e))
            raise Errors.VibFormatError(vibnv, msg)
      if swPlatforms:
         kwargs['swplatforms'] = swPlatforms

      filelist = set()
      for elem in xml.findall("file-list/file"):
         if elem.text:
            text = elem.text.strip()
            if text:
               filelist.add(text)
      if len(filelist):
         kwargs["filelist"] = filelist

      text = (xml.findtext("acceptance-level") or "community").strip()
      if text not in cls.ACCEPTANCE_LEVELS:
         msg = "VIB '%s' has invalid acceptance level '%s'." % (vibnv, text)
         raise Errors.VibFormatError(vibnv, msg)
      kwargs["acceptancelevel"] = text

      for tag in ("live-install-allowed", "live-remove-allowed",
                  "cimom-restart", "stateless-ready"):
         text = xml.findtext(tag) or "false"
         try:
            kw = tag.replace("-", "").replace("allowed", "ok")
            kwargs[kw] = XmlUtils.ParseXsdBoolean(text)
         except Exception:
            msg = ("VIB '%s' has invalid %s value '%s'." %
                   (vibnv, tag, text))
            raise Errors.VibFormatError(vibnv, msg)

      # Enable this in OP
      #elem = xml.find("resources")
      #if elem is not None:
      #   try:
      #      kwargs["resources"] = ResourceConstraints.FromXml(elem)
      #   except Exception as e:
      #      msg = "VIB '%s' has invalid resource constraints: %s" % (vibnv, e)
      #      raise Errors.VibFormatError(None, msg)

      text = xml.findtext("overlay")
      if text:
         try:
            kwargs["overlay"] = XmlUtils.ParseXsdBoolean(text)
         except:
            msg = ("VIB '%s' has invalid overlay value '%s'." % (vibnv, text))
            raise Errors.VibFormatError(vibnv, msg)

      kwargs["payloads"] = list()
      payloadnames = set()
      for elem in xml.findall("payloads/payload"):
         try:
            payload = Payload.FromXml(elem)
            if payload.name in payloadnames:
               msg = "Duplicate payload name '%s'." % payload.name
               raise Errors.VibFormatError(vibnv, msg)
            kwargs["payloads"].append(payload)
         except Exception as e:
            msg = "VIB '%s' has invalid payload: %s" % (vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)

      return kwargs

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = BaseVib.ToXml(self)

      elem = etree.SubElement(xml, "system-requires")
      elem.append(self.maintenancemode.ToXml())
      for hwplatform in self.hwplatforms:
         elem.append(hwplatform.ToXml())
      for swPlatform in self.swplatforms:
         elem.append(swPlatform.ToXml())

      if self.filelist:
         # Python sets are unordered, but we want filelist to be deterministic.
         filelist = list(self.filelist)
         filelist.sort()
         elem = etree.SubElement(xml, "file-list")
         for filename in filelist:
            etree.SubElement(elem, "file").text = filename

      elem = etree.SubElement(xml, "acceptance-level")
      elem.text = self.acceptancelevel

      elem = etree.SubElement(xml, "live-install-allowed")
      elem.text = self.liveinstallok and "true" or "false"

      elem = etree.SubElement(xml, "live-remove-allowed")
      elem.text = self.liveremoveok and "true" or "false"

      elem = etree.SubElement(xml, "cimom-restart")
      elem.text = self.cimomrestart and "true" or "false"

      elem = etree.SubElement(xml, "stateless-ready")
      elem.text = self.statelessready and "true" or "false"

      # Enable this in OP
      #xml.append(self.resources.ToXml())

      etree.SubElement(xml, "overlay").text = self.overlay and "true" or "false"

      elem = etree.SubElement(xml, "payloads")
      for payload in self.payloads:
         elem.append(payload.ToXml())

      return xml

   def VerifyAcceptanceLevel(self, schemaonly=False):
      """Validates the acceptance level of this VIB against the policy for that
         acceptance level.
            Raises:
               * VibFormatError     - The VIB specifies an invalid acceptance
                                      level.
               * VibSigMissingError - The VIB is not signed.
               * VibSigFormatError  - The VIB signature does not have the
                                      appropriate format.
               * VibSigInvalidError - The VIB signature cannot be verified to
                                      be signed by a trusted CA.
               * VibSigDigestError  - The digest from the PKCS7 signature does
                                      not match a digest computed for the
                                      descriptor text.
      """
      try:
         policyobj = AcceptanceLevels.GetPolicy(self.acceptancelevel)
      except KeyError:
         msg = "Invalid acceptance level '%s'." % self.acceptancelevel
         raise Errors.VibFormatError(self.id, msg)

      if schemaonly:
         policyobj.VerifySchema(self)
      else:
         policyobj.Verify(self)

   def VerifySignature(self, verifyobj=None, cacerts=None, crls=None,
                       checkCertDates=False):
      """Validates the cryptographic signature of the descriptor.
            Parameters:
               * verifyobj      - An instance of VibSign.VibSigner that can be
                                  used to verify the signature.
               * cacerts        - A list of files to read CA certificates from.
                                  The CA certificates are used to validate the
                                  trust of the signer.
               * crls           - A list of files to read CRLs from.
               * checkCertDates - Whether to perform a stricter check on cert
                                  expiration and invalid Not-Before/Not-After
                                  dates.
            Returns: A VibSigner.SignerInfo object with information about the
                     signer.
            Exceptions:
               VibSigMissingError - The VIB is not signed.
               VibSigFormatError  - The VIB signature does not have the
                                    appropriate format.
               VibSigInvalidError - The VIB signature cannot be verified to be
                                    signed by a trusted CA.
               VibSigDigestError  - The digest from the PKCS7 signature does
                                    not match a digest computed for the
                                    descriptor text.
      """
      if not self._pkcs7:
         msg = "The VIB %s does not contain a signature." % (self.id)
         raise Errors.VibSigMissingError(self.id, msg)

      VibSign = loadVibSign()
      if verifyobj is None:
         if VibSign is None:
            msg = "Can not verify VIB signature: Required module missing."
            raise Errors.VibSigInvalidError(self.id, msg)
         try:
            verifyobj = VibSign.VibSigner(cacerts=cacerts, crls=crls)
         except Exception as e:
            msg = "Error creating VibSigner object: %s" % e
            raise Errors.VibSigInvalidError(self.id, msg)

      # For backward cert store compatibility, convention of storing signatures
      # is to put newer signature at the end. When checking signature, start
      # from the end and stop when a pass happens.
      for i in range(len(self._pkcs7) - 1, -1, -1):
         pkcs7 = self._pkcs7[i]
         try:
            logger.debug('Verifying VIB %s signature #%d' %
                         (self.id, i + 1))
            try:
               signer = verifyobj.Verify(self._signeddesctext, pkcs7,
                                         checkCertDates)
            except TypeError:
               logger.warning("Ignoring the argument, checkCertDates")
               signer = verifyobj.Verify(self._signeddesctext, pkcs7)
            break
         except VibSign.PKCS7FormatError as e:
            err = Errors.VibSigFormatError(self.id, str(e))
         except VibSign.PKCS7DigestError as e:
            err =  Errors.VibSigDigestError(self.id, str(e))
         except VibSign.PKCS7CertError as e:
            err = Errors.VibSigInvalidError(self.id, str(e))
         except Exception as e:
            msg = "Unexpected error validating VIB signature: %s" % e
            err = Errors.VibSigInvalidError(self.id, msg)
         # Last signature failed to verify, propagate error
         if i == 0:
            raise err
         # Log error and try next one
         else:
            logger.error('Failed to verify VIB signature #%d: %s' %
                         (i + 1, str(err)))

      return signer

   def IterPayloads(self, checkDigests=False):
      """A Generator method that iterates over each payload of the VIB.
         Parameters:
            * checkDigests: If true, provide hashed payload object to perform
                            checksum verification.
         Returns:
            An iterator object. Each iteration returns a tuple (payload,
            fileobj):
             * payload - An instance of Payload describing the VIB payload
             * fileobj - a File-like object for reading from the payload;
                         supports read(), tell(), and close().
         Throws:
            * VibFormatError - if a corresponding file object cannot be found
                               for a payload.
      """
      # If we have any payloads with a fileobj already set, it might be
      # because one of the Installer classes specifically set it. So we default
      # to using that one first.
      for payload in self.payloads:
         if payload.fileobj is not None:
            fileObj = payload.fileobj
         elif self._arfile:
            fileObj = None
            for info, fobj in self._arfile:
               if info.filename == payload.name:
                  fileObj = fobj
                  break
            if fileObj is None:
               msg = ("Could not find file object for payload '%s'." %
                      payload.name)
               raise Errors.VibFormatError(self.id, msg)
         else:
            continue

         if checkDigests:
            fileObj = wrapPayloadReadFileobj(payload, fileObj, decompress=False,
                                             checkdigest=True)
         yield payload, fileObj

   def CheckPayloadDigests(self):
      """Iterate over all the payloads in this VIB, and verifies checksums for
      these payloads.
      """
      for payload, fobj in self.IterPayloads(checkDigests=True):
         try:
            while fobj.read(PAYLOAD_READ_CHUNKSIZE):
               pass
         except HashError as e:
            raise Errors.VibPayloadDigestError(self.id, payload.name, str(e))
         finally:
            fobj.close()

   @classmethod
   def FromFile(cls, f, schema=None):
      """Constructs an ArFileVib instance from a File object stream.
         Reads in the descriptor and optional signature, moving the
         file pointer to the first payload member.

         Parameters:
            * f - a File-like object supporting read() and close() methods,
                  or the path to a VIB file to read from.
            * schema - A file path to an XML schema for validation; must be in
                  .rng, .xsd, or .dtd format.  If None is passed in, then
                  schema validation is skipped.
         Exceptions:
            VibFormatError - AR members unexpected or descriptor XML invalid
                             or fails schema validation
      """
      if isString(f):
         srcfile = f
         try:
            fileobj = open(f, 'rb')
         except Exception as e:
            raise Errors.VibFormatError(f, str(e))
      else:
         fileobj = f
         if hasattr(f, 'name'):
            srcfile = f.name
         elif hasattr(f, 'url'):  #URLGrabberFileObject has url attribute
            srcfile = f.url
         else:
            srcfile = str(f)
      try:
         try:
            ar = ArFile.ArFile(fileobj=fileobj)
            info, memberfp = ar.next()
         except ArFile.ArError:
            raise Errors.VibFormatError(srcfile, "Bad VIB archive header")
         except StopIteration:
            raise Errors.VibFormatError(srcfile, "VIB archive is too short")
         except Exception as e:
            raise Errors.VibFormatError(srcfile, "Unable to open VIB archive "
                                    "in streaming mode: %s" % (str(e)))

         # read in descriptor
         if info.filename != 'descriptor.xml':
            if info.filename == 'debian-binary':
               msg = ("This VIB is a ESX 4.x VIB and is not supported on "
                      "this version of ESX.")
            else:
               msg = ("This VIB has an unsupported format.  The first member "
                      "found was %s, but descriptor.xml was expected." %
                      (info.filename))
            raise Errors.VibFormatError(srcfile, msg)

         try:
            desctext = memberfp.read()
            if sys.version_info[0] >= 3:
               desctext = desctext.decode()
         except Exception as e:
            raise Errors.VibFormatError(srcfile,
               "Unable to read VIB descriptor: %s" % (str(e)))
         if schema:
            vib = cls.FromXml(desctext, schema = schema)
         else:
            vib = cls.FromXml(desctext, validate = False)
         # preserve original descriptor
         vib._signeddesctext = desctext
         vib._arfile = ar
         if hasattr(fileobj, "name"):
            vib._fileorigin = fileobj.name

         # read in signature, if present
         try:
            info, memberfp = ar.next()
         except ArFile.ArError:
            raise Errors.VibFormatError(srcfile, "Bad VIB archive header")
         except StopIteration:
            raise Errors.VibFormatError(srcfile, "VIB archive is too short")
         except Exception as e:
            msg = "Unable to read VIB archive: %s" % str(e)
            raise Errors.VibFormatError(srcfile, msg)

         if info.filename != 'sig.pkcs7':
            raise Errors.VibFormatError(srcfile,
               "Expected second member of Vib file to be sig.pkcs7, but found "
               "%s instead." % (info.filename))
         if info.size:
            try:
               vib.SetSignature(memberfp.read())
            except Exception as e:
               raise Errors.VibFormatError(srcfile,
                  "Unable to read VIB signature: %s" % (str(e)))
         else:
            vib.ClearSignature()

      except (Errors.VibValidationError, Errors.VibFormatError, Errors.VibIOError):
         if isString(f):
            fileobj.close()
         raise

      return vib

   def OpenFile(self, f):
      """Associates a file object with the VIB.
            Paramaters:
               * f - A string giving a file path, or a file-like object.
            Raises:
               * VibFormatError - If the file content is not in the expected
                                  format.
            Note: This method does not implicitly call VerifySignature(). This
                  may be done, however, after this method call returns.
      """
      new = self.FromFile(f)
      for attr in self.ATTRS_TO_VERIFY + self.ATTRS_TO_COPY:
         expected = getattr(self, attr)
         found = getattr(new, attr)
         if expected != found:
            msg = ("Error opening file object for VIB '%s': Expected value "
                   "'%s' for attribute '%s', but found value '%s'."
                   % (self.id, expected, attr, found))
            raise Errors.VibFormatError(str(f), msg)

      self._arfile = new._arfile
      self._pkcs7 = new._pkcs7
      self._signeddesctext = new._signeddesctext

   def AddPayload(self, payloadobj, filepath, xzip=None, tmpDir=None,
                  objcopy=None):
      """Adds a payload at filepath to the VIB.  If the payload name does not
         match one of the existing payloads, the payload will be added at the
         end.  If the payload matches an existing payload, then that payload
         will be replaced in the same archive position when WriteVibFile()
         is called.  The size and fileobj attributes of the payloadobj
         will be modified.

         Parameters:
            * payloadobj - an instance of Payload
            * filepath   - the file path to the payload binary to add
            * xzip       - whether the payload is xzip compressed. This
                           should *only* be used during ESXi build.
            * tmpDir     - the temporary directory where xz uncompression
                           takes place. Only to be used during ESXi build.
            * objcopy    - Only used during build time for vmkboot payload.
                           If path is given, extract text segment of the
                           payload using objcopy and calculate txt-mle hash.
         Exceptions:
            VibIOError
      """
      # check the payload file exists
      try:
         payloadobj.fileobj = open(filepath, 'rb')
      except IOError:
         raise Errors.VibIOError(filepath, "Unable to access payload")

      # Add file size
      try:
         payloadobj.size = os.stat(filepath).st_size
      except Exception:
         raise Errors.VibIOError(filepath, "Unable to get file size")

      try:
         # XXX: Backward compatibility hack
         # Checksum type is confined to sha-256 and sha-1 in ESXi 6.0 and
         # before. By putting new checksums at the start of list, regular
         # sha-256 without special verify process will be returned by
         # GetPreferredChecksum() for payload verification, otherwise VIB
         # install for new VIBs on older hosts will fail.

         # Produce txt-mle checksum for vmkboot during build
         if objcopy is not None:
            cksum_obj = getTxtMleChecksum(payloadobj, objcopy, tmpDir)
            payloadobj.checksums.append(cksum_obj)
            payloadobj.fileobj.seek(0)

         cktypes = [("sha-256", "gunzip"), ("sha-256", ""), ("sha-1", "gunzip")]
         for cksum_type, verifier_type in cktypes:
            decompress = (verifier_type == "gunzip")
            if decompress and payloadobj.payloadtype not in Payload.GZIP_TYPES:
               # "gunzip" verifier only makes sense for things that are
               # actually gzip'ed.
               continue
            checksum = calculatePayloadChecksum(payloadobj, payloadobj.fileobj,
                                                cksum_type, decompress)
            cksum_obj = Checksum(cksum_type, checksum, verifier_type)
            payloadobj.checksums.append(cksum_obj)
            payloadobj.fileobj.seek(0)
      except Exception as e:
         msg = ("Error calculating hash for payload %s: %s" %
                (payloadobj.name, e))
         raise Errors.VibSigningError(self.id, msg)

      if payloadobj.payloadtype in (payloadobj.TYPE_VGZ, payloadobj.TYPE_TGZ):
         tar = None
         try:
            tar = getTarFromPayload(payloadobj, xzip, tmpDir)

            for member in tar.getmembers():
               if member.type != tarfile.DIRTYPE:
                  self.filelist.add(member.name)

                  # If the file is a config schema, extract it and add
                  # the swtag based on the schema's metadata.
                  if member.name.startswith(CONFIG_SCHEMA_DIR):
                     extractDir = tempfile.mkdtemp(dir=tmpDir)
                     try:
                        tar.extract(member, path=extractDir)
                        filePath = os.path.join(extractDir, member.name)
                        csTag = ConfigSchemaSoftwareTag.FromPayloadFile(
                                                               filePath,
                                                               payloadobj.name,
                                                               member.name)
                     finally:
                        shutil.rmtree(extractDir)
                     self.swtags.append(csTag.ToString())

            payloadobj.fileobj.seek(0)
         finally:
            if tar is not None:
               tar.close()

      for i in range(len(self.payloads)):
         if self.payloads[i].name == payloadobj.name:
            self.payloads[i] = payloadobj
            return
      self.payloads.append(payloadobj)

      # We rely on self.payloads being sorted in order for comparison with
      # other VIB objects.
      self.payloads.sort(key = operator.attrgetter("name"))

   def GetDescriptorText(self):
      """Gets descriptor.xml text as a string.

         This function should be called to get the descriptor text
         before generating a signature for the vib
      """
      if sys.version_info[0] >= 3:
         encoding = 'unicode'
      else:
         encoding = 'us-ascii'

      try:
         self._signeddesctext = etree.tostring(self.ToXml(), encoding=encoding)
      except Exception as e:
         msg = "Error serializing VIB metadata to XML: %s" % e
         raise Errors.VibFormatError(self.id, msg)
      return self._signeddesctext

   def AddSignatureText(self, signaturetext):
      """Adds signature data to the VIB. Descriptor is not validated against
         the signature; it is the caller's responsibility to ensure that the
         object is not modified between calling GetDescriptorText and
         AddSignatureText, or between AddSignatureText and WriteVibFile.
            Parameters:
               * signaturetext - A string containing a PKCS7 signature of the
                                 descriptor.xml.
      """
      if sys.version_info[0] >= 3 and isinstance(signaturetext, str):
         signaturetext = signaturetext.encode()
      # Signature text is appended directly to self._pkcs7 list
      self._pkcs7.append(signaturetext.strip(b'\n'))

   def AddSignature(self, signerkey, signercert, extracerts=None):
      """Adds signature data to a VIB.
            Parameters:
               * signerkey  - A string giving the path name to the signer's
                              private key. Must be in PEM format.
               * signercert - A string giving the path name to the signer's
                              certificate. Must be in PEM format.
               * extracerts - A list of strings giving file names. Each file
                              name is searched for additional certificates to
                              add to the PKCS7 data.
            Returns: None
            Raises:
               * Errors.VibSigningError - on any problem generating the
                                          digest or signing it.
               * Errors.VibFormatError  - The descriptor data could not be
                                          serialized to a string.
            Notes: Any changes to the object's attributes which are stored in
                   descriptor.xml metadata will invalidate the signature, and
                   require calling this method again.
      """
      VibSign = loadVibSign()
      if VibSign is None:
         msg = "Do not have required signing function."
         raise Errors.VibSigningError(self.id, msg)

      desctext = self.GetDescriptorText()

      try:
         signer = VibSign.VibSigner(signerkey = signerkey,
                                    signercert = signercert,
                                    extracerts = extracerts)
         self.AddSignatureText(signer.Sign(desctext))
      except Exception as e:
         msg = "Error signing VIB: %s" % e
         raise Errors.VibSigningError(self.id, msg)

   def WriteVibFile(self, destfile):
      """Writes out a VIB to the file path destfile.  The payloads are written
         according to the order in the payloads attribute.
         If the payload has a fileobj, it tries to read the data from
         fileobj.  If fileobj is None, see if we can read it
         from the current AR archive.   The current file position
         must be at the first payload when this method is called, which means
         that after a WriteVibFile in which payloads are copied from the
         current AR archive, it cannot be called again.
         Note that the caller must make sure destfile is not the same as the
         source file.

         To create a VIB archive from scratch:
            vib = Vib.ArFileVib(name='myvib', ...)
            p1 = Vib.Payload(name='abc.tgz', payloadtype=...)
            vib.AddPayload(p1, filepath)
            vib.WriteVibFile(...)

         To add a payload to an existing VIB:
            vib = Vib.ArFileVib.FromFile('/path/to/my.vib')
            vib.AddPayload(p1, filepath)
            [any other changes to the VIB object]
            vib.WriteVibFile(...)

         To replace a payload, use the AddPayload() method with one of the
         existing payload names.  This populates the fileobj attribute and
         causes this method to write the payload from a file instead of from
         the current archive.

         To delete a payload, remove the payload from payloads list before
         invoking this method.

         Parameters:
            * destfile - The file path to write a VIB to.
         Exceptions:
            VibIOError - a payload instance cannot be sourced: fileobj is not
                         a file and the corresponding payload cannot be found
                         in the current AR archive (or there is no archive)
      """
      try:
         ar = ArFile.ArFile(name=destfile, mode='wb')

         # Write out descriptor
         # TODO: investigate when original descriptor is stored as bytes and
         # correct to string.
         if self._signeddesctext:
            desctext = (self._signeddesctext.encode()
                        if isString(self._signeddesctext)
                        else self._signeddesctext)
         else:
            desctext = etree.tostring(self.ToXml())
         ar.Writestr(ArFile.ArInfo("descriptor.xml"), desctext)

         # Write out signature
         ar.Writestr(ArFile.ArInfo("sig.pkcs7"), self.GetSignature())

         # Write out the payloads in the payloads attribute
         # If the payload has a fileobj, try to read the data from
         # fileobj.  If fileobj is None, see if we can read it
         # from the current AR archive.   The current file position
         # must be at the first payload for the above to work.
         for payload, sfp in self.IterPayloads():
            ar.Writefile(ArFile.ArInfo(payload.name, size=payload.size), sfp)
            sfp.close()

         ar.Close()
      except (EnvironmentError, ArFile.ArError) as e:
         raise Errors.VibIOError(destfile, "Error writing VIB file: %s"
                                 % (str(e)))

   def GetFileOrigin(self):
      """Gets the original vib file name."""
      return self._fileorigin

   def GetModifiedConf(self):
      """ Gets all the config files which are modified in the system from a vib.
      """
      modifiedconf = []
      for fn in self.filelist:
         normalfile = PathUtils.CustomNormPath('/' + fn)
         fn_path = os.path.dirname(normalfile)
         fn_name = os.path.basename(normalfile)
         fn_name = '.#%s' % fn_name
         branch_fn = os.path.join(fn_path, fn_name)
         # if original file exists, .# file exists,
         # and original file has sticky bit on
         if os.path.isfile(normalfile) and \
               os.path.isfile(branch_fn) and \
               os.stat(normalfile).st_mode & stat.S_ISVTX:
            modifiedconf.append(fn)
      return modifiedconf

   def GetConfigSchemaTag(self):
      """Returns a ConfigSchemaSoftwareTag objects representing this
         one config schema software tag.
      """
      csTags = [ConfigSchemaSoftwareTag.FromString(s) for s in self.swtags
                if s.startswith(ConfigSchemaSoftwareTag.CONFIG_SCHEMA_MAGIC)]
      if len(csTags) > 1:
         raise ValueError('%u config schema tags found, expect at most 1'
                          % len(csTags))
      return csTags[0] if csTags else None

   def HasPlatform(self, platform):
      """Checks if the Vib is intended for given software platform.
      """
      # Backward compatibility for VIBs without platform info, assume they
      # are all for embeddedEsx.
      if platform == SoftwarePlatform.PRODUCT_EMBEDDEDESX and \
         not self.swplatforms:
         return True

      for sp in self.swplatforms:
         if sp.productLineID == platform:
            return True

      return False

   def Close(self):
      if self._arfile:
         self._arfile.Close()
         self._arfile = None

def wrapPayloadReadFileobj(payload, srcfObj, decompress, checkdigest):
   '''Get a wrapped payload fileobj for decompression and/or checksum check
      during read, typically used when iterating a VIB with IterPayloads.
      Parameters:
         payload                - Payload class object
         srcfObj                - read file object associated with the payload,
                                  returned after wrapping
         decompress/checkdigest - True/False flags indicating decompress and
                                  checksum check requirements
      Returns:
         A wrapped file object based on srcfObj
      Scenarios:
         decompress             - return unzip stream
         checkdigest            - return a HashedStream verifying regular
                                  stream with regular checksum
         decompress/checkdigest - return a EsxGzip stream while using
                                  HashedStream to verify checksum of the
                                  original file

      Raises:
         ValueError - when the payload object does not have the needed checksum
   '''
   if checkdigest:
      # Verify complete payload checksum everytime.
      checksum = payload.GetPreferredChecksum(verifyprocess='')
      if not checksum:
         msg = 'Payload %s does not have a checksum' % payload.name
         raise ValueError(msg)
      srcfObj = HashedStream(srcfObj, checksum.checksum,
                             checksum.checksumtype.replace('-', ''))

   if decompress:
      if payload.payloadtype in Payload.GZIP_TYPES:
         srcfObj = EsxGzip.GzipFile(mode='rb', fileobj=srcfObj)

   return srcfObj

def wrapPayloadWriteFileobj(payload, destfObj, compress):
   '''Get a wrapped payload fileobj for compressing payload data, typically
      used when staging from /tardisks to the stage bootbank.
   '''
   if compress:
      if payload.payloadtype in Payload.GZIP_TYPES:
         destfObj = EsxGzip.GzipFile(mode='wb', fileobj=destfObj)
   return destfObj

def calculatePayloadChecksum(payload, srcfObj, hashAlgo, decompress):
   '''Calculate checksum on a payload file object.
      Parameters:
         payload    - Payload class object
         srcfObj    - payload file for read, will not be closed after read
         hashAlgo   - hashing algorithm to use
         decompress - if to calculate decompressed checksum;
                      when set to True, wrapPayloadReadFileobj() is used to
                      provide the proper file object for decompression
      Return:
         Digest of the file in hex
   '''
   if decompress:
      srcfObj = wrapPayloadReadFileobj(payload, srcfObj, decompress=True,
                                       checkdigest=False)
   hs = HashedStream(srcfObj, method=hashAlgo.replace('-', ''))
   data = hs.read(PAYLOAD_READ_CHUNKSIZE)
   while data:
      data = hs.read(PAYLOAD_READ_CHUNKSIZE)
   return hs.hexdigest

def getTxtMleChecksum(payloadobj, objcopy, tmpDir):
   """Calculate txt-mle checksum for the payload, returns a Checksum object.
      This function should be only called at build time for vmkboot payload
      specifically.
   """
   # Unzip payload
   with tempfile.NamedTemporaryFile(dir=tmpDir, delete=False) as tmp_extract:
      f = wrapPayloadReadFileobj(payloadobj, payloadobj.fileobj,
                                 decompress=True, checkdigest=False)
      data = f.read(PAYLOAD_READ_CHUNKSIZE)
      while data:
         tmp_extract.write(data)
         data = f.read(PAYLOAD_READ_CHUNKSIZE)
      tmp_extract.flush()
   with tempfile.NamedTemporaryFile(dir=tmpDir) as tmp_text:
      # Dump text part of vmkboot
      subprocess.check_call([objcopy,
                             '--output-target=binary',
                             '--only-section=.text',
                             tmp_extract.name,
                             tmp_text.name])
      checksum = calculatePayloadChecksum(payloadobj, tmp_text, 'sha256',
                                          decompress=False)
      cksum_obj = Checksum('sha-256', checksum, 'txt-mle')
      os.remove(tmp_extract.name)
      return cksum_obj

def getTarFromPayload(payloadobj, xzip, tmpDir=None):
   ''' Return a given payload as a tarfile object.
       Caller needs to provide a temporary directory in case the payload
       is xzipped. It is caller's reponsibility to cleanup the directory.

       Parameter:
         xzip   - Path to xzip binary.
         tmpDir - A temporary diretory to use, if not given the system default
                  is used.
   '''
   DEFAULT_XZ = '/bin/xz'
   XZ_MAGIC = b'\xfd7zXZ\x00'

   if payloadobj.payloadtype == payloadobj.TYPE_VGZ:
      src = wrapPayloadReadFileobj(payloadobj, payloadobj.fileobj,
                                   decompress=True, checkdigest=False)
      isXzipped = src.read(len(XZ_MAGIC)) == XZ_MAGIC
      # Seek back, the file object might be a .vib file and seeking an absolute
      # location is incorrect.
      src.seek(-len(XZ_MAGIC), 1)
      if isXzipped:
         # xzip payload, extract to a temporary file
         if xzip is None:
            if os.path.exists(DEFAULT_XZ):
               xzip = DEFAULT_XZ
            else:
               raise ValueError("Payload %s is xzipped, but no xzip was "
                                "provided" % payloadobj.name)

         with tempfile.NamedTemporaryFile(dir=tmpDir) as gunzipFd:
            with tempfile.NamedTemporaryFile(dir=tmpDir, delete=False) as tarFd:
               # Gzip decompress to gunzipFd and then xz decompress to tarFd.
               # gunzipFd file is deleted after use, while tarFd file is kept
               # in-place assuming tmpDir would be cleaned up afterwards.
               #
               # --stdin input to xz is not used since xz can close pipe before
               # receiving the appended signature after the compressed payload.
               shutil.copyfileobj(src, gunzipFd)

               # Suppress errors from xzip since we know that it will complain
               # about the signature appended.
               # To suppress errors --quiet needs to be passed twice, and
               # return code cannot be checked since it will be 1.
               subprocess.call("%s --decompress --stdout --quiet --quiet "
                               "--check=crc32 %s" % (xzip, gunzipFd.name),
                               stdout=tarFd,
                               shell=True)

               # Check file size instead.
               if os.path.getsize(tarFd.name) == 0:
                  raise RuntimeError('Failed to xz decompress payload %s'
                                     % payloadobj.name)

               tar = VmTar.Open(name=tarFd.name, mode='r')
      else:
         tar = VmTar.Open(fileobj=src, mode='r')
   elif payloadobj.payloadtype == payloadobj.TYPE_TGZ:
      tar = tarfile.open(mode="r:gz", fileobj=payloadobj.fileobj)
   else:
      raise ValueError('Payload type %s is not a regular tar type payload'
                       % payloadobj.payloadtype)
   return tar

def getConfigSchema(vib, xzip, tmpDir=None):
   """Returns a config schema object from the VIB, or None when the VIB has
      no config schema. Requires the VIB object to be backed by the actual
      VIB file so payload can be extracted.
      Parameters: similar to getTarFromPayload().
   """
   csTag = vib.GetConfigSchemaTag()
   if csTag is None:
      return None
   for payload, fObj in vib.IterPayloads():
      if payload.name == csTag.vibPayload:
         payload.fileobj = fObj
         tar = getTarFromPayload(payload, xzip, tmpDir)
         extractDir = tempfile.mkdtemp(dir=tmpDir)
         try:
            tar.extract(csTag.payloadFilePath, path=extractDir)
            filePath = os.path.join(extractDir, csTag.payloadFilePath)
            cs = ConfigSchema.FromFile(filePath)
         finally:
            shutil.rmtree(extractDir)
         return cs
   raise RuntimeError('Unable to find payload %s that contains a config '
                      'schema' % csTag.vibPayload)

def copyPayloadFileObj(payload, srcfObj, destfObj, compress=False,
                       decompress=False, checkdigest=False, checksize=False):
   """Copy contents from a payload file object to another file object.
      In compress mode, the destfObj will be compressing the data stream
      from srcfObj according to the payload type.
      In decompress mode, the srcfObj is decompressed according to the
      payload type and optional has its checksum verified during the write.
      In checkdigest mode, the srcfObj is copied to the destfObj with its
      checksum verified during the write.
      In checksize mode, the size of srcfObj is compared with the size field
      provided via descriptor to detect any tampering in VIB during
      installation. It is kept False as default as some existing payloads
      present on the host may have been gzipped using a different gzip tool
      than the build process, resulting in a different size.
   """
   if compress and decompress:
      raise ValueError('Only one of compress or decompress can be set')
   if compress:
      if checkdigest:
         # compress + checkdigest is not implemented now since the only
         # use case is to stage a tardisk from /tardisks and it was verified
         # before mounting.
         raise NotImplementedError('checkdigest is not implemented with '
                                   'compress mode')
      destfObj = wrapPayloadWriteFileobj(payload, destfObj, True)
   else:
      # decompress + checkdigest, or just checkdigest
      if isinstance(srcfObj, ArFile._FileInFile):
         size = srcfObj.size
      else:
         size = os.fstat(srcfObj.fileno()).st_size

      # Special case for payloads that encapsulate state.
      # These payloads can change. Hence their sizes
      # will not match the ones in the header.
      # TODO: Since OEMs can add additional useropts modules,
      # we will need to get this list from the kernel.
      if payload.name not in CHECKSUM_EXEMPT_PAYLOADS and size != payload.size:
         msg = "Payload %s size does not match with the one specified in " \
               "descriptor, '%s' != '%s'" % (payload.name, size, payload.size)
         if checksize:
            raise ValueError(msg)
         logger.warning(msg)

      srcfObj = wrapPayloadReadFileobj(payload, srcfObj, decompress,
                                       checkdigest)
   totalBytes = 0
   inBytes = srcfObj.read(PAYLOAD_READ_CHUNKSIZE)
   while inBytes:
      destfObj.write(inBytes)
      totalBytes += len(inBytes)
      inBytes = srcfObj.read(PAYLOAD_READ_CHUNKSIZE)
   logger.debug('Total of %d bytes were written.' % totalBytes)

BaseVib._vib_classes[BaseVib.TYPE_BOOTBANK] = ArFileVib
BaseVib._vib_classes[BaseVib.TYPE_META] = ArFileVib
BaseVib._vib_classes[BaseVib.TYPE_LOCKER] = ArFileVib
