Source code for COT.vm_description.ovf.name_helper

#!/usr/bin/env python
#
# name_helper.py - Handling the many XML names in an OVF descriptor
#
# June 2016, Glenn F. Matthews
# Copyright (c) 2013-2016 the COT project developers.
# See the COPYRIGHT.txt file at the top-level directory of this distribution
# and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt.
#
# This file is part of the Common OVF Tool (COT) project.
# It is subject to the license terms in the LICENSE.txt file found in the
# top-level directory of this distribution and at
# https://github.com/glennmatthews/cot/blob/master/LICENSE.txt. No part
# of COT, including this file, may be copied, modified, propagated, or
# distributed except according to the terms contained in the LICENSE.txt file.
"""Module for handling the differences in XML between OVF spec versions.

**Variation between OVF versions**

XML can be a pain to work with, and when working with multiple OVF schema
versions (currently 3 of them -- 0.9, 1.x.y, 2.0.y) it gets extra painful.
While we could use :mod:`lxml` to *validate* inbound XML against the
appropriate schema version, even that package does not (as far as I can
determine) provide any assistance in *creating* XML against the appropriate
schema definition. So we have to do it ourselves.

=================== ==========================================================
Variation           Details and examples
=================== ==========================================================
Root namespace      - 0.9: "http://www.vmware.com/schema/ovf/1/envelope"
                    - 1.x: "http://schemas.dmtf.org/ovf/envelope/1"
                    - 2.x: "http://schemas.dmtf.org/ovf/envelope/2"

                    Unfortunately, the :mod:`xml.etree.ElementTree` and
                    :mod:`lxml.etree` modules both rely on the absolute
                    namespace URI (rather than any defined namespace
                    prefix aliases) throughout. Thus, we can't make use of
                    the fact that OVF descriptors of all three versions
                    typically define the prefix "ovf" for whichever of
                    the above URIs is appropriate. We have to use the
                    version-appropriate namespace URI everywhere in code.

Element namespaces  Example: hardware details for network interfaces are
                    in the ``ResourceAllocationSettingData`` namespace in
                    versions 0.x and 1.x, but split out into a separate
                    ``EthernetPortAllocationSettingData`` namespace in
                    version 2.x.

Element tags        Network definitions are grouped under an
                    ``ovf:Section`` element in version 0.x but under an
                    ``ovf:NetworkSection`` in versions 1.x and 2.x.
                    Network cards are ``ovf:Item`` elements in 0.x and
                    1.x, but ``ovf:EthernetPortItem`` in 2.x.

Element attributes  In version 0.x, the different Section types are
                    identified by a type attribute (``<ovf:Section
                    xsi:type="ovf:DiskSection_Type">``) while in later
                    versions they are identified by tag
                    (``<ovf:DiskSection>``) and do not have such an attribute.

Element ordering    Most notably, the various child elements under an Item
                    require alphabetical order in OVF 1.x and 2.x, but in
                    0.9 they require a different, idiosyncratic order.
=================== ==========================================================

**Functions**

.. autosummary::
  :nosignatures:

  name_helper

**Classes and Exceptions**

.. autosummary::
  :nosignatures:

  OVFNameHelper1
  OVFNameHelper0
  OVFNameHelper2
"""

from COT.data_validation import ValueUnsupportedError


