Source code for COT.ovf.hardware

#!/usr/bin/env python
#
# hardware.py - OVFHardware class
#
# 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.

"""Representation of OVF hardware definitions.

**Classes and Exceptions**

.. autosummary::
  :nosignatures:

  OVFHardware
  OVFHardwareDataError
"""

import copy
import logging

from COT.data_validation import natural_sort
from COT.xml_file import XML

from .item import OVFItem, OVFItemDataError

logger = logging.getLogger(__name__)


[docs]class OVFHardwareDataError(Exception): """The input data used to construct an :class:`OVFHardware` is not sane."""
[docs]class OVFHardware(object): """Helper class for :class:`~COT.ovf.ovf.OVF`. Represents all hardware items defined by this OVF; i.e., the contents of all Items in the VirtualHardwareSection. Fundamentally it's just a dict of :class:`~COT.ovf.item.OVFItem` objects with a bunch of helper methods. """
[docs] def __init__(self, ovf): """Construct an OVFHardware object describing all Items in the OVF. Args: ovf (OVF): OVF instance to extract hardware information from. Raises: OVFHardwareDataError: if any data errors are seen """ self.ovf = ovf self.item_dict = {} valid_profiles = set(ovf.config_profiles) item_count = 0 for item in ovf.virtual_hw_section: ns = ovf.namespace_for_item_tag(item.tag) if not ns: continue item_count += 1 # We index the dict by InstanceID as it's the one property of # an Item that uniquely identifies this set of hardware items. instance = item.find(ns + self.ovf.INSTANCE_ID).text # Pre-sanity check - are all of the profiles associated with this # item properly defined in the OVF DeploymentOptionSection? item_profiles = set(item.get(self.ovf.ITEM_CONFIG, "").split()) unknown_profiles = item_profiles - valid_profiles if unknown_profiles: raise OVFHardwareDataError("Unknown profile(s) {0} for " "Item instance {1}" .format(unknown_profiles, instance)) if instance not in self.item_dict: self.item_dict[instance] = OVFItem(self.ovf, item) else: try: self.item_dict[instance].add_item(item) except OVFItemDataError as e: logger.debug(e) # Mask away the nitty-gritty details from our caller raise OVFHardwareDataError("Data conflict for instance {0}" .format(instance)) logger.verbose( "OVF contains %s hardware Item elements describing %s " "unique devices", item_count, len(self.item_dict)) # Treat the current state as golden: for ovfitem in self.item_dict.values(): ovfitem.modified = False
[docs] def update_xml(self): """Regenerate all Items under the VirtualHardwareSection, if needed. Will do nothing if no Items have been changed. """ modified = False if len(self.item_dict) != len(XML.find_all_children( self.ovf.virtual_hw_section, set([self.ovf.ITEM, self.ovf.STORAGE_ITEM, self.ovf.ETHERNET_PORT_ITEM]))): modified = True else: for ovfitem in self.item_dict.values(): if ovfitem.modified: modified = True break if not modified: logger.debug("No changes to hardware definition, " "so no XML update is required") return # Delete the existing Items: delete_count = 0 for item in list(self.ovf.virtual_hw_section): if (item.tag == self.ovf.ITEM or item.tag == self.ovf.STORAGE_ITEM or item.tag == self.ovf.ETHERNET_PORT_ITEM): self.ovf.virtual_hw_section.remove(item) delete_count += 1 logger.verbose("Cleared %d existing items from VirtualHWSection", delete_count) # Generate the new XML Items, in appropriately sorted order by Instance ordering = [self.ovf.INFO, self.ovf.SYSTEM, self.ovf.ITEM] for instance in natural_sort(self.item_dict): logger.debug("Writing Item(s) with InstanceID %s", instance) ovfitem = self.item_dict[instance] new_items = ovfitem.generate_items() logger.debug("Generated %d items", len(new_items)) for item in new_items: XML.add_child(self.ovf.virtual_hw_section, item, ordering) logger.verbose("Updated XML VirtualHardwareSection, now contains %d " "Items representing %d devices", len(self.ovf.virtual_hw_section.findall(self.ovf.ITEM)), len(self.item_dict))
[docs] def find_unused_instance_id(self): """Find the first available ``InstanceID`` number. Returns: str: An instance ID that is not yet in use. """ i = 1 while str(i) in self.item_dict.keys(): i += 1 logger.debug("Found unused InstanceID %d", i) return str(i)
[docs] def new_item(self, resource_type, profile_list=None): """Create a new :class:`~COT.ovf.item.OVFItem` of the given type. Args: resource_type (str): String such as 'cpu' or 'harddisk' - used as a key to :data:`~COT.ovf.name_helper.OVFNameHelper1.RES_MAP` profile_list (list): Profiles the new item should belong to Returns: tuple: ``(instance_id, ovfitem)`` """ instance = self.find_unused_instance_id() ovfitem = OVFItem(self.ovf) ovfitem.set_property(self.ovf.INSTANCE_ID, instance, profile_list) ovfitem.set_property(self.ovf.RESOURCE_TYPE, self.ovf.RES_MAP[resource_type], profile_list) # ovftool freaks out if we leave out the ElementName on an Item, # so provide a simple default value. ovfitem.set_property(self.ovf.ELEMENT_NAME, resource_type, profile_list) self.item_dict[instance] = ovfitem ovfitem.modified = True logger.info("Added new %s under %s, instance is %s", resource_type, profile_list, instance) return (instance, ovfitem)
[docs] def delete_item(self, item): """Delete the given Item from the hardware. Args: item (OVFItem): Item to delete """ instance = item.get_value(self.ovf.INSTANCE_ID) if self.item_dict[instance] == item: del self.item_dict[instance]
# TODO: error handling - currently a no-op if item not in item_dict
[docs] def clone_item(self, parent_item, profile_list): """Clone an :class:`OVFItem` to create a new instance. Args: parent_item (OVFItem): Instance to clone from profile_list (list): List of profiles to clone into Returns: tuple: ``(instance_id, ovfitem)`` """ instance = self.find_unused_instance_id() ovfitem = copy.deepcopy(parent_item) ovfitem.set_property(self.ovf.INSTANCE_ID, instance, profile_list) ovfitem.modified = True self.item_dict[instance] = ovfitem logger.debug("Added clone of %s under %s, instance is %s", parent_item, profile_list, instance) return (instance, ovfitem)
[docs] def item_match(self, item, resource_type, properties, profile_list): """Check whether the given item matches the given filters. Args: item (OVFItem): Item to validate resource_type (str): Resource type string like 'scsi' or 'serial' properties (dict): Properties and their values to match profile_list (list): List of profiles to filter on Returns: bool: True if the item matches all filters, False if not. """ if resource_type and (self.ovf.RES_MAP[resource_type] != item.get_value(self.ovf.RESOURCE_TYPE)): return False if profile_list: for profile in profile_list: if not item.has_profile(profile): return False for (prop, value) in properties.items(): if item.get_value(prop) != value: return False return True
[docs] def find_all_items(self, resource_type=None, properties=None, profile_list=None): """Find all items matching the given type, properties, and profiles. Args: resource_type (str): Resource type string like 'scsi' or 'serial' properties (dict): Properties and their values to match profile_list (list): List of profiles to filter on Returns: list: Matching :class:`~COT.ovf.item.OVFItem` instances """ items = [self.item_dict[instance] for instance in natural_sort(self.item_dict)] filtered_items = [] if properties is None: properties = {} for item in items: if self.item_match(item, resource_type, properties, profile_list): filtered_items.append(item) logger.debug("Found %s %s Items", len(filtered_items), resource_type) return filtered_items
[docs] def find_item(self, resource_type=None, properties=None, profile=None): """Find the only :class:`OVFItem` of the given :attr:`resource_type`. Args: resource_type (str): Resource type string like 'scsi' or 'serial' properties (dict): Properties and their values to match profile (str): Single profile ID to search within Returns: OVFItem: Matching instance, or None Raises: LookupError: if more than one such Item exists. """ matches = self.find_all_items(resource_type, properties, [profile]) if len(matches) > 1: raise LookupError( "Found multiple matching '{0}' Items (instances {1})" .format(resource_type, [m.instance_id for m in matches])) elif len(matches) == 0: return None else: return matches[0]
[docs] def get_item_count(self, resource_type, profile): """Wrapper for :meth:`get_item_count_per_profile`. Args: resource_type (str): Resource type string like 'scsi' or 'serial' profile (str): Single profile identifier string to look up. Returns: int: Number of items of this type in this profile. """ return (self.get_item_count_per_profile(resource_type, [profile]) [profile])
[docs] def get_item_count_per_profile(self, resource_type, profile_list): """Get the number of Items of the given type per profile. Items present under "no profile" will be counted against the total for each profile. Args: resource_type (str): Resource type string like 'scsi' or 'serial' profile_list (list): List of profiles to filter on (default: apply across all profiles) Returns: dict: mapping profile strings to the number of items under each profile. """ count_dict = {} if not profile_list: # Get the count under all profiles profile_list = self.ovf.config_profiles + [None] for profile in profile_list: count_dict[profile] = 0 for ovfitem in self.find_all_items(resource_type): for profile in profile_list: if ovfitem.has_profile(profile): count_dict[profile] += 1 for (profile, count) in count_dict.items(): logger.debug("Profile '%s' has %s %s Item(s)", profile, count, resource_type) return count_dict
[docs] def update_existing_item_count_per_profile(self, resource_type, count, profile_list): """Change profile membership of existing items as needed. Helper method for :meth:`set_item_count_per_profile`. Args: resource_type (str): 'cpu', 'harddisk', etc. count (int): Desired number of items profile_list (list): List of profiles to filter on (default: apply across all profiles) Returns: tuple: (count_dict, items_to_add, last_item) """ count_dict = self.get_item_count_per_profile(resource_type, profile_list) items_seen = dict.fromkeys(profile_list, 0) last_item = None # First, iterate over existing Items. # Once we've seen "count" items under a profile, remove all subsequent # items from this profile. # If we don't have enough items under a profile, add any items found # under other profiles to this profile as well. for ovfitem in self.find_all_items(resource_type): last_item = ovfitem for profile in profile_list: if ovfitem.has_profile(profile): if items_seen[profile] >= count: # Too many items - remove this one! ovfitem.remove_profile(profile) else: items_seen[profile] += 1 else: if count_dict[profile] < count: # Add this profile to this Item ovfitem.add_profile(profile) count_dict[profile] += 1 items_seen[profile] += 1 # How many new Items do we need to create in total? items_to_add = 0 for profile in profile_list: delta = count - items_seen[profile] if delta > items_to_add: items_to_add = delta return count_dict, items_to_add, last_item
def _update_cloned_item(self, new_item, new_item_profiles, item_count): """Update a cloned item to make it distinct from its parent. Helper method for :meth:`set_item_count_per_profile`. Args: new_item (OVFItem): Newly cloned Item new_item_profiles (list): Profiles new_item should belong to item_count (int): How many Items of this type (including this item) now exist. Used with :meth:`COT.platform.GenericPlatform.guess_nic_name` Returns: OVFItem: Updated :param:`new_item` Raises: NotImplementedError: No support yet for updating ``Address`` NotImplementedError: If updating ``AddressOnParent`` but the prior value varies across config profiles. NotImplementedError: if ``AddressOnParent`` is not an integer. """ resource_type = new_item.hardware_type address = new_item.get(self.ovf.ADDRESS) if address: raise NotImplementedError("Don't know how to ensure a unique " "Address value when cloning an Item " "of type {0}".format(resource_type)) address_on_parent = new_item.get(self.ovf.ADDRESS_ON_PARENT) if address_on_parent: address_list = new_item.get_all_values(self.ovf.ADDRESS_ON_PARENT) if len(address_list) > 1: raise NotImplementedError("AddressOnParent is not common " "across all profiles but has " "multiple values {0}. COT can't " "handle this yet." .format(address_list)) address_on_parent = address_list[0] # Currently we only handle integer addresses try: address_on_parent = int(address_on_parent) address_on_parent += 1 new_item.set_property(self.ovf.ADDRESS_ON_PARENT, str(address_on_parent), new_item_profiles) except ValueError: raise NotImplementedError("Don't know how to ensure a " "unique AddressOnParent value " "given base value '{0}'" .format(address_on_parent)) if resource_type == 'ethernet': # Update ElementName to reflect the NIC number element_name = self.ovf.platform.guess_nic_name(item_count) new_item.set_property(self.ovf.ELEMENT_NAME, element_name, new_item_profiles) return new_item
[docs] def set_item_count_per_profile(self, resource_type, count, profile_list): """Set the number of items of a given type under the given profile(s). If the new count is greater than the current count under this profile, then additional instances that already exist under another profile will be added to this profile, starting with the lowest-sequence instance not already present, and only as a last resort will new instances be created. If the new count is less than the current count under this profile, then the highest-numbered instances will be removed preferentially. Args: resource_type (str): 'cpu', 'harddisk', etc. count (int): Desired number of items profile_list (list): List of profiles to filter on (default: apply across all profiles) """ if not profile_list: # Set the profile list for all profiles, including the default profile_list = self.ovf.config_profiles + [None] count_dict, items_to_add, last_item = \ self.update_existing_item_count_per_profile( resource_type, count, profile_list) logger.debug("Creating %d new items", items_to_add) while items_to_add > 0: # Which profiles does this Item need to belong to? new_item_profiles = [] for profile in profile_list: if count_dict[profile] < count: new_item_profiles.append(profile) count_dict[profile] += 1 if last_item is None: logger.warning("No existing items of type %s found. " "Will create new %s from scratch.", resource_type, resource_type) (_, new_item) = self.new_item(resource_type, new_item_profiles) else: (_, new_item) = self.clone_item(last_item, new_item_profiles) # Check/update other properties of the clone that should be unique: # TODO - we assume that the count is the same across profiles new_item = self._update_cloned_item( new_item, new_item_profiles, count_dict[new_item_profiles[0]]) last_item = new_item items_to_add -= 1
[docs] def set_value_for_all_items(self, resource_type, prop_name, new_value, profile_list, create_new=False): """Set a property to the given value for all items of the given type. If no items of the given type exist, will create a new ``Item`` if :attr:`create_new` is set to ``True``; otherwise will log a warning and do nothing. Args: resource_type (str): Resource type such as 'cpu' or 'harddisk' prop_name (str): Property name to update new_value (str): New value to set the property to profile_list (list): List of profiles to filter on (default: apply across all profiles) create_new (bool): Whether to create a new entry if no items of this :attr:`resource_type` presently exist. """ ovfitem_list = self.find_all_items(resource_type) if not ovfitem_list: if not create_new: logger.warning("No items of type %s found. Nothing to do.", resource_type) return logger.warning("No existing items of type %s found. " "Will create new %s from scratch.", resource_type, resource_type) (_, ovfitem) = self.new_item(resource_type, profile_list) ovfitem_list = [ovfitem] for ovfitem in ovfitem_list: ovfitem.set_property(prop_name, new_value, profile_list) logger.info("Updated %s %s to %s under %s", resource_type, prop_name, new_value, profile_list)
[docs] def set_item_values_per_profile(self, resource_type, prop_name, value_list, profile_list, default=None): """Set value(s) for a property of multiple items of a type. Args: resource_type (str): Device type such as 'harddisk' or 'cpu' prop_name (str): Property name to update value_list (list): List of values to set (one value per item of the given :attr:`resource_type`) profile_list (list): List of profiles to filter on (default: apply across all profiles) default (str): If there are more matching items than entries in :attr:`value_list`, set extra items to this value """ if profile_list is None: profile_list = self.ovf.config_profiles + [None] for ovfitem in self.find_all_items(resource_type): if len(value_list): new_value = value_list.pop(0) else: new_value = default for profile in profile_list: if ovfitem.has_profile(profile): ovfitem.set_property(prop_name, new_value, [profile]) logger.info("Updated %s property %s to %s under %s", resource_type, prop_name, new_value, profile_list) if len(value_list): logger.error("After scanning all known %s Items, not all " "%s values were used - leftover %s", resource_type, prop_name, value_list)