[docs]def name_helper(version): """Generate an instance of the correct OVFNameHelper variant class. Args: version (float): OVF specification version to use, such as 0.9, 1.0, or 2.0 Returns: Instance of OVFNameHelper[012] as appropriate. """ if version < 1.0: return OVFNameHelper0() elif version < 2.0: return OVFNameHelper1() else: return OVFNameHelper2()
class _Tag(object): """Helper class representing a named XML namespace and associated tag.""" def __init__(self, namespace_name, tag): """Store namespace name and tag. Args: namespace_name (str): XML namespace name tag (str): XML tag """ self.namespace_name = namespace_name.upper() self.tag = tag CIM_URI = "http://schemas.dmtf.org/wbem/wscim/1"
[docs]class OVFNameHelper1(object): """Helper class for :class:`OVF` version 1.x. Provides string constants for easier lookup of various OVF XML elements and attributes. Version-specific subclasses below provide variant properties. """ # For the standard namespace URIs in an OVF descriptor, let's define # shorthand identifiers to be used when writing back out to XML: NSM = dict( xsi="http://www.w3.org/2001/XMLSchema-instance", cim=CIM_URI + "/common", rasd=CIM_URI + "/cim-schema/2/CIM_ResourceAllocationSettingData", vssd=CIM_URI + "/cim-schema/2/CIM_VirtualSystemSettingData", # The OVF namespace varies by version ovf="http://schemas.dmtf.org/ovf/envelope/1", # Older OVF versions have ethernet and storage items # in the same RASD namespace as other hardware, but 2.x has separate ) """Shorthand for XML namespace URIs usually seen in a version 1.x OVF.""" # Non-standard namespaces (such as VMWare's # 'http://www.vmware.com/schema/ovf') should not be added to the NSM # dictionary, but may be registered manually by calling # register_namespace() as needed - see OVF.write() for examples. # List of ResourceType string values we know about # http://schemas.dmtf.org/wbem/cim-html/2/ # CIM_ResourceAllocationSettingData.html RES_MAP = { 'cpu': '3', 'memory': '4', 'ide': '5', 'scsi': '6', 'fc': '7', 'iscsi': '8', 'ib': '9', 'ethernet': '10', 'floppy': '14', 'cdrom': '15', 'dvd': '16', 'harddisk': '17', 'sata': '20', # 'Other Storage' but VBox uses for SATA 'serial': '21', 'parallel': '22', 'usb': '23', } """Mapping of human-readable strings to ResourceType values. See http://schemas.dmtf.org/wbem/cim-html/2/CIM_ResourceAllocationSettingData.html for more details. """ # noqa: E501 # Cached strings, built on the fly _cache = {} # XML elements we care about in the OVF descriptor # TagPlusNamespace objects _raw = dict( # Top-level element is Envelope ENVELOPE=_Tag('ovf', 'Envelope'), # All Section elements have an Info element as child INFO=_Tag('ovf', 'Info'), # Envelope -> NetworkSection -> Network NETWORK_SECTION=_Tag('ovf', 'NetworkSection'), NETWORK=_Tag('ovf', 'Network'), # Attributes of a Network element NETWORK_NAME=_Tag('ovf', 'name'), # Network sub-elements NWK_DESC=_Tag('ovf', 'Description'), # Envelope -> DeploymentOptionSection -> Configuration DEPLOY_OPT_SECTION=_Tag('ovf', 'DeploymentOptionSection'), CONFIG=_Tag('ovf', 'Configuration'), # Attributes of a Configuration element CONFIG_ID=_Tag('ovf', 'id'), CONFIG_DEFAULT=_Tag('ovf', 'default'), # Configuration sub-elements CFG_LABEL=_Tag('ovf', 'Label'), CFG_DESC=_Tag('ovf', 'Description'), # Envelope -> References -> File REFERENCES=_Tag('ovf', 'References'), FILE=_Tag('ovf', 'File'), # Attributes of a File element FILE_ID=_Tag('ovf', 'id'), FILE_HREF=_Tag('ovf', 'href'), FILE_SIZE=_Tag('ovf', 'size'), # Envelope -> DiskSection -> Disk DISK_SECTION=_Tag('ovf', 'DiskSection'), DISK=_Tag('ovf', 'Disk'), # Attributes of a Disk element DISK_ID=_Tag('ovf', 'diskId'), DISK_FILE_REF=_Tag('ovf', 'fileRef'), DISK_CAPACITY=_Tag('ovf', 'capacity'), DISK_CAP_UNITS=_Tag('ovf', 'capacityAllocationUnits'), DISK_FORMAT=_Tag('ovf', 'format'), # Envelope -> VirtualSystem -> AnnotationSection -> Annotation ANNOTATION_SECTION=_Tag('ovf', 'AnnotationSection'), ANNOTATION=_Tag('ovf', 'Annotation'), # Envelope -> VirtualSystem -> ProductSection VIRTUAL_SYSTEM=_Tag('ovf', 'VirtualSystem'), PRODUCT_SECTION=_Tag('ovf', 'ProductSection'), # ProductSection attributes PRODUCT_CLASS=_Tag('ovf', 'class'), # ProductSection sub-elements PRODUCT=_Tag('ovf', 'Product'), VENDOR=_Tag('ovf', 'Vendor'), VERSION=_Tag('ovf', 'Version'), FULL_VERSION=_Tag('ovf', 'FullVersion'), PRODUCT_URL=_Tag('ovf', 'ProductUrl'), VENDOR_URL=_Tag('ovf', 'VendorUrl'), APPLICATION_URL=_Tag('ovf', 'AppUrl'), PROPERTY=_Tag('ovf', 'Property'), # Attributes of a Property element PROP_KEY=_Tag('ovf', 'key'), PROP_VALUE=_Tag('ovf', 'value'), PROP_QUAL=_Tag('ovf', 'qualifiers'), PROP_TYPE=_Tag('ovf', 'type'), PROP_USER_CONFIGABLE=_Tag('ovf', 'userConfigurable'), # Property sub-elements PROPERTY_LABEL=_Tag('ovf', 'Label'), PROPERTY_DESC=_Tag('ovf', 'Description'), ENVIRONMENT_TRANSPORT=_Tag('ovf', 'transport'), # Envelope -> VirtualSystem -> EulaSection -> License EULA_SECTION=_Tag('ovf', 'EulaSection'), EULA_LICENSE=_Tag('ovf', 'License'), # Envelope -> VirtualSystem -> VirtualHardwareSection -> Item(s) # In version 2.x, there can also be StorageItem and EthernetPortItem VIRTUAL_HW_SECTION=_Tag('ovf', 'VirtualHardwareSection'), ITEM=_Tag('ovf', 'Item'), # These are just regular Items in older OVF versions STORAGE_ITEM=_Tag('ovf', 'Item'), ETHERNET_PORT_ITEM=_Tag('ovf', 'Item'), # Item attributes ITEM_CONFIG=_Tag('ovf', 'configuration'), # ... VirtualHardwareSection -> System -> VirtualSystemType SYSTEM=_Tag('ovf', 'System'), VIRTUAL_SYSTEM_TYPE=_Tag('vssd', 'VirtualSystemType'), ) # Item sub-elements # As these are shared across the RASD, SASD, and EPASD namespaces # in OVF 2.0, we don't hard-code a namespace any more. _item_children = dict( ADDRESS='Address', ADDRESS_ON_PARENT='AddressOnParent', ALLOCATION_UNITS='AllocationUnits', AUTOMATIC_ALLOCATION='AutomaticAllocation', AUTOMATIC_DEALLOCATION='AutomaticDeallocation', CAPTION='Caption', CONNECTION='Connection', CONSUMER_VISIBILITY='ConsumerVisibility', ITEM_DESCRIPTION='Description', ELEMENT_NAME='ElementName', HOST_RESOURCE='HostResource', OLD_HOST_RSRC_FILE_REF="/file/", OLD_HOST_RSRC_DISK_REF="/disk/", HOST_RSRC_FILE_REF="ovf:/file/", HOST_RSRC_DISK_REF="ovf:/disk/", INSTANCE_ID='InstanceID', LIMIT='Limit', MAPPING_BEHAVIOR='MappingBehavior', OTHER_RESOURCE_TYPE='OtherResourceType', PARENT='Parent', POOL_ID='PoolID', RESERVATION='Reservation', RESOURCE_SUB_TYPE='ResourceSubType', RESOURCE_TYPE='ResourceType', VIRTUAL_QUANTITY='VirtualQuantity', WEIGHT='Weight', ) def __getattr__(self, name): """Transparently pass attribute lookups to _raw and _cache. Args: name (str): Attribute name to look up. Returns: Value looked up from :attr:`_raw` and/or :attr:`_cache`. Raises: AttributeError: if the given ``name`` is not found. """ if name in self._item_children: return self._item_children[name] if name not in self._cache: if name.lower() in self.NSM: self._cache[name] = "{%s}" % self.NSM[name.lower()] elif name == "EPASD" or name == "SASD": self._cache[name] = self.RASD elif name not in self._raw: raise AttributeError("Unknown attribute '{0}'".format(name)) else: namespace = getattr(self, self._raw[name].namespace_name) tag = self._raw[name].tag self._cache[name] = namespace + tag return self._cache[name]
[docs] def __init__(self): """Create a name helper for OVF version 1.x.""" # 1.0 is nice in that they're all in alphabetical order # Pylint doesn't like naming instance attributes like constants, # even though really these are treated as instance constants. # This needs future refactoring anyway, so for now: # pylint: disable=invalid-name self.ITEM_CHILDREN = ( self.ADDRESS, self.ADDRESS_ON_PARENT, self.ALLOCATION_UNITS, self.AUTOMATIC_ALLOCATION, self.AUTOMATIC_DEALLOCATION, self.CAPTION, self.CONNECTION, self.CONSUMER_VISIBILITY, self.ITEM_DESCRIPTION, self.ELEMENT_NAME, self.HOST_RESOURCE, self.INSTANCE_ID, self.LIMIT, self.MAPPING_BEHAVIOR, self.OTHER_RESOURCE_TYPE, self.PARENT, self.POOL_ID, self.RESERVATION, self.RESOURCE_SUB_TYPE, self.RESOURCE_TYPE, self.VIRTUAL_QUANTITY, self.WEIGHT, ) # all of these are 0.9 exclusive self.NETWORK_SECTION_ATTRIB = {} self.DISK_SECTION_ATTRIB = {} self.ANNOTATION_SECTION_ATTRIB = {} self.VIRTUAL_SYSTEM_ATTRIB = {} self.PRODUCT_SECTION_ATTRIB = {} self.EULA_SECTION_ATTRIB = {} self.VIRTUAL_HW_SECTION_ATTRIB = {}
[docs] def namespace_for_item_tag(self, tag): """Get the XML namespace for the given item tag. Args: tag (str): Un-namespaced XML tag. Returns: str: XML namespace string, or None. """ if tag == self.ITEM: return self.RASD elif tag == self.STORAGE_ITEM: return self.SASD elif tag == self.ETHERNET_PORT_ITEM: return self.EPASD return None
[docs] def namespace_for_resource_type(self, resource_type): """Get the XML namespace for the given ResourceType. Args: resource_type (str): ResourceType value string. Returns: str: XML namespace string, or None. """ if resource_type == self.RES_MAP['ethernet']: return self.EPASD elif (resource_type == self.RES_MAP['harddisk'] or resource_type == self.RES_MAP['cdrom']): return self.SASD else: return self.RASD
[docs] def item_tag_for_namespace(self, namespace): """Get the Item tag for the given XML namespace. Args: namespace (str): XML namespace Returns: str: 'Item', 'StorageItem', or 'EthernetPortItem' as appropriate. Raises: ValueUnsupportedError: if the namespace is unrecognized """ if namespace == self.RASD: return self.ITEM elif namespace == self.SASD: return self.STORAGE_ITEM elif namespace == self.EPASD: return self.ETHERNET_PORT_ITEM else: raise ValueUnsupportedError("namespace", namespace, [self.RASD, self.SASD, self.EPASD])
[docs]class OVFNameHelper0(OVFNameHelper1): """Helper class for :class:`OVF` of versions prior to 1.0. Provides string constants for easier lookup of various OVF XML elements and attributes. """ NSM = dict( OVFNameHelper1.NSM, ovf="http://www.vmware.com/schema/ovf/1/envelope", ) """Shorthand for XML namespace URIs usually seen in a version 0.x OVF.""" _cache = dict(OVFNameHelper1._cache) _raw = dict( OVFNameHelper1._raw, NETWORK_SECTION=_Tag('ovf', 'Section'), DISK_SECTION=_Tag('ovf', 'Section'), ANNOTATION_SECTION=_Tag('ovf', 'Section'), VIRTUAL_SYSTEM=_Tag('ovf', 'Content'), PRODUCT_SECTION=_Tag('ovf', 'Section'), PROP_VALUE=_Tag('ovf', 'defaultValue'), PROP_USER_CONFIGABLE=_Tag('ovf', 'configurableByUser'), EULA_SECTION=_Tag('ovf', 'Section'), VIRTUAL_HW_SECTION=_Tag('ovf', 'Section'), ) _item_children = dict( OVFNameHelper1._item_children, BUS_NUMBER='BusNumber', # No ElementName in 0.9, but Caption serves a similar purpose ELEMENT_NAME='Caption', HOST_RSRC_FILE_REF="/file/", HOST_RSRC_DISK_REF="/disk/", INSTANCE_ID='InstanceId', )
[docs] def __init__(self): """Create a name helper for OVF version 0.x.""" super(OVFNameHelper0, self).__init__() self.ITEM_CHILDREN = ( self.CAPTION, self.ITEM_DESCRIPTION, self.INSTANCE_ID, self.RESOURCE_TYPE, self.OTHER_RESOURCE_TYPE, self.RESOURCE_SUB_TYPE, self.POOL_ID, self.CONSUMER_VISIBILITY, self.HOST_RESOURCE, self.ALLOCATION_UNITS, self.VIRTUAL_QUANTITY, self.RESERVATION, self.LIMIT, self.WEIGHT, self.AUTOMATIC_ALLOCATION, self.AUTOMATIC_DEALLOCATION, self.PARENT, self.CONNECTION, self.ADDRESS, self.MAPPING_BEHAVIOR, self.ADDRESS_ON_PARENT, self.BUS_NUMBER, ) xsi_type = "{" + self.NSM['xsi'] + "}type" self.NETWORK_SECTION_ATTRIB = { xsi_type: "ovf:NetworkSection_Type" } self.DISK_SECTION_ATTRIB = { xsi_type: "ovf:DiskSection_Type" } self.ANNOTATION_SECTION_ATTRIB = { xsi_type: "ovf:AnnotationSection_Type" } self.VIRTUAL_SYSTEM_ATTRIB = { xsi_type: "ovf:VirtualSystem_Type" } self.PRODUCT_SECTION_ATTRIB = { xsi_type: "ovf:ProductSection_Type" } self.EULA_SECTION_ATTRIB = { xsi_type: "ovf:EulaSection_Type" } self.VIRTUAL_HW_SECTION_ATTRIB = { xsi_type: "ovf:VirtualHardwareSection_Type" }
[docs]class OVFNameHelper2(OVFNameHelper1): """Helper class for :class:`OVF` of version 2.x. TODO. Provides string constants for easier lookup of various OVF XML elements and attributes. """ NSM = dict( OVFNameHelper1.NSM, ovf="http://schemas.dmtf.org/ovf/envelope/2", # OVF 2.0 adds new namespaces for ethernet ports & storage devices epasd=(CIM_URI + "/cim-schema/2/CIM_EthernetPortAllocationSettingData.xsd"), sasd=(CIM_URI + "/cim-schema/2/CIM_StorageAllocationSettingData.xsd"), ) """Shorthand for XML namespace URIs usually seen in a version 2.x OVF.""" _cache = dict(OVFNameHelper1._cache) _raw = dict( OVFNameHelper1._raw, STORAGE_ITEM=_Tag('ovf', 'StorageItem'), ETHERNET_PORT_ITEM=_Tag('ovf', 'EthernetPortItem'), )
[docs] def __init__(self): """Create a name helper for OVF version 2.x.""" super(OVFNameHelper2, self).__init__()