diff --git a/.pylintrc b/.pylintrc index e50751d..85dbeaa 100644 --- a/.pylintrc +++ b/.pylintrc @@ -78,7 +78,7 @@ accept-no-return-type-doc=yes # default: max-module-lines: 1000 # current worst offender: COT/ovf/ovf.py -max-module-lines=3000 +max-module-lines=2900 [LOGGING] diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8c0e2a6..ee63510 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,58 @@ Change Log All notable changes to the COT project will be documented in this file. This project adheres to `Semantic Versioning`_. +`Unreleased`_ +------------- + +**Changed** + +- With ``cot edit-hardware``, the platform hardware validation is no longer + a hard limit. Instead, if a value appears to be invalid, the user will be + warned about the validation failure and given the option to continue or + abort (`#61`_). + ``cot --force ...``, as usual, can be used to auto-continue without prompting. +- Lots of API changes: + + - Moved the ``to_string`` function from ``COT.data_validation`` to + ``COT.ui_shared``. + - Function ``COT.deploy_esxi.get_object_from_connection`` is now method + ``PyVmomiVMReconfigSpec.lookup_object``. + - Function ``COT.cli.formatter`` is now class ``COT.logging_.COTFormatter``. + + - COT.disks module: + + - Function ``create_disk`` is now split into class methods + ``DiskRepresentation.for_new_file`` (creates the disk file and returns a + corresponding ``DiskRepresentation`` instance) and + ``DiskRepresentation.create_file`` (creates disk file only). + - Function ``convert_disk`` is now class method + ``DiskRepresentation.convert_to`` + - Function ``disk_representation_from_file`` is now + class method ``DiskRepresentation.from_file`` + - The ``DiskRepresentation`` constructor now only takes the path to a file + as input - if you want to create a new file, use + ``DiskRepresentation.for_new_file`` instead of calling the + constructor directly. + + - COT.ovf module: + + - ``COT.ovf.ovf.byte_string`` has been moved and renamed to + ``COT.ui_shared.pretty_bytes``. + - ``COT.ovf.ovf.byte_count`` has been moved and renamed to + ``COT.ovf.utilities.programmatic_bytes_to_int``. + - ``COT.ovf.ovf.factor_bytes`` has been moved and renamed to + ``COT.ovf.utilities.int_bytes_to_programmatic_units``. + + - COT.platforms module: + + - Class ``GenericPlatform`` is now ``Platform``. + - Function ``platform_from_product_class`` is now class method + ``Platform.for_product_string`` and returns an instance + of a ``Platform`` class rather than the class object itself. + - Most ``Platform`` APIs are now instance methods instead of + class methods. + - Function ``is_known_product_class`` has been removed. + `1.9.1`_ - 2017-02-21 --------------------- @@ -619,6 +671,7 @@ Initial public release. .. _#58: https://github.com/glennmatthews/cot/issues/58 .. _#59: https://github.com/glennmatthews/cot/issues/59 .. _#60: https://github.com/glennmatthews/cot/issues/60 +.. _#61: https://github.com/glennmatthews/cot/issues/61 .. _Semantic Versioning: http://semver.org/ .. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/ diff --git a/COT/__init__.py b/COT/__init__.py index 137c317..ee4a4b9 100644 --- a/COT/__init__.py +++ b/COT/__init__.py @@ -49,7 +49,7 @@ COT.data_validation COT.file_reference - COT.platforms + COT.logging_ User interface modules ---------------------- @@ -64,8 +64,10 @@ .. autosummary:: :toctree: + COT.disks COT.helpers COT.ovf + COT.platforms """ from ._version import get_versions diff --git a/COT/add_disk.py b/COT/add_disk.py index e7f2e87..b014c52 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -40,7 +40,7 @@ import logging import os.path -from COT.disks import disk_representation_from_file +from COT.disks import DiskRepresentation from COT.data_validation import ( InvalidInputError, ValueUnsupportedError, check_for_conflict, device_address, match_or_die, @@ -144,7 +144,7 @@ def disk_image(self): @disk_image.setter def disk_image(self, value): - self._disk_image = disk_representation_from_file(value) + self._disk_image = DiskRepresentation.from_file(value) @property def address(self): @@ -393,7 +393,7 @@ def guess_controller_type(platform, ctrl_item, drive_type): """If a controller type wasn't specified, try to guess from context. Args: - platform (GenericPlatform): Platform class to guess controller for + platform (Platform): Platform instance to guess controller for ctrl_item (object): Any known controller object, or None drive_type (str): "cdrom" or "harddisk" Returns: @@ -404,8 +404,8 @@ def guess_controller_type(platform, ctrl_item, drive_type): Examples: :: - >>> from COT.platforms import GenericPlatform - >>> guess_controller_type(GenericPlatform, None, 'harddisk') + >>> from COT.platforms import Platform + >>> guess_controller_type(Platform(), None, 'harddisk') 'ide' """ if ctrl_item is None: @@ -416,7 +416,7 @@ def guess_controller_type(platform, ctrl_item, drive_type): ctrl_type = platform.controller_type_for_device(drive_type) logger.warning("Guessing controller type should be %s " "based on disk drive type %s and platform %s", - ctrl_type, drive_type, platform.__name__) + ctrl_type, drive_type, platform) else: ctrl_type = ctrl_item.hardware_type if ctrl_type != 'ide' and ctrl_type != 'scsi': @@ -634,6 +634,6 @@ def add_disk_worker(vm, description, disk, file_obj, ctrl_item, disk_item) -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover import doctest doctest.testmod() diff --git a/COT/cli.py b/COT/cli.py index 9885af8..e52a4a4 100644 --- a/COT/cli.py +++ b/COT/cli.py @@ -3,7 +3,7 @@ # cli.py - CLI handling for the Common OVF Tool suite # # August 2013, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 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. # @@ -18,13 +18,6 @@ """CLI entry point for the Common OVF Tool (COT) suite. -**Functions** - -.. autosummary:: - :nosignatures: - - formatter - **Classes** .. autosummary:: @@ -58,45 +51,11 @@ from COT import __version_long__ from COT.data_validation import InvalidInputError, ValueMismatchError from COT.ui_shared import UI +from COT.logging_ import COTFormatter logger = logging.getLogger(__name__) -def formatter(verbosity=logging.INFO): - """Create formatter for log output. - - We offer different (more verbose) formatting when debugging is enabled, - hence this need. - - Args: - verbosity (int): Logging level as defined by :mod:`logging`. - - Returns: - colorlog.ColoredFormatter: Formatter object to use with :mod:`logging`. - """ - from colorlog import ColoredFormatter - log_colors = { - 'DEBUG': 'blue', - 'VERBOSE': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'red', - } - format_string = "%(log_color)s" - datefmt = None - if verbosity <= logging.DEBUG: - format_string += "%(asctime)s.%(msecs)d " - datefmt = "%H:%M:%S" - format_string += "%(levelname)8s: " - if verbosity <= logging.VERBOSE: - format_string += "%(name)-22s " - format_string += "%(message)s" - return ColoredFormatter(format_string, - datefmt=datefmt, - log_colors=log_colors) - - class CLI(UI): """Command-line user interface for COT. @@ -312,8 +271,7 @@ def fill_examples(self, example_list): def set_verbosity(self, level): """Enable logging and/or change the logging verbosity level. - Will call :func:`formatter` and associate the resulting formatter - with logging. + Will create a :class:`COTFormatter` and use it for log formatting. Args: level (int): Logging level as defined by :mod:`logging` @@ -321,7 +279,7 @@ def set_verbosity(self, level): if not self.handler: self.handler = logging.StreamHandler() self.handler.setLevel(level) - self.handler.setFormatter(formatter(level)) + self.handler.setFormatter(COTFormatter(level)) if not self.master_logger: self.master_logger = logging.getLogger('COT') self.master_logger.addHandler(self.handler) @@ -684,5 +642,5 @@ def main(): CLI().run(sys.argv[1:]) -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover main() diff --git a/COT/data_validation.py b/COT/data_validation.py index bccea62..8e287cb 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -46,7 +46,6 @@ no_whitespace non_negative_int positive_int - to_string validate_int truth_value @@ -56,40 +55,12 @@ NIC_TYPES """ -import xml.etree.ElementTree as ET import hashlib import re -import sys +from collections import namedtuple from distutils.util import strtobool - -def to_string(obj): - """Get string representation of an object, special-case for XML Element. - - Args: - obj (object): Object to represent as a string. - Returns: - str: string representation - Examples: - :: - - >>> to_string("Hello") - 'Hello' - >>> to_string(27.5) - '27.5' - >>> e = ET.Element('hello', attrib={'key': 'value'}) - >>> print(e) # doctest: +ELLIPSIS - - >>> print(to_string(e)) - - """ - if ET.iselement(obj): - if sys.version_info[0] >= 3: - return ET.tostring(obj, encoding='unicode') - else: - return ET.tostring(obj) - else: - return str(obj) +from COT.ui_shared import to_string def alphanum_split(key): @@ -261,7 +232,7 @@ def canonicalize_nic_subtype(subtype): Unsupported value 'foobar' for NIC subtype ... .. seealso:: - :meth:`COT.platforms.GenericPlatform.validate_nic_type` + :meth:`COT.platforms.Platform.validate_nic_type` """ return canonicalize_helper("NIC subtype", subtype, _NIC_MAPPINGS, re.IGNORECASE) @@ -473,7 +444,7 @@ def no_whitespace(string): def validate_int(string, minimum=None, maximum=None, - label="input"): + label=None): """Parser helper function for validating integer arguments in a range. Args: @@ -506,6 +477,8 @@ def validate_int(string, ... print(e) Value '100' for x is too high - must be at most 10 """ + if label is None: + label = "input" try: i = int(string) except ValueError: @@ -517,13 +490,14 @@ def validate_int(string, return i -def non_negative_int(string): +def non_negative_int(string, label=None): """Parser helper function for integer arguments that must be 0 or more. Alias for :func:`validate_int` setting :attr:`minimum` to 0. Args: string (str): String to validate. + label (str): Label to include in any errors raised Returns: int: Validated integer value Raises: @@ -542,16 +516,17 @@ def non_negative_int(string): ... print(e) Value '-1' for input is too low - must be at least 0 """ - return validate_int(string, minimum=0) + return validate_int(string, minimum=0, label=label) -def positive_int(string): +def positive_int(string, label=None): """Parser helper function for integer arguments that must be 1 or more. Alias for :func:`validate_int` setting :attr:`minimum` to 1. Args: string (str): String to validate. + label (str): Label to include in any errors raised Returns: int: Validated integer value Raises: @@ -568,7 +543,7 @@ def positive_int(string): ... print(e) Value '0' for input is too low - must be at least 1 """ - return validate_int(string, minimum=1) + return validate_int(string, minimum=1, label=label) def truth_value(value): @@ -612,6 +587,10 @@ def truth_value(value): ) +ValidRange = namedtuple('ValidRange', ['minimum', 'maximum']) +"""Simple helper class representing a range of valid values.""" + + # Some handy exception and error types we can throw class ValueMismatchError(ValueError): """Values which were expected to be equal turned out to be not equal.""" @@ -680,6 +659,6 @@ def __str__(self): self.expected_value)) -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover import doctest doctest.testmod() diff --git a/COT/deploy.py b/COT/deploy.py index cff7d18..f26b192 100644 --- a/COT/deploy.py +++ b/COT/deploy.py @@ -466,6 +466,6 @@ def create_subparser(self): "'kind:value,options'.") -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover import doctest doctest.testmod() diff --git a/COT/deploy_esxi.py b/COT/deploy_esxi.py index 5a539bd..29bc74c 100644 --- a/COT/deploy_esxi.py +++ b/COT/deploy_esxi.py @@ -3,7 +3,7 @@ # deploy_esxi.py - Implements "cot deploy ... esxi" command # # August 2015, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # # This file is part of the Common OVF Tool (COT) project. @@ -14,13 +14,6 @@ """Module for deploying VMs to ESXi, vCenter, and vSphere. -**Functions** - -.. autosummary:: - :nosignatures: - - get_object_from_connection - **Classes** .. autosummary:: @@ -80,6 +73,8 @@ def __enter__(self): Raises: vim.fault.HostConnectFault: TODO requests.exceptions.ConnectionError: TODO + Yields: + pyVmomi.VmomiSupport.vim.ServiceInstance: Session service instance. """ logger.verbose("Establishing connection to %s:%s...", self.server, self.port) @@ -156,44 +151,29 @@ def unwrap_connection_error(outer_e): return errno, inner_message -def get_object_from_connection(conn, vimtype, name): - """Look up an object by name. - - Args: - conn (SmarterConnection): Connection to ESXi. - vimtype (object): currently only ``vim.VirtualMachine`` - name (str): Name of the object to look up. - Returns: - object: Located object - """ - obj = None - content = conn.RetrieveContent() - container = content.viewManager.CreateContainerView( - content.rootFolder, [vimtype], True) - for c in container.view: - if c.name == name: - obj = c - break - return obj - - class PyVmomiVMReconfigSpec(object): """Context manager for reconfiguring an ESXi VM using PyVmomi.""" - def __init__(self, conn, vm_name): - """Use the given name to look up a VM using the given connection. + def __init__(self, service_instance, vm_name): + """Use the given name to look up a VM using the given service_instance. Args: - conn (SmarterConnection): Connection to ESXi. + service_instance (pyVmomi.VmomiSupport.vim.ServiceInstance): + Connection to ESXi. vm_name (str): Virtual machine name. """ - self.vm = get_object_from_connection(conn, vim.VirtualMachine, vm_name) + self.service_instance = service_instance + self.vm = self.lookup_object(vim.VirtualMachine, vm_name) if not self.vm: raise LookupError("No VM '{0}' was found!".format(vm_name)) self.spec = vim.vm.ConfigSpec() def __enter__(self): - """Use a ConfigSpec as the context manager object.""" + """Use a ConfigSpec as the context manager object. + + Yields: + pyVmomi.VmomiSupport.vim.vm.ConfigSpec: config specification + """ return self.spec def __exit__(self, exc_type, exc_value, trace): @@ -206,6 +186,23 @@ def __exit__(self, exc_type, exc_value, trace): logger.verbose("Reconfiguring VM...") self.vm.ReconfigVM_Task(spec=self.spec) + def lookup_object(self, vimtype, name): + """Look up an object by name. + + Args: + vimtype (object): currently only ``vim.VirtualMachine`` + name (str): Name of the object to look up. + Returns: + object: Located object + """ + content = self.service_instance.RetrieveContent() + container = content.viewManager.CreateContainerView( + content.rootFolder, [vimtype], True) + for c in container.view: + if c.name == name: + return c + return None + class COTDeployESXi(COTDeploy): """Submodule for deploying VMs on ESXi and VMware vCenter/vSphere. @@ -421,10 +418,12 @@ def fixup_serial_ports(self): 'tcp', 'telnet', or 'device' """ logger.info("Fixing up serial ports...") - with SmarterConnection(self.ui, self.server, - self.username, self.password) as conn: + with SmarterConnection(self.ui, + self.server, + self.username, + self.password) as service_instance: logger.verbose("Connection established") - with PyVmomiVMReconfigSpec(conn, self.vm_name) as spec: + with PyVmomiVMReconfigSpec(service_instance, self.vm_name) as spec: logger.verbose("Spec created") spec.deviceChange = [] for s in self.serial_connection: diff --git a/COT/disks/__init__.py b/COT/disks/__init__.py index 92cc7f2..3b37fa8 100644 --- a/COT/disks/__init__.py +++ b/COT/disks/__init__.py @@ -21,10 +21,7 @@ .. autosummary:: :nosignatures: - convert_disk - create_disk - disk_representation_from_file - ~COT.disks.disk.DiskRepresentation + DiskRepresentation Disk modules ------------ @@ -32,85 +29,20 @@ .. autosummary:: :toctree: - COT.disks.disk COT.disks.iso COT.disks.qcow2 COT.disks.raw COT.disks.vmdk """ -import os +# flake8: noqa: F401 +from .disk import DiskRepresentation from .iso import ISO from .qcow2 import QCOW2 from .raw import RAW from .vmdk import VMDK - -_class_for_format = { - "iso": ISO, - "vmdk": VMDK, - "qcow2": QCOW2, - "raw": RAW, -} - - -def convert_disk(disk_image, new_directory, new_format, new_subformat=None): - """Convert a disk representation into a new format. - - Args: - disk_image (DiskRepresentation): Existing disk image as input. - new_directory (str): Directory to create new image under - new_format (str): Format to convert to. - new_subformat (str): (optional) Sub-format to convert to. - - Returns: - DiskRepresentation: Converted disk. - """ - if new_format not in _class_for_format: - raise NotImplementedError("No support for converting to type '{0}'" - .format(new_format)) - return _class_for_format[new_format].from_other_image(disk_image, - new_directory, - new_subformat) - - -def create_disk(disk_format, *args, **kwargs): - """Create a disk of the requested format. - - Args: - disk_format (str): Disk format such as 'iso' or 'vmdk'. - - For the other parameters, see :class:`~COT.disks.disk.DiskRepresentation`. - - Returns: - DiskRepresentation: Created disk - """ - if disk_format in _class_for_format: - return _class_for_format[disk_format](*args, **kwargs) - raise NotImplementedError("No support for files of type '{0}'" - .format(disk_format)) - - -def disk_representation_from_file(file_path): - """Get a DiskRepresentation appropriate to the given file. - - Args: - file_path (str): Path of existing file to represent. - - Returns: - DiskRepresentation: Representation of this file. - """ - if not os.path.exists(file_path): - raise IOError(2, "No such file or directory: {0}".format(file_path)) - for cls in [VMDK, QCOW2, ISO, RAW]: - if cls.file_is_this_type(file_path): - return cls(path=file_path) - raise NotImplementedError("No support for files of this type") - - __all__ = ( - 'convert_disk', - 'create_disk', - 'disk_representation_from_file', + 'DiskRepresentation', ) diff --git a/COT/disks/disk.py b/COT/disks/disk.py index 45842c4..5e2c818 100644 --- a/COT/disks/disk.py +++ b/COT/disks/disk.py @@ -27,28 +27,112 @@ class DiskRepresentation(object): disk_format = None """Disk format represented by this class.""" - def __init__(self, path, - disk_subformat=None, - capacity=None, - files=None): - """Create a representation of an existing disk or create a new disk. + @staticmethod + def subclasses(): + """List of subclasses of DiskRepresentation. + + Wraps the :meth:`class.__subclasses__` builtin. + """ + # pylint doesn't know about __subclasses__ + # https://github.com/PyCQA/pylint/issues/555 + # TODO: this should be fixed when pylint 2.0 is released + # pylint:disable=no-member + return DiskRepresentation.__subclasses__() + + @staticmethod + def supported_disk_formats(): + """List of disk format strings with support.""" + return [sc.disk_format for sc in DiskRepresentation.subclasses()] + + @staticmethod + def class_for_format(disk_format): + """Get the DiskRepresentation subclass associated with the given format. Args: - path (str): Path to existing file or path to create new file at. - disk_subformat (str): Subformat option(s) of the disk to create - (e.g., 'rockridge' for ISO, 'streamOptimized' for VMDK), if any. - capacity (int): Capacity of disk to create - files (int): Files to place in the filesystem of this disk. + disk_format (str): Disk format string such as 'iso' or 'vmdk' + + Returns: + DiskRepresentation: appropriate subclass object. + """ + return next((sc for sc in DiskRepresentation.subclasses() if + sc.disk_format == disk_format), + None) + + @staticmethod + def from_file(path): + """Get a DiskRepresentation instance appropriate to the given file. + + Args: + path (str): Path of existing file to represent. + + Returns: + DiskRepresentation: Representation of this file. + + Raises: + IOError: if no file exists at the given path + NotImplementedError: if the file is not a supported type. + """ + if not os.path.exists(path): + raise IOError(2, "No such file or directory: {0}".format(path)) + best_guess = None + best_confidence = 0 + for sc in DiskRepresentation.subclasses(): + confidence = sc.file_is_this_type(path) + if confidence > best_confidence: + logger.verbose("File %s may be a %s, with confidence %d", + path, sc.disk_format, confidence) + best_guess = sc + best_confidence = confidence + elif confidence > 0 and confidence == best_confidence: + logger.warning("For file %s, same confidence level (%d) for " + "classes %s and %s. Using %s", + path, confidence, best_guess, + sc, best_guess) + if best_guess is not None: + return best_guess(path) + else: + raise NotImplementedError("No support for files of this type") + + @classmethod + def for_new_file(cls, path, disk_format, **kwargs): + """Create a new disk file and return a DiskRepresentation. + + Args: + path (str): Path to create file at. + disk_format (str): Disk format to create, such as 'iso' or 'vmdk'. + **kwargs: Arguments to pass through to appropriate DiskRepresentation + subclass for this format. + + Returns: + DiskRepresentation: representation of the created file. + + Raises: + NotImplementedError: if ``disk_format`` is not supported. + """ + if cls.disk_format != disk_format: + cls = cls.class_for_format(disk_format) + if cls is None: + raise NotImplementedError("No support for files of type '{0}'" + .format(disk_format)) + cls.create_file(path, **kwargs) + return cls(path) + + def __init__(self, path): + """Create a representation of an existing disk. + + Args: + path (str): Path to existing file. """ if not path: raise ValueError("Path must be set to a valid value, but got {0}" .format(path)) - self._path = path - self._disk_subformat = disk_subformat - self._capacity = capacity - self._files = files if not os.path.exists(path): - self.create_file() + raise HelperError(2, "No such file or directory: '{0}'" + .format(path)) + self._path = path + self._disk_subformat = None + self._capacity = None + self._files = None @property def path(self): @@ -83,6 +167,28 @@ def files(self): return self._files raise NotImplementedError("Unable to determine file contents") + def convert_to(self, new_format, new_directory, new_subformat=None): + """Convert the disk file to a new format and return the new instance. + + Args: + new_format (str): Format to convert to. + new_subformat (str): (optional) Sub-format to convert to. + new_directory (str): Directory path to store new image into. + + Returns: + DiskRepresentation: Converted disk + + Raises: + NotImplementedError: if new_format is not a supported type + + .. seealso:: :meth:`from_other_image` + """ + sc = self.class_for_format(new_format) + if sc is None: + raise NotImplementedError("No support for converting to type '{0}'" + .format(new_format)) + return sc.from_other_image(self, new_directory, new_subformat) + @classmethod def from_other_image(cls, input_image, output_dir, output_subformat=None): """Convert the other disk image into an image of this type. @@ -105,7 +211,8 @@ def file_is_this_type(cls, path): path (str): Path to file to check. Returns: - bool: True (file matches this type) or False (file does not match) + int: Confidence that this file matches. 0 is definitely not a match, + 100 is definitely a match. Raises: HelperError: if no file exists at ``path``. @@ -123,21 +230,59 @@ def file_is_this_type(cls, path): "the output from qemu-img:\n{0}" .format(output)) file_format = match.group(1) - return file_format == cls.disk_format + if file_format == cls.disk_format: + return 100 + else: + return 0 + + @classmethod + def create_file(cls, path, files=None, capacity=None, **kwargs): + """Create a new disk image file of this type. - def create_file(self): - """Given parameters but not an existing file, create that file.""" - if os.path.exists(self.path): - raise RuntimeError("File already exists at {0}".format(self.path)) - if self._capacity is None and self._files is None: + Args: + path (str): Location to create disk file. + files (list): List of files to include in the disk's filesystem. + capacity (str): Disk capacity. + **kwargs: Subclasses and :meth:`_create_file` may accept additional + parameters. + + Raises: + ValueError: if path is not a valid string + RuntimeError: if a file already exists at path. + RuntimeError: if neither files nor capacity is specified + """ + if not path: + raise ValueError("Path must be set to a valid value, but got {0}" + .format(path)) + if os.path.exists(path): + raise RuntimeError("File already exists at {0}".format(path)) + if capacity is None and files is None: raise RuntimeError("Capacity and/or files must be specified!") - self._create_file() + cls._create_file(path, files=files, capacity=capacity, **kwargs) + + @classmethod + def _create_file(cls, path, files=None, capacity=None, disk_subformat=None, + **kwargs): + """Worker function for create_file(). + + Args: + path (str): Location to create disk file. + files (list): List of files to include in the disk's filesystem. + capacity (str): Disk capacity. + disk_subformat (str): Disk subformat such as 'streamOptimized'. + **kwargs: Subclasses may accept additional parameters. + + Raises: + NotImplementedError: this generic implementation doesn't know how to + handle any non-empty value for ``files``. + """ + # pylint: disable=unused-argument - def _create_file(self): - """Worker function for create_file().""" # Default implementation - create a blank disk using qemu-img - if self._files: + if files: raise NotImplementedError("Don't know how to create a disk of " "this format containing a filesystem") - helpers['qemu-img'].call(['create', '-f', self.disk_format, - self.path, self.capacity]) + args = ['create', '-f', cls.disk_format, path, capacity] + if disk_subformat is not None: + args += ['-o', 'subformat=' + disk_subformat] + helpers['qemu-img'].call(args) diff --git a/COT/disks/iso.py b/COT/disks/iso.py index c8433e7..32e11f3 100644 --- a/COT/disks/iso.py +++ b/COT/disks/iso.py @@ -67,29 +67,31 @@ def files(self): self._files = result return self._files - def _create_file(self): - """Create an ISO file.""" - if not self._files: + @staticmethod + def _create_file(path, disk_subformat="rockridge", files=None, **kwargs): + """Create an ISO file. + + Args: + path (str): Location to create the ISO file. + disk_subformat (str): Defaults to "rockridge". Set to "" to not + include Rock Ridge extensions. + files (list): List of files to include in this ISO (required) + **kwargs: unused + """ + if not files: raise RuntimeError("Unable to create an empty ISO file") - # Default subformat is to include Rock Ridge extensions. - # To not have these, use subformat="" - if self._disk_subformat is None: - self._disk_subformat = 'rockridge' # We can use mkisofs, genisoimage, or xorriso, and fortunately # all three take similar parameters - args = ['-output', self.path, '-full-iso9660-filenames', + args = ['-output', path, '-full-iso9660-filenames', '-iso-level', '2', '-allow-lowercase'] - if self._disk_subformat == 'rockridge': + if disk_subformat == 'rockridge': args.append('-r') - args += self.files + args += files helper = helper_select(['mkisofs', 'genisoimage', 'xorriso']) if helper.name == "xorriso": args = ['-as', 'mkisofs'] + args helper.call(args) - self._disk_subformat = None - self._files = None - @classmethod def file_is_this_type(cls, path): """Detect whether the given file is an ISO image. @@ -109,10 +111,10 @@ def file_is_this_type(cls, path): if helpers['isoinfo']: try: helpers['isoinfo'].call(['-i', path, '-d']) - return True + return 100 except HelperError: # Not an ISO - return False + return 0 # else, try to detect ISO files by file magic number with open(path, 'rb') as f: @@ -120,8 +122,8 @@ def file_is_this_type(cls, path): f.seek(offset) magic = f.read(5).decode('ascii', 'ignore') if magic == "CD001": - return True - return False + return 100 + return 0 @classmethod def from_other_image(cls, input_image, output_dir, output_subformat=None): diff --git a/COT/disks/raw.py b/COT/disks/raw.py index 0b2b0d3..7fbfca9 100644 --- a/COT/disks/raw.py +++ b/COT/disks/raw.py @@ -54,38 +54,58 @@ def files(self): self._files = result return self._files - def _create_file(self): - """Create a raw disk image file.""" - if not self.files: - helpers['qemu-img'].call(['create', '-f', 'raw', - self.path, self.capacity]) - else: - if not self._capacity: - # What size disk do we need to contain the requested file(s)? - capacity_val = 0 - for content_file in self.files: - capacity_val += os.path.getsize(content_file) - # Round capacity to the next larger multiple of 8 MB - # just to be safe... - capacity_val = int(8 * ((capacity_val / 1024 / 1024 / 8) + 1)) - capacity_str = "{0}M".format(capacity_val) - self._capacity = capacity_str - logger.verbose( - "To contain files %s, disk capacity of %s will be %s", - self.files, self.path, capacity_str) - logger.info("Calling fatdisk to create/format a raw disk image") - helpers['fatdisk'].call( - [self.path, 'format', 'size', self.capacity, 'fat32']) - for content_file in self.files: - logger.verbose("Calling fatdisk to add %s to the image", - content_file) - helpers['fatdisk'].call( - [self.path, 'fileadd', content_file, - os.path.basename(content_file)]) - logger.info("All requested files successfully added to %s", - self.path) - self._capacity = None - self._files = None + @classmethod + def file_is_this_type(cls, path): + """Whether this file is a RAW image. + + Any file conceivably can be a RAW image; there's no file magic number. + + For the parameters, see :meth:`DiskRepresentation.file_is_this_type`. + """ + # Any file *could* be a RAW image, so let that be our fallback option, + # i.e., less than 100% confidence: + confidence = super(RAW, cls).file_is_this_type(path) + if confidence == 100: + confidence = 10 + return confidence + + @classmethod + def _create_file(cls, path, files=None, capacity=None, **kwargs): + """Create a raw disk image file. + + Args: + path (str): Location to create RAW file. + files (list): List of files to include in a FAT32 filesystem. + capacity (str): Disk capacity string. If not set, will be calculated + as just sufficient to include the given ``files``. + **kwargs: passed through to :meth:`DiskRepresentation._create_file` + """ + if not files: + super(RAW, cls)._create_file(path, capacity=capacity, **kwargs) + return + + if not capacity: + # What size disk do we need to contain the requested file(s)? + capacity_val = 0 + for content_file in files: + capacity_val += os.path.getsize(content_file) + # Round capacity to the next larger multiple of 8 MB + # just to be safe... + capacity_val = int(8 * ((capacity_val / 1024 / 1024 / 8) + 1)) + capacity_str = "{0}M".format(capacity_val) + capacity = capacity_str + logger.verbose( + "To contain files %s, disk capacity of %s will be %s", + files, path, capacity_str) + + logger.info("Calling fatdisk to create/format a raw disk image") + helpers['fatdisk'].call([path, 'format', 'size', capacity, 'fat32']) + for content_file in files: + logger.verbose("Calling fatdisk to add %s to the image", + content_file) + helpers['fatdisk'].call([path, 'fileadd', content_file, + os.path.basename(content_file)]) + logger.info("All requested files successfully added to %s", path) @classmethod def from_other_image(cls, input_image, output_dir, output_subformat=None): diff --git a/COT/disks/tests/test_api.py b/COT/disks/tests/test_api.py deleted file mode 100644 index e546bbe..0000000 --- a/COT/disks/tests/test_api.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python -# -# test_api.py - Unit test cases for public API of COT.disks module. -# -# October 2016, Glenn F. Matthews -# Copyright (c) 2014-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. - -"""Unit test cases for public API of COT.disks module.""" - -import os -import logging -import mock - -from COT.tests.ut import COT_UT -from COT.helpers import helpers -import COT.disks - -logger = logging.getLogger(__name__) - - -class TestDiskAPI(COT_UT): - """Test public API of COT.disks module.""" - - def test_create_disk_errors(self): - """Invalid inputs to create_disk().""" - # No support for VHD format at present - self.assertRaises(NotImplementedError, - COT.disks.create_disk, 'vhd', capacity="1M") - - def test_disk_representation_from_file_raw(self): - """Test if disk_representation_from_file() works for raw images.""" - temp_disk = os.path.join(self.temp_dir, 'foo.img') - helpers['qemu-img'].call(['create', '-f', 'raw', temp_disk, "16M"]) - dr = COT.disks.disk_representation_from_file(temp_disk) - self.assertEqual(dr.disk_format, "raw") - self.assertEqual(dr.disk_subformat, None) - - def test_disk_representation_from_file_qcow2(self): - """Test if disk_representation_from_file() works for qcow2 images.""" - temp_disk = os.path.join(self.temp_dir, 'foo.qcow2') - helpers['qemu-img'].call(['create', '-f', 'qcow2', temp_disk, "16M"]) - dr = COT.disks.disk_representation_from_file(temp_disk) - self.assertEqual(dr.disk_format, "qcow2") - self.assertEqual(dr.disk_subformat, None) - - def test_disk_representation_from_file_vmdk(self): - """Test if disk_representation_from_file() works for vmdk images.""" - dr = COT.disks.disk_representation_from_file(self.blank_vmdk) - self.assertEqual(dr.disk_format, "vmdk") - self.assertEqual(dr.disk_subformat, "streamOptimized") - - def test_disk_representation_from_file_iso(self): - """Test if disk_representation_from_file() works for iso images.""" - dr = COT.disks.disk_representation_from_file(self.input_iso) - self.assertEqual(dr.disk_format, "iso") - # In Travis CI we can't currently install isoinfo (via genisoimage). - # https://github.com/travis-ci/apt-package-whitelist/issues/588 - if helpers['isoinfo']: - self.assertEqual(dr.disk_subformat, "") - - def test_disk_representation_from_file_errors(self): - """Check disk_representation_from_file() error handling.""" - self.assertRaises(IOError, COT.disks.disk_representation_from_file, - "") - self.assertRaises(IOError, COT.disks.disk_representation_from_file, - "/foo/bar/baz") - self.assertRaises(TypeError, COT.disks.disk_representation_from_file, - None) - with mock.patch('COT.helpers.helper.check_output') as mock_co: - mock_co.return_value = "qemu-img info: unsupported command" - self.assertRaises(RuntimeError, - COT.disks.disk_representation_from_file, - self.input_vmdk) - # We support QCOW2 but not QCOW at present - temp_path = os.path.join(self.temp_dir, "foo.qcow") - helpers['qemu-img'].call(['create', '-f', 'qcow', temp_path, '8M']) - self.assertRaises(NotImplementedError, - COT.disks.disk_representation_from_file, temp_path) - - def test_convert_disk_errors(self): - """Invalid inputs to convert_disk().""" - self.assertRaises( - NotImplementedError, COT.disks.convert_disk, - COT.disks.disk_representation_from_file(self.blank_vmdk), - self.temp_dir, "frobozz") diff --git a/COT/disks/tests/test_disk_representation.py b/COT/disks/tests/test_disk_representation.py index 3266dec..fc524e8 100644 --- a/COT/disks/tests/test_disk_representation.py +++ b/COT/disks/tests/test_disk_representation.py @@ -21,16 +21,66 @@ import mock from COT.tests.ut import COT_UT -from COT.disks.disk import DiskRepresentation -from COT.helpers import HelperError +from COT.disks import DiskRepresentation +from COT.helpers import helpers, HelperError logger = logging.getLogger(__name__) # pylint: disable=missing-type-doc,missing-param-doc -class TestDisk(COT_UT): - """Test Disk class.""" +class TestDiskRepresentation(COT_UT): + """Test DiskRepresentation class.""" + + def test_disk_representation_from_file_raw(self): + """Test if DiskRepresentation.from_file() works for raw images.""" + temp_disk = os.path.join(self.temp_dir, 'foo.img') + helpers['qemu-img'].call(['create', '-f', 'raw', temp_disk, "16M"]) + dr = DiskRepresentation.from_file(temp_disk) + self.assertEqual(dr.disk_format, "raw") + self.assertEqual(dr.disk_subformat, None) + + def test_disk_representation_from_file_qcow2(self): + """Test if DiskRepresentation.from_file() works for qcow2 images.""" + temp_disk = os.path.join(self.temp_dir, 'foo.qcow2') + helpers['qemu-img'].call(['create', '-f', 'qcow2', temp_disk, "16M"]) + dr = DiskRepresentation.from_file(temp_disk) + self.assertEqual(dr.disk_format, "qcow2") + self.assertEqual(dr.disk_subformat, None) + + def test_disk_representation_from_file_vmdk(self): + """Test if DiskRepresentation.from_file() works for vmdk images.""" + dr = DiskRepresentation.from_file(self.blank_vmdk) + self.assertEqual(dr.disk_format, "vmdk") + self.assertEqual(dr.disk_subformat, "streamOptimized") + + def test_disk_representation_from_file_iso(self): + """Test if DiskRepresentation.from_file() works for iso images.""" + dr = DiskRepresentation.from_file(self.input_iso) + self.assertEqual(dr.disk_format, "iso") + # In Travis CI we can't currently install isoinfo (via genisoimage). + # https://github.com/travis-ci/apt-package-whitelist/issues/588 + if helpers['isoinfo']: + self.assertEqual(dr.disk_subformat, "") + + def test_disk_representation_from_file_errors(self): + """Check DiskRepresentation.from_file() error handling.""" + self.assertRaises(IOError, DiskRepresentation.from_file, + "") + self.assertRaises(IOError, DiskRepresentation.from_file, + "/foo/bar/baz") + self.assertRaises(TypeError, DiskRepresentation.from_file, + None) + with mock.patch('COT.helpers.helper.check_output') as mock_co: + mock_co.return_value = "qemu-img info: unsupported command" + self.assertRaises(RuntimeError, + DiskRepresentation.from_file, + self.input_vmdk) + # We support QCOW2 but not QCOW at present + temp_path = os.path.join(self.temp_dir, "foo.qcow") + helpers['qemu-img'].call(['create', '-f', 'qcow', temp_path, '8M']) + self.assertRaises(NotImplementedError, + DiskRepresentation.from_file, temp_path) @mock.patch('COT.helpers.helper.check_output') def test_capacity_qemu_error(self, mock_check_output): @@ -55,17 +105,35 @@ def test_file_is_this_type_missing_file(self): self.assertRaises(HelperError, DiskRepresentation.file_is_this_type, "/foo/bar") + def test_for_new_file_errors(self): + """Invalid inputs to for_new_file().""" + # No support for VHD format at present + self.assertRaises(NotImplementedError, + DiskRepresentation.for_new_file, + path=os.path.join(self.temp_dir, "foo.vhd"), + disk_format="vhd", + capacity="1M") + def test_create_file_path_mandatory(self): """Can't create a file without specifying a path.""" - self.assertRaises(ValueError, DiskRepresentation, path=None) + self.assertRaises(ValueError, + DiskRepresentation.create_file, path=None) def test_create_file_already_extant(self): """Can't call create_file if the file already exists.""" self.assertRaises(RuntimeError, - DiskRepresentation(path=self.blank_vmdk).create_file) + DiskRepresentation.create_file, + path=self.blank_vmdk) def test_create_file_insufficient_info(self): """Can't create a file with neither files nor capacity.""" self.assertRaises(RuntimeError, - DiskRepresentation, + DiskRepresentation.create_file, path=os.path.join(self.temp_dir, "foo")) + + def test_convert_to_errors(self): + """Invalid inputs to convert_to().""" + self.assertRaises( + NotImplementedError, + DiskRepresentation.from_file(self.blank_vmdk).convert_to, + "frobozz", self.temp_dir) diff --git a/COT/disks/tests/test_iso.py b/COT/disks/tests/test_iso.py index d2beffa..7119b88 100644 --- a/COT/disks/tests/test_iso.py +++ b/COT/disks/tests/test_iso.py @@ -61,8 +61,9 @@ def test_representation(self): def test_create_with_files(self): """Creation of a ISO with specific file contents.""" - iso = ISO(path=os.path.join(self.temp_dir, "out.iso"), - files=[self.input_ovf]) + disk_path = os.path.join(self.temp_dir, "out.iso") + ISO.create_file(disk_path, files=[self.input_ovf]) + iso = ISO(disk_path) if helpers['isoinfo']: # Our default create format is rockridge self.assertEqual(iso.disk_subformat, "rockridge") @@ -85,9 +86,9 @@ def test_create_with_files(self): def test_create_with_files_non_rockridge(self): """Creation of a non-rock-ridge ISO with specific file contents.""" - iso = ISO(path=os.path.join(self.temp_dir, "out.iso"), - files=[self.input_ovf], - disk_subformat="") + disk_path = os.path.join(self.temp_dir, "out.iso") + ISO.create_file(disk_path, files=[self.input_ovf], disk_subformat="") + iso = ISO(disk_path) if helpers['isoinfo']: self.assertEqual(iso.disk_subformat, "") self.assertEqual(iso.files, @@ -109,7 +110,7 @@ def test_create_with_files_non_rockridge(self): def test_create_without_files(self): """Can't create an empty ISO.""" self.assertRaises(RuntimeError, - ISO, + ISO.create_file, path=os.path.join(self.temp_dir, "out.iso"), capacity="100") @@ -117,7 +118,7 @@ def test_create_without_files(self): def test_create_with_mkisofs(self, mock_call): """Creation of an ISO with mkisofs (default).""" helpers['mkisofs']._installed = True - ISO(path=self.foo_iso, files=[self.input_ovf]) + ISO.create_file(path=self.foo_iso, files=[self.input_ovf]) mock_call.assert_called_with( ['-output', self.foo_iso, '-full-iso9660-filenames', '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) @@ -127,7 +128,7 @@ def test_create_with_genisoimage(self, mock_call): """Creation of an ISO with genisoimage if mkisofs is unavailable.""" helpers['mkisofs']._installed = False helpers['genisoimage']._installed = True - ISO(path=self.foo_iso, files=[self.input_ovf]) + ISO.create_file(path=self.foo_iso, files=[self.input_ovf]) mock_call.assert_called_with( ['-output', self.foo_iso, '-full-iso9660-filenames', '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) @@ -138,7 +139,7 @@ def test_create_with_xorriso(self, mock_call): helpers['mkisofs']._installed = False helpers['genisoimage']._installed = False helpers['xorriso']._installed = True - ISO(path=self.foo_iso, files=[self.input_ovf]) + ISO.create_file(path=self.foo_iso, files=[self.input_ovf]) mock_call.assert_called_with( ['-as', 'mkisofs', '-output', self.foo_iso, '-full-iso9660-filenames', '-iso-level', '2', '-allow-lowercase', @@ -154,7 +155,7 @@ def test_create_no_helpers_available(self): helpers['port']._installed = False helpers['yum']._installed = False self.assertRaises(HelperNotFoundError, - ISO, + ISO.create_file, path=self.foo_iso, files=[self.input_ovf]) @@ -162,7 +163,8 @@ def test_create_no_helpers_available(self): def test_create_with_mkisofs_non_rockridge(self, mock_call): """Creation of a non-Rock-Ridge ISO with mkisofs (default).""" helpers['mkisofs']._installed = True - ISO(path=self.foo_iso, files=[self.input_ovf], disk_subformat="") + ISO.create_file(path=self.foo_iso, files=[self.input_ovf], + disk_subformat="") mock_call.assert_called_with( ['-output', self.foo_iso, '-full-iso9660-filenames', '-iso-level', '2', '-allow-lowercase', self.input_ovf]) diff --git a/COT/disks/tests/test_qcow2.py b/COT/disks/tests/test_qcow2.py index 34a6c02..eb824a1 100644 --- a/COT/disks/tests/test_qcow2.py +++ b/COT/disks/tests/test_qcow2.py @@ -43,7 +43,7 @@ def setUp(self): def test_init_with_files_unsupported(self): """Creation of a QCOW2 with specific file contents is not supported.""" self.assertRaises(NotImplementedError, - QCOW2, + QCOW2.create_file, path=os.path.join(self.temp_dir, "out.qcow2"), files=[self.input_ovf]) @@ -64,7 +64,7 @@ def test_from_other_image_vmdk(self): @mock.patch('COT.helpers.qemu_img.QEMUImg.version', new_callable=mock.PropertyMock, return_value=StrictVersion("1.0.0")) - @mock.patch('COT.disks.qcow2.QCOW2.create_file') + @mock.patch('os.path.exists', return_value=True) @mock.patch('COT.disks.raw.RAW.from_other_image') @mock.patch('COT.helpers.qemu_img.QEMUImg.call') def test_convert_from_vmdk_old_qemu(self, @@ -84,7 +84,7 @@ def test_convert_from_vmdk_old_qemu(self, @mock.patch('COT.helpers.qemu_img.QEMUImg.version', new_callable=mock.PropertyMock, return_value=StrictVersion("1.2.0")) - @mock.patch('COT.disks.qcow2.QCOW2.create_file') + @mock.patch('os.path.exists', return_value=True) @mock.patch('COT.helpers.qemu_img.QEMUImg.call') @mock.patch('COT.helpers.vmdktool.VMDKTool.call') def test_convert_from_vmdk_new_qemu(self, mock_vmdktool, mock_qemuimg, *_): diff --git a/COT/disks/tests/test_raw.py b/COT/disks/tests/test_raw.py index df7055a..3f11706 100644 --- a/COT/disks/tests/test_raw.py +++ b/COT/disks/tests/test_raw.py @@ -23,7 +23,7 @@ import mock from COT.tests.ut import COT_UT -from COT.disks import RAW, VMDK, disk_representation_from_file +from COT.disks import RAW, VMDK, DiskRepresentation from COT.helpers import HelperError logger = logging.getLogger(__name__) @@ -42,7 +42,7 @@ def test_representation_invalid(self): def test_convert_from_vmdk(self): """Test conversion of a RAW image from a VMDK.""" - old = disk_representation_from_file(self.blank_vmdk) + old = DiskRepresentation.from_file(self.blank_vmdk) raw = RAW.from_other_image(old, self.temp_dir) self.assertEqual(raw.disk_format, 'raw') @@ -51,7 +51,7 @@ def test_convert_from_vmdk(self): @mock.patch('COT.helpers.qemu_img.QEMUImg.version', new_callable=mock.PropertyMock, return_value=StrictVersion("1.0.0")) - @mock.patch('COT.disks.raw.RAW.create_file') + @mock.patch('os.path.exists', return_value=True) @mock.patch('COT.helpers.qemu_img.QEMUImg.call') @mock.patch('COT.helpers.vmdktool.VMDKTool.call') def test_convert_from_vmdk_old_qemu(self, mock_vmdktool, mock_qemuimg, *_): @@ -65,7 +65,7 @@ def test_convert_from_vmdk_old_qemu(self, mock_vmdktool, mock_qemuimg, *_): @mock.patch('COT.helpers.qemu_img.QEMUImg.version', new_callable=mock.PropertyMock, return_value=StrictVersion("1.2.0")) - @mock.patch('COT.disks.raw.RAW.create_file') + @mock.patch('os.path.exists', return_value=True) @mock.patch('COT.helpers.qemu_img.QEMUImg.call') @mock.patch('COT.helpers.vmdktool.VMDKTool.call') def test_convert_from_vmdk_new_qemu(self, mock_vmdktool, mock_qemuimg, *_): @@ -79,24 +79,26 @@ def test_convert_from_vmdk_new_qemu(self, mock_vmdktool, mock_qemuimg, *_): def test_create_with_capacity(self): """Creation of a raw image of a particular size.""" - raw = RAW(path=os.path.join(self.temp_dir, "out.raw"), - capacity="16M") + disk_path = os.path.join(self.temp_dir, "out.raw") + RAW.create_file(disk_path, capacity="16M") + raw = RAW(disk_path) self.assertEqual(raw.disk_format, 'raw') self.assertEqual(raw.disk_subformat, None) def test_create_with_files(self): """Creation of a raw image with specific file contents.""" - raw = RAW(path=os.path.join(self.temp_dir, "out.img"), - files=[self.input_ovf]) + disk_path = os.path.join(self.temp_dir, "out.raw") + RAW.create_file(disk_path, files=[self.input_ovf]) + raw = RAW(disk_path) self.assertEqual(raw.files, [os.path.basename(self.input_ovf)]) self.assertEqual(raw.capacity, "8388608") def test_create_with_files_and_capacity(self): """Creation of raw image with specified capacity and file contents.""" - raw = RAW(path=os.path.join(self.temp_dir, "out.img"), - files=[self.input_ovf], - capacity="64M") + disk_path = os.path.join(self.temp_dir, "out.img") + RAW.create_file(disk_path, files=[self.input_ovf], capacity="64M") + raw = RAW(disk_path) self.assertEqual(raw.files, [os.path.basename(self.input_ovf)]) self.assertEqual(raw.capacity, "67108864") diff --git a/COT/disks/tests/test_vmdk.py b/COT/disks/tests/test_vmdk.py index a001b4d..8460381 100644 --- a/COT/disks/tests/test_vmdk.py +++ b/COT/disks/tests/test_vmdk.py @@ -23,7 +23,7 @@ import mock from COT.tests.ut import COT_UT -from COT.disks import VMDK, disk_representation_from_file +from COT.disks import VMDK, DiskRepresentation from COT.helpers import helpers, HelperError logger = logging.getLogger(__name__) @@ -40,7 +40,7 @@ def other_format_to_vmdk_test(self, disk_format, temp_disk = os.path.join(self.temp_dir, "foo.{0}".format(disk_format)) helpers['qemu-img'].call(['create', '-f', disk_format, temp_disk, "16M"]) - old = disk_representation_from_file(temp_disk) + old = DiskRepresentation.from_file(temp_disk) vmdk = VMDK.from_other_image(old, self.temp_dir, output_subformat) self.assertEqual(vmdk.disk_format, 'vmdk') @@ -80,8 +80,9 @@ def test_capacity(self): def test_create_default(self): """Default creation logic.""" - vmdk = VMDK(path=os.path.join(self.temp_dir, "foo.vmdk"), - capacity="16M") + disk_path = os.path.join(self.temp_dir, "foo.vmdk") + VMDK.create_file(path=disk_path, capacity="16M") + vmdk = VMDK(disk_path) self.assertEqual(vmdk.path, os.path.join(self.temp_dir, "foo.vmdk")) self.assertEqual(vmdk.disk_format, "vmdk") self.assertEqual(vmdk.disk_subformat, "streamOptimized") @@ -89,9 +90,10 @@ def test_create_default(self): def test_create_stream_optimized(self): """Explicit subformat specification.""" - vmdk = VMDK(path=os.path.join(self.temp_dir, "foo.vmdk"), - capacity="16M", - disk_subformat="streamOptimized") + disk_path = os.path.join(self.temp_dir, "foo.vmdk") + VMDK.create_file(path=disk_path, capacity="16M", + disk_subformat="streamOptimized") + vmdk = VMDK(disk_path) self.assertEqual(vmdk.path, os.path.join(self.temp_dir, "foo.vmdk")) self.assertEqual(vmdk.disk_format, "vmdk") self.assertEqual(vmdk.disk_subformat, "streamOptimized") @@ -99,9 +101,10 @@ def test_create_stream_optimized(self): def test_create_monolithic_sparse(self): """Explicit subformat specification.""" - vmdk = VMDK(path=os.path.join(self.temp_dir, "foo.vmdk"), - capacity="16M", - disk_subformat="monolithicSparse") + disk_path = os.path.join(self.temp_dir, "foo.vmdk") + VMDK.create_file(path=disk_path, capacity="16M", + disk_subformat="monolithicSparse") + vmdk = VMDK(disk_path) self.assertEqual(vmdk.path, os.path.join(self.temp_dir, "foo.vmdk")) self.assertEqual(vmdk.disk_format, "vmdk") self.assertEqual(vmdk.disk_subformat, "monolithicSparse") @@ -110,5 +113,6 @@ def test_create_monolithic_sparse(self): def test_create_files_unsupported(self): """No support for creating a VMDK with a filesystem.""" self.assertRaises(NotImplementedError, - VMDK, path=os.path.join(self.temp_dir, "foo.vmdk"), + VMDK.create_file, + path=os.path.join(self.temp_dir, "foo.vmdk"), files=[self.input_iso]) diff --git a/COT/disks/vmdk.py b/COT/disks/vmdk.py index 2e5adb9..b9cc978 100644 --- a/COT/disks/vmdk.py +++ b/COT/disks/vmdk.py @@ -95,16 +95,14 @@ def from_other_image(cls, input_image, output_dir, output_path]) return cls(output_path) - def _create_file(self): - """Worker function for create_file().""" - if self._files: - raise NotImplementedError("Don't know how to create a disk of " - "this format containing a filesystem") - if self._disk_subformat is None: - self._disk_subformat = "streamOptimized" + @classmethod + def _create_file(cls, path, disk_subformat="streamOptimized", **kwargs): + """Worker function for create_file(). - helpers['qemu-img'].call(['create', '-f', self.disk_format, - '-o', 'subformat=' + self._disk_subformat, - self.path, self.capacity]) - self._disk_subformat = None - self._capacity = None + Args: + path (str): Location to create VMDK file. + disk_subformat (str): Defaults to "streamOptimized". + **kwargs: See :meth:`DiskRepresentation._create_file` + """ + super(VMDK, cls)._create_file(path, disk_subformat=disk_subformat, + **kwargs) diff --git a/COT/edit_hardware.py b/COT/edit_hardware.py index d8a4fd3..e09eff1 100644 --- a/COT/edit_hardware.py +++ b/COT/edit_hardware.py @@ -130,7 +130,7 @@ def cpus(self, value): raise InvalidInputError("cpus value must be an integer") if value < 1: raise InvalidInputError("CPU count must be at least 1") - self.vm.platform.validate_cpu_count(value) + self.ui.validate_value(self.vm.platform.validate_cpu_count, value) self._cpus = value @property @@ -169,7 +169,8 @@ def memory(self, value): logger.warning("Memory units not specified, " "guessing '%s' means '%s MiB'", mem_value, mem_value) - self.vm.platform.validate_memory_amount(mem_value) + self.ui.validate_value(self.vm.platform.validate_memory_amount, + mem_value) self._memory = mem_value @property @@ -183,7 +184,8 @@ def nics(self, value): value = int(value) except ValueError: raise InvalidInputError("nics value must be an integer") - self.vm.platform.validate_nic_count(value) + non_negative_int(value, label="nics") + self.ui.validate_value(self.vm.platform.validate_nic_count, value) self._nics = value @property @@ -215,7 +217,7 @@ def nic_types(self): @nic_types.setter def nic_types(self, value): value = [canonicalize_nic_subtype(v) for v in value] - self.vm.platform.validate_nic_types(value) + self.ui.validate_value(self.vm.platform.validate_nic_types, value) self._nic_types = value @property @@ -229,7 +231,8 @@ def serial_ports(self, value): value = int(value) except ValueError: raise InvalidInputError("serial_ports value must be an integer") - self.vm.platform.validate_serial_count(value) + non_negative_int(value, label="serial_ports") + self.ui.validate_value(self.vm.platform.validate_serial_count, value) self._serial_ports = value @property @@ -829,6 +832,6 @@ def guess_list_wildcard(known_values): return None -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover import doctest doctest.testmod() diff --git a/COT/edit_properties.py b/COT/edit_properties.py index 06d833d..aad4593 100644 --- a/COT/edit_properties.py +++ b/COT/edit_properties.py @@ -384,6 +384,6 @@ def create_subparser(self): p.set_defaults(instance=self) -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover import doctest doctest.testmod() diff --git a/COT/file_reference.py b/COT/file_reference.py index 782d588..be0793d 100644 --- a/COT/file_reference.py +++ b/COT/file_reference.py @@ -254,6 +254,6 @@ def add_to_archive(self, tarf): self.close() -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover import doctest doctest.testmod() diff --git a/COT/helpers/__init__.py b/COT/helpers/__init__.py index ab4e02c..8ebd678 100644 --- a/COT/helpers/__init__.py +++ b/COT/helpers/__init__.py @@ -55,18 +55,21 @@ Helper, PackageManager, helpers, HelperError, HelperNotFoundError, helper_select, ) -from .apt_get import AptGet # noqa -from .brew import Brew # noqa -from .fatdisk import FatDisk # noqa -from .gcc import GCC # noqa -from .isoinfo import ISOInfo # noqa -from .make import Make # noqa -from .mkisofs import MkISOFS, GenISOImage, XorrISO # noqa -from .ovftool import OVFTool # noqa -from .port import Port # noqa -from .qemu_img import QEMUImg # noqa -from .vmdktool import VMDKTool # noqa -from .yum import Yum # noqa + +# flake8: noqa: F401 + +from .apt_get import AptGet +from .brew import Brew +from .fatdisk import FatDisk +from .gcc import GCC +from .isoinfo import ISOInfo +from .make import Make +from .mkisofs import MkISOFS, GenISOImage, XorrISO +from .ovftool import OVFTool +from .port import Port +from .qemu_img import QEMUImg +from .vmdktool import VMDKTool +from .yum import Yum # pylint doesn't know about __subclasses__ # https://github.com/PyCQA/pylint/issues/555 diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 5d4726d..bd91184 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -729,6 +729,6 @@ def _name_min_ver_from_choice(choice): raise HelperNotFoundError(msg) -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover import doctest doctest.testmod() diff --git a/COT/inject_config.py b/COT/inject_config.py index a0bf473..fe837a0 100644 --- a/COT/inject_config.py +++ b/COT/inject_config.py @@ -3,7 +3,7 @@ # inject_config.py - Implements "cot inject-config" command # # February 2014, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 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. # @@ -22,7 +22,7 @@ from COT.add_disk import add_disk_worker from COT.data_validation import ValueUnsupportedError, InvalidInputError -from COT.disks import create_disk +from COT.disks import DiskRepresentation from COT.submodule import COTSubmodule logger = logging.getLogger(__name__) @@ -74,7 +74,7 @@ def config_file(self, value): if not self.vm.platform.CONFIG_TEXT_FILE: raise InvalidInputError( "Configuration file not supported for platform {0}" - .format(self.vm.platform.__name__)) + .format(self.vm.platform)) self._config_file = value @property @@ -98,7 +98,7 @@ def secondary_config_file(self, value): if not self.vm.platform.SECONDARY_CONFIG_TEXT_FILE: raise InvalidInputError( "Secondary configuration file not supported " - "for platform {0}".format(self.vm.platform.__name__)) + "for platform {0}".format(self.vm.platform)) self._secondary_config_file = value @property @@ -135,7 +135,7 @@ def run(self): Raises: InvalidInputError: if :func:`ready_to_run` reports ``False`` ValueUnsupportedError: if the - :const:`~COT.platforms.GenericPlatform.BOOTSTRAP_DISK_TYPE` of + :const:`~COT.platforms.Platform.BOOTSTRAP_DISK_TYPE` of the associated VM's :attr:`~COT.vm_description.VMDescription.platform` is not 'cdrom' or 'harddisk' @@ -194,19 +194,19 @@ def run(self): # pylint:disable=redefined-variable-type if platform.BOOTSTRAP_DISK_TYPE == 'cdrom': bootstrap_file = os.path.join(vm.working_dir, 'config.iso') - disk_image = create_disk(disk_format='iso', - path=bootstrap_file, - files=config_files) + disk_format = 'iso' elif platform.BOOTSTRAP_DISK_TYPE == 'harddisk': bootstrap_file = os.path.join(vm.working_dir, 'config.img') - disk_image = create_disk(disk_format='raw', - path=bootstrap_file, - files=config_files) + disk_format = 'raw' else: raise ValueUnsupportedError("bootstrap disk drive type", platform.BOOTSTRAP_DISK_TYPE, "'cdrom' or 'harddisk'") + disk_image = DiskRepresentation.for_new_file(bootstrap_file, + disk_format, + files=config_files) + # Inject the disk image into the OVA, using "add-disk" functionality add_disk_worker( ui=self.ui, diff --git a/COT/logging_.py b/COT/logging_.py new file mode 100644 index 0000000..6922712 --- /dev/null +++ b/COT/logging_.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# logging.py - Common OVF Tool infrastructure for logging +# +# February 2017, Glenn F. Matthews +# Copyright (c) 2013-2017 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. + +"""Logging module for the Common OVF Tool (COT). + +**Classes** + +.. autosummary:: + :nosignatures: + + COTFormatter +""" + +from __future__ import absolute_import + +import logging +from verboselogs import VerboseLogger +from colorlog import ColoredFormatter + +# VerboseLogger adds a log level 'verbose' between 'info' and 'debug'. +# This lets us be a bit more fine-grained in our logging verbosity. +logging.setLoggerClass(VerboseLogger) + +logger = logging.getLogger(__name__) + + +class COTFormatter(ColoredFormatter): + r"""Logging formatter with colorization and variable verbosity. + + COT logs are formatted differently (more or less verbosely) depending + on the logging level. + + .. seealso:: :class:`logging.Formatter` + + Args: + verbosity (int): Logging level as defined by :mod:`logging`. + + Examples:: + + >>> record = logging.LogRecord("test_func", logging.INFO, + ... "/fake.py", 22, "Hello world!", + ... None, None) + >>> record.created = 0 + >>> record.msecs = 0 + >>> COTFormatter(logging.DEBUG).format(record) + '\x1b[32m19:00:00.0 INFO: test_func Hello world!\x1b[0m' + >>> COTFormatter(logging.VERBOSE).format(record) + '\x1b[32m INFO: test_func Hello world!\x1b[0m' + >>> COTFormatter(logging.INFO).format(record) + '\x1b[32m INFO: Hello world!\x1b[0m' + """ + + LOG_COLORS = { + 'DEBUG': 'blue', + 'VERBOSE': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'red', + } + + def __init__(self, verbosity=logging.INFO): + """Create formatter for COT log output with the given verbosity.""" + format_string = "%(log_color)s" + datefmt = None + if verbosity <= logging.DEBUG: + format_string += "%(asctime)s.%(msecs)d " + datefmt = "%H:%M:%S" + format_string += "%(levelname)8s: " + if verbosity <= logging.VERBOSE: + format_string += "%(name)-22s " + format_string += "%(message)s" + super(COTFormatter, self).__init__(format_string, + datefmt=datefmt, + log_colors=self.LOG_COLORS) + + +if __name__ == "__main__": # pragma: no cover + import doctest + doctest.testmod() diff --git a/COT/ovf/__init__.py b/COT/ovf/__init__.py index ff6b795..af9fceb 100644 --- a/COT/ovf/__init__.py +++ b/COT/ovf/__init__.py @@ -1,5 +1,5 @@ # June 2016, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 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. # @@ -43,6 +43,7 @@ COT.ovf.hardware COT.ovf.item COT.ovf.name_helper + COT.ovf.utilities """ from .ovf import OVF diff --git a/COT/ovf/hardware.py b/COT/ovf/hardware.py index 9e2b503..f1794d0 100644 --- a/COT/ovf/hardware.py +++ b/COT/ovf/hardware.py @@ -385,7 +385,7 @@ def _update_cloned_item(self, new_item, new_item_profiles, item_count): 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` + :meth:`COT.platform.Platform.guess_nic_name` Returns: OVFItem: Updated :param:`new_item` diff --git a/COT/ovf/item.py b/COT/ovf/item.py index 1b5ee1d..c38193f 100644 --- a/COT/ovf/item.py +++ b/COT/ovf/item.py @@ -814,6 +814,6 @@ def generate_items(self): return item_list -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover import doctest doctest.testmod() diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index 24b7705..44cc08e 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -3,7 +3,7 @@ # ovf.py - Class for OVF/OVA handling # # August 2013, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 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. # @@ -16,15 +16,6 @@ """Module for handling OVF and OVA virtual machine description files. -**Functions** - -.. autosummary:: - :nosignatures: - - byte_count - byte_string - factor_bytes - **Classes** .. autosummary:: @@ -55,135 +46,20 @@ ValueTooHighError, ValueUnsupportedError, canonicalize_nic_subtype, ) from COT.file_reference import FileOnDisk, FileInTAR -from COT.platforms import platform_from_product_class, GenericPlatform -from COT.disks import convert_disk, disk_representation_from_file +from COT.platforms import Platform +from COT.disks import DiskRepresentation +from COT.ui_shared import pretty_bytes from COT.ovf.name_helper import name_helper from COT.ovf.hardware import OVFHardware, OVFHardwareDataError from COT.ovf.item import list_union +from COT.ovf.utilities import ( + programmatic_bytes_to_int, int_bytes_to_programmatic_units +) logger = logging.getLogger(__name__) -def byte_count(base_val, multiplier): - """Convert an OVF-style value + multiplier into decimal byte count. - - Inverse operation of :func:`factor_bytes`. - - Args: - base_val (str): Base value string (value of ``ovf:capacity``, etc.) - multiplier (str): Multiplier string (value of - ``ovf:capacityAllocationUnits``, etc.) - - Returns: - int: Number of bytes - - Examples: - :: - - >>> byte_count("128", "byte * 2^20") - 134217728 - >>> byte_count("512", "MegaBytes") - 536870912 - """ - if not multiplier: - return int(base_val) - - # multiplier like 'byte * 2^30' - match = re.search(r"2\^(\d+)", multiplier) - if match: - return int(base_val) << int(match.group(1)) - - # multiplier like 'MegaBytes' - si_prefixes = ["", "kilo", "mega", "giga", "tera"] - match = re.search("^(.*)bytes$", multiplier, re.IGNORECASE) - if match: - shift = si_prefixes.index(match.group(1).lower()) - # Technically the below is correct: - # return int(base_val) * (1000 ** shift) - # but instead we'll reflect common usage: - return int(base_val) << (10 * shift) - - if multiplier and multiplier != 'byte': - logger.warning("Unknown multiplier string '%s'", multiplier) - - return int(base_val) - - -def factor_bytes(byte_value): - """Convert a byte count into OVF-style bytes + multiplier. - - Inverse operation of :func:`byte_count` - - Args: - byte_value (int): Number of bytes - - Returns: - tuple: ``(base_val, multiplier)`` - - Examples: - :: - - >>> factor_bytes(134217728) - ('128', 'byte * 2^20') - >>> factor_bytes(134217729) - ('134217729', 'byte') - """ - shift = 0 - byte_value = int(byte_value) - while byte_value % 1024 == 0: - shift += 10 - byte_value /= 1024 - byte_str = str(int(byte_value)) - if shift == 0: - return (byte_str, "byte") - return (byte_str, "byte * 2^{0}".format(shift)) - - -def byte_string(byte_value, base_shift=0): - """Pretty-print the given bytes value. - - Args: - byte_value (float): Value - base_shift (int): Base value of byte_value - (0 = bytes, 1 = KiB, 2 = MiB, etc.) - - Returns: - str: Pretty-printed byte string such as "1.00 GiB" - - Examples: - :: - - >>> byte_string(512) - '512 B' - >>> byte_string(512, 2) - '512 MiB' - >>> byte_string(65536, 2) - '64 GiB' - >>> byte_string(65547) - '64.01 KiB' - >>> byte_string(65530, 3) - '63.99 TiB' - >>> byte_string(1023850) - '999.9 KiB' - >>> byte_string(1024000) - '1000 KiB' - >>> byte_string(1048575) - '1024 KiB' - >>> byte_string(1049200) - '1.001 MiB' - >>> byte_string(2560) - '2.5 KiB' - """ - tags = ["B", "KiB", "MiB", "GiB", "TiB"] - byte_value = float(byte_value) - shift = base_shift - while byte_value >= 1024.0: - byte_value /= 1024.0 - shift += 1 - return "{0:.4g} {1}".format(byte_value, tags[shift]) - - class OVF(VMDescription, XML): """Representation of the contents of an OVF or OVA. @@ -483,13 +359,13 @@ def product_class(self, product_class): def platform(self): """The platform type, as determined from the OVF descriptor. - This will be the class :class:`~COT.platforms.GenericPlatform` or + This will be the class :class:`~COT.platforms.Platform` or a more-specific subclass if recognized as such. """ if self._platform is None: - self._platform = platform_from_product_class(self.product_class) + self._platform = Platform.for_product_string(self.product_class) logger.info("OVF product class %s --> platform %s", - self.product_class, self.platform.__name__) + self.product_class, self.platform) return self._platform def validate_hardware(self): @@ -538,7 +414,7 @@ def _validate_helper(label, fn, *args): ram_item = self.hardware.find_item('memory', profile=profile_id) if ram_item: - megabytes = (byte_count( + megabytes = (programmatic_bytes_to_int( ram_item.get_value(self.VIRTUAL_QUANTITY, [profile_id]), ram_item.get_value(self.ALLOCATION_UNITS, [profile_id]) ) / (1024 * 1024)) @@ -860,7 +736,7 @@ def validate_and_update_file_references(self): # It seems wasteful to extract the disk file (could be # quite large) from the TAR just to check, so we don't. if file_ref.file_path is not None: - dr = disk_representation_from_file(file_ref.file_path) + dr = DiskRepresentation.from_file(file_ref.file_path) real_capacity = dr.capacity disk_item = self.find_disk_from_file_id( @@ -927,9 +803,9 @@ def _info_string_header(self, width): str_list = [] str_list.append('-' * width) str_list.append(self.input_file) - if self.platform and self.platform is not GenericPlatform: + if self.platform and self.platform.__class__ is not Platform: str_list.append("COT detected platform type: {0}" - .format(self.platform.PLATFORM_NAME)) + .format(self.platform)) str_list.append('-' * width) return '\n'.join(str_list) @@ -1068,7 +944,7 @@ def _info_strings_for_file(self, file_obj): # TODO - check file size in working dir and/or tarfile file_size_str = "" else: - file_size_str = byte_string(file_obj.get(self.FILE_SIZE)) + file_size_str = pretty_bytes(file_obj.get(self.FILE_SIZE)) disk_obj = self.find_disk_from_file_id(file_obj.get(self.FILE_ID)) if disk_obj is None: @@ -1077,7 +953,7 @@ def _info_strings_for_file(self, file_obj): device_item = self.find_item_from_file(file_obj) else: disk_id = disk_obj.get(self.DISK_ID) - disk_cap_string = byte_string( + disk_cap_string = pretty_bytes( self.get_capacity_from_disk(disk_obj)) device_item = self.find_item_from_disk(disk_obj) device_str = self.device_info_str(device_item) @@ -1144,7 +1020,7 @@ def _info_string_files_disks(self, width, verbosity_option): attrib={self.FILE_ID: file_id}) if file_obj is not None: continue # already reported on above - disk_cap_string = byte_string(self.get_capacity_from_disk(disk)) + disk_cap_string = pretty_bytes(self.get_capacity_from_disk(disk)) device_item = self.find_item_from_disk(disk) device_str = self.device_info_str(device_item) str_list.append(template.format(" (disk placeholder)", @@ -1465,7 +1341,7 @@ def profile_info_list(self, width=79, verbose=False): mem_bytes = 0 ram_item = self.hardware.find_item('memory', profile=profile_id) if ram_item: - mem_bytes = byte_count( + mem_bytes = programmatic_bytes_to_int( ram_item.get_value(self.VIRTUAL_QUANTITY, [profile_id]), ram_item.get_value(self.ALLOCATION_UNITS, [profile_id])) nics = self.hardware.get_item_count('ethernet', profile_id) @@ -1483,11 +1359,11 @@ def profile_info_list(self, width=79, verbose=False): str_list.append(template.format( profile_str, cpus, - byte_string(mem_bytes), + pretty_bytes(mem_bytes), nics, serials, "{0:2} / {1:>9}".format(disk_count, - byte_string(disks_size)))) + pretty_bytes(disks_size)))) if profile_id is not None and verbose: profile = self.find_child(self.deploy_opt_section, self.CONFIG, @@ -1922,12 +1798,12 @@ def config_file_to_properties(self, file_path, user_configurable=None): Raises: NotImplementedError: if the :attr:`platform` for this OVF does not define - :const:`~COT.platforms.GenericPlatform.LITERAL_CLI_STRING` + :const:`~COT.platforms.Platform.LITERAL_CLI_STRING` """ i = 0 if not self.platform.LITERAL_CLI_STRING: raise NotImplementedError("no known support for literal CLI on " + - self.platform.PLATFORM_NAME) + str(self.platform)) with open(file_path, 'r') as f: for line in f: line = line.strip() @@ -1968,8 +1844,9 @@ def convert_disk_if_needed(self, disk_image, kind): logger.debug("No disk conversion needed") return disk_image - return convert_disk(disk_image, self.working_dir, - 'vmdk', 'streamOptimized') + return disk_image.convert_to(new_format='vmdk', + new_subformat='streamOptimized', + new_directory=self.working_dir) def search_from_filename(self, filename): """From the given filename, try to find any existing objects. @@ -2974,7 +2851,7 @@ def get_capacity_from_disk(self, disk): """ cap = int(disk.get(self.DISK_CAPACITY)) cap_units = disk.get(self.DISK_CAP_UNITS, 'byte') - return byte_count(cap, cap_units) + return programmatic_bytes_to_int(cap, cap_units) def set_capacity_of_disk(self, disk, capacity_bytes): """Set the storage capacity of the given Disk. @@ -2990,11 +2867,7 @@ def set_capacity_of_disk(self, disk, capacity_bytes): # In OVF 0.9 only bytes is supported as a unit disk.set(self.DISK_CAPACITY, capacity_bytes) else: - (capacity, cap_units) = factor_bytes(capacity_bytes) + (capacity, cap_units) = int_bytes_to_programmatic_units( + capacity_bytes) disk.set(self.DISK_CAPACITY, capacity) disk.set(self.DISK_CAP_UNITS, cap_units) - - -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/COT/ovf/tests/test_doctests.py b/COT/ovf/tests/test_doctests.py index b795f9b..7b06fa6 100644 --- a/COT/ovf/tests/test_doctests.py +++ b/COT/ovf/tests/test_doctests.py @@ -26,6 +26,6 @@ def load_tests(*_): For the parameters, see :mod:`unittest`. The parameters are unused here. """ suite = TestSuite() - suite.addTests(DocTestSuite('COT.ovf.ovf')) suite.addTests(DocTestSuite('COT.ovf.item')) + suite.addTests(DocTestSuite('COT.ovf.utilities')) return suite diff --git a/COT/ovf/tests/test_ovf.py b/COT/ovf/tests/test_ovf.py index eb14086..2faee0a 100644 --- a/COT/ovf/tests/test_ovf.py +++ b/COT/ovf/tests/test_ovf.py @@ -3,7 +3,7 @@ # test_ovf.py - Unit test cases for COT OVF/OVA handling # # September 2013, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 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. # @@ -31,7 +31,6 @@ from COT.tests.ut import COT_UT from COT.ovf import OVF -from COT.ovf.ovf import byte_count, byte_string, factor_bytes from COT.vm_description import VMInitError from COT.data_validation import ValueUnsupportedError from COT.helpers import helpers, HelperError @@ -40,33 +39,6 @@ logger = logging.getLogger(__name__) -class TestByteString(COT_UT): - """Test cases for byte-count to string conversion functions.""" - - def test_byte_count(self): - """Test byte_count() function.""" - self.assertEqual(byte_count("128", "byte"), 128) - self.assertEqual(byte_count("1", "byte * 2^10"), 1024) - - # unknown multiplier is ignored with a warning - self.assertEqual(byte_count("100", "foobar"), 100) - self.assertLogged(levelname='WARNING', - msg="Unknown multiplier string '%s'", - args=('foobar',)) - - def test_factor_bytes(self): - """Test factor_bytes() function.""" - self.assertEqual(factor_bytes("2147483648"), ("2", "byte * 2^30")) - self.assertEqual(factor_bytes(2147483649), ("2147483649", "byte")) - - def test_byte_string(self): - """Test byte_string() function.""" - self.assertEqual(byte_string(1024), "1 KiB") - self.assertEqual(byte_string(250691584), "239.1 MiB") - self.assertEqual(byte_string(2560, base_shift=2), "2.5 GiB") - self.assertEqual(byte_string(512, base_shift=2), "512 MiB") - - class TestOVFInputOutput(COT_UT): """Test cases for OVF file input/output.""" diff --git a/COT/ovf/tests/test_utilities.py b/COT/ovf/tests/test_utilities.py new file mode 100644 index 0000000..05e9858 --- /dev/null +++ b/COT/ovf/tests/test_utilities.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# +# test_utilities.py - Unit test cases for COT OVF/OVA utility functions +# +# February 2017, Glenn F. Matthews +# Copyright (c) 2013-2017 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. + +"""Unit test cases for COT.ovf.utilities module. + +Most test cases are implemented as doctests in the COT.ovf.utilities +module; this is for additional tests that are impractical as doctests. +""" + +from COT.tests.ut import COT_UT +from COT.ovf.utilities import programmatic_bytes_to_int + + +class TestProgrammaticUnits(COT_UT): + """Test cases for programmatic unit conversion functions.""" + + def test_programmatic_bytes_to_int(self): + """Test programmatic_bytes_to_int() function.""" + # unknown units are ignored with a warning + self.assertEqual(programmatic_bytes_to_int("100", "foobar"), 100) + self.assertLogged(levelname='WARNING', + msg="Unknown programmatic units string '%s'", + args=('foobar',)) diff --git a/COT/ovf/utilities.py b/COT/ovf/utilities.py new file mode 100644 index 0000000..dc0d32e --- /dev/null +++ b/COT/ovf/utilities.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# +# utilities.py - Module providing utility functions for OVF/OVA handling +# +# February 2017, Glenn F. Matthews +# Copyright (c) 2013-2017 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 providing utility functions for OVF and OVA handling. + +**Functions** + +.. autosummary:: + :nosignatures: + + programmatic_bytes_to_int + int_bytes_to_programmatic_units +""" + +import logging +import re + +logger = logging.getLogger(__name__) + + +def programmatic_bytes_to_int(base_value, programmatic_units): + """Convert a byte value expressed in programmatic units to the raw number. + + Inverse operation of :func:`int_bytes_to_programmatic_units`. + + .. seealso:: + `DMTF DSP0004, Common Information Model (CIM) Infrastructure + Specification 2.5 + `_ + + Args: + base_value (str): Base value string (value of ``ovf:capacity``, etc.) + programmatic_units (str): Programmatic units string (value of + ``ovf:capacityAllocationUnits``, etc.) + + Returns: + int: Number of bytes + + Examples: + :: + + >>> programmatic_bytes_to_int("128", "byte") + 128 + >>> programmatic_bytes_to_int("1", "byte * 2^10") + 1024 + >>> programmatic_bytes_to_int("128", "byte * 2^20") + 134217728 + >>> programmatic_bytes_to_int("512", "MegaBytes") + 536870912 + """ + if not programmatic_units: + return int(base_value) + + # programmatic units like 'byte * 2^30' + match = re.search(r"2\^(\d+)", programmatic_units) + if match: + return int(base_value) << int(match.group(1)) + + # programmatic units like 'MegaBytes' + si_prefixes = ["", "kilo", "mega", "giga", "tera"] + match = re.search("^(.*)bytes$", programmatic_units, re.IGNORECASE) + if match: + shift = si_prefixes.index(match.group(1).lower()) + # Technically the correct answer would be: + # return int(base_value) * (1000 ** shift) + # but instead we'll reflect common usage: + return int(base_value) << (10 * shift) + + if programmatic_units and programmatic_units != 'byte': + logger.warning("Unknown programmatic units string '%s'", + programmatic_units) + + return int(base_value) + + +def int_bytes_to_programmatic_units(byte_value): + """Convert a byte count into OVF-style bytes + multiplier. + + Inverse operation of :func:`programmatic_bytes_to_int` + + Args: + byte_value (int): Number of bytes + + Returns: + tuple: ``(base_value, programmatic_units)`` + + Examples: + :: + + >>> int_bytes_to_programmatic_units(2147483648) + ('2', 'byte * 2^30') + >>> int_bytes_to_programmatic_units(2147483647) + ('2147483647', 'byte') + >>> int_bytes_to_programmatic_units(134217728) + ('128', 'byte * 2^20') + >>> int_bytes_to_programmatic_units(134217729) + ('134217729', 'byte') + """ + shift = 0 + byte_value = int(byte_value) + while byte_value % 1024 == 0: + shift += 10 + byte_value /= 1024 + byte_str = str(int(byte_value)) + if shift == 0: + return (byte_str, "byte") + return (byte_str, "byte * 2^{0}".format(shift)) + + +if __name__ == "__main__": # pragma: no cover + import doctest + doctest.testmod() diff --git a/COT/platforms/__init__.py b/COT/platforms/__init__.py index 6aafdf6..44f05f4 100644 --- a/COT/platforms/__init__.py +++ b/COT/platforms/__init__.py @@ -12,13 +12,13 @@ """Package for identifying guest platforms and handling platform differences. -The :class:`~COT.platforms.generic.GenericPlatform` class describes the API +The :class:`~COT.platforms.platform.Platform` class describes the API and provides a generic implementation that can be overridden by subclasses to provide platform-specific logic. -In general, other modules should not instantiate subclasses directly but should -instead use the :func:`~COT.platforms.platform_from_product_class` API to -derive the appropriate subclass instance. +In general, other modules should not access subclasses directly but should +instead use the :meth:`~COT.platforms.platform.Platform.for_product_string` +API to derive the appropriate subclass object. API --- @@ -26,8 +26,7 @@ .. autosummary:: :nosignatures: - is_known_product_class - platform_from_product_class + ~COT.platforms.platform.Platform Platform modules ---------------- @@ -35,7 +34,7 @@ .. autosummary:: :toctree: - COT.platforms.generic + COT.platforms.platform COT.platforms.cisco_csr1000v COT.platforms.cisco_iosv COT.platforms.cisco_iosxrv @@ -46,7 +45,9 @@ import logging -from .generic import GenericPlatform +# flake8: noqa: F401 + +from .platform import Platform from .cisco_csr1000v import CSR1000V from .cisco_iosv import IOSv from .cisco_iosxrv import IOSXRv, IOSXRvRP, IOSXRvLC @@ -57,72 +58,6 @@ logger = logging.getLogger(__name__) -PRODUCT_PLATFORM_MAP = { - 'com.cisco.csr1000v': CSR1000V, - 'com.cisco.iosv': IOSv, - 'com.cisco.n9k': Nexus9000v, - 'com.cisco.nx-osv': NXOSv, - 'com.cisco.ios-xrv': IOSXRv, - 'com.cisco.ios-xrv.rp': IOSXRvRP, - 'com.cisco.ios-xrv.lc': IOSXRvLC, - 'com.cisco.ios-xrv9000': IOSXRv9000, - # Some early releases of IOS XRv 9000 used the - # incorrect string 'com.cisco.ios-xrv64'. - 'com.cisco.ios-xrv64': IOSXRv9000, -} -"""Mapping of known product class strings to Platform classes.""" - - -def is_known_product_class(product_class): - """Determine if the given product class string is a known one. - - Args: - product_class (str): String such as 'com.cisco.iosv' - - Returns: - bool: Whether product_class is known. - - Examples: - :: - - >>> is_known_product_class("com.cisco.n9k") - True - >>> is_known_product_class("foobar") - False - """ - return product_class in PRODUCT_PLATFORM_MAP - - -def platform_from_product_class(product_class): - """Get the class of Platform corresponding to a product class string. - - Args: - product_class (str): String such as 'com.cisco.iosv' - - Returns: - class: GenericPlatform or a subclass of it - - Examples: - :: - - >>> platform_from_product_class("com.cisco.n9k") - - >>> platform_from_product_class(None) - - >>> platform_from_product_class("frobozz") - - """ - if product_class is None: - return GenericPlatform - if is_known_product_class(product_class): - return PRODUCT_PLATFORM_MAP[product_class] - logger.warning("Unrecognized product class '%s' - known classes " - "are %s. Treating as a generic platform", - product_class, PRODUCT_PLATFORM_MAP.keys()) - return GenericPlatform - - __all__ = ( - 'is_known_product_class', - 'platform_from_product_class', + 'Platform', ) diff --git a/COT/platforms/cisco_csr1000v.py b/COT/platforms/cisco_csr1000v.py index a34b594..25f45c2 100644 --- a/COT/platforms/cisco_csr1000v.py +++ b/COT/platforms/cisco_csr1000v.py @@ -1,5 +1,5 @@ # September 2016, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 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. # @@ -14,16 +14,13 @@ import logging -from COT.platforms.generic import GenericPlatform -from COT.data_validation import ( - ValueUnsupportedError, ValueTooLowError, ValueTooHighError, - validate_int, -) +from COT.platforms.platform import Platform, Hardware +from COT.data_validation import ValueUnsupportedError, ValidRange logger = logging.getLogger(__name__) -class CSR1000V(GenericPlatform): +class CSR1000V(Platform): """Platform-specific logic for Cisco CSR1000V platform.""" PLATFORM_NAME = "Cisco CSR1000V" @@ -33,8 +30,15 @@ class CSR1000V(GenericPlatform): # CSR1000v doesn't 'officially' support E1000, but it mostly works SUPPORTED_NIC_TYPES = ["E1000", "virtio", "VMXNET3"] - @classmethod - def controller_type_for_device(cls, device_type): + HARDWARE_LIMITS = Platform.HARDWARE_LIMITS.copy() + HARDWARE_LIMITS.update({ + Hardware.cpus: ValidRange(1, 8), # but see below + Hardware.memory: ValidRange(2560, 8192), + Hardware.nic_count: ValidRange(3, 26), + Hardware.serial_count: ValidRange(0, 2), + }) + + def controller_type_for_device(self, device_type): """CSR1000V uses SCSI for hard disks and IDE for CD-ROMs. Args: @@ -47,10 +51,10 @@ def controller_type_for_device(cls, device_type): elif device_type == 'cdrom': return 'ide' else: - return super(CSR1000V, cls).controller_type_for_device(device_type) + return super(CSR1000V, self).controller_type_for_device( + device_type) - @classmethod - def guess_nic_name(cls, nic_number): + def guess_nic_name(self, nic_number): """GigabitEthernet1, GigabitEthernet2, etc. .. warning:: @@ -67,8 +71,7 @@ def guess_nic_name(cls, nic_number): """ return "GigabitEthernet" + str(nic_number) - @classmethod - def validate_cpu_count(cls, cpus): + def validate_cpu_count(self, cpus): """CSR1000V supports 1, 2, 4, or 8 CPUs. Args: @@ -80,48 +83,9 @@ def validate_cpu_count(cls, cpus): ValueUnsupportedError: if ``cpus`` is an unsupported value between 1 and 8 """ - validate_int(cpus, 1, 8, "CPUs") + super(CSR1000V, self).validate_cpu_count(cpus) if cpus not in [1, 2, 4, 8]: raise ValueUnsupportedError("CPUs", cpus, [1, 2, 4, 8]) - @classmethod - def validate_memory_amount(cls, mebibytes): - """Minimum 2.5 GiB, max 8 GiB. - - Args: - mebibytes (int): RAM, in MiB. - - Raises: - ValueTooLowError: if ``mebibytes`` is less than 2560 - ValueTooHighError: if ``mebibytes`` is more than 8192 - """ - if mebibytes < 2560: - raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "2.5 GiB") - elif mebibytes > 8192: - raise ValueTooHighError("RAM", str(mebibytes) + " MiB", "8 GiB") - - @classmethod - def validate_nic_count(cls, count): - """CSR1000V requires 3 NICs and supports up to 26. - - Args: - count (int): Number of NICs. - Raises: - ValueTooLowError: if ``count`` is less than 3 - ValueTooHighError: if ``count`` is more than 26 - """ - validate_int(count, 3, 26, "NIC count") - - @classmethod - def validate_serial_count(cls, count): - """CSR1000V supports 0-2 serial ports. - - Args: - count (int): Number of serial ports. - - Raises: - ValueTooLowError: if ``count`` is less than 0 - ValueTooHighError: if ``count`` is more than 2 - """ - validate_int(count, 0, 2, "serial ports") +Platform.PRODUCT_PLATFORM_MAP['com.cisco.csr1000v'] = CSR1000V diff --git a/COT/platforms/cisco_iosv.py b/COT/platforms/cisco_iosv.py index dcd9d10..cc9c2e6 100644 --- a/COT/platforms/cisco_iosv.py +++ b/COT/platforms/cisco_iosv.py @@ -1,5 +1,5 @@ # September 2016, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 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. # @@ -14,15 +14,13 @@ import logging -from COT.platforms.generic import GenericPlatform -from COT.data_validation import ( - ValueTooLowError, ValueTooHighError, validate_int, -) +from COT.platforms.platform import Platform, Hardware +from COT.data_validation import ValidRange logger = logging.getLogger(__name__) -class IOSv(GenericPlatform): +class IOSv(Platform): """Platform-specific logic for Cisco IOSv.""" PLATFORM_NAME = "Cisco IOSv" @@ -33,8 +31,15 @@ class IOSv(GenericPlatform): BOOTSTRAP_DISK_TYPE = 'harddisk' SUPPORTED_NIC_TYPES = ["E1000"] - @classmethod - def guess_nic_name(cls, nic_number): + HARDWARE_LIMITS = Platform.HARDWARE_LIMITS.copy() + HARDWARE_LIMITS.update({ + Hardware.cpus: ValidRange(1, 1), + Hardware.memory: ValidRange(192, 3072), # but see also below + Hardware.nic_count: ValidRange(0, 16), + Hardware.serial_count: ValidRange(1, 2), + }) + + def guess_nic_name(self, nic_number): """GigabitEthernet0/0, GigabitEthernet0/1, etc. Args: @@ -46,21 +51,7 @@ def guess_nic_name(cls, nic_number): """ return "GigabitEthernet0/" + str(nic_number - 1) - @classmethod - def validate_cpu_count(cls, cpus): - """IOSv only supports a single CPU. - - Args: - cpus (int): Number of CPUs. - - Raises: - ValueTooLowError: if ``cpus`` is less than 1 - ValueTooHighError: if ``cpus`` is more than 1 - """ - validate_int(cpus, 1, 1, "CPUs") - - @classmethod - def validate_memory_amount(cls, mebibytes): + def validate_memory_amount(self, mebibytes): """IOSv has minimum 192 MiB (with minimal feature set), max 3 GiB. Args: @@ -70,37 +61,13 @@ def validate_memory_amount(cls, mebibytes): ValueTooLowError: if ``mebibytes`` is less than 192 ValueTooHighError: if ``mebibytes`` is more than 3072 """ - if mebibytes < 192: - raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "192 MiB") - elif mebibytes < 384: + previously_validated = (mebibytes in + self._already_validated[Hardware.memory]) + super(IOSv, self).validate_memory_amount(mebibytes) + if mebibytes < 384 and not previously_validated: # Warn but allow logger.warning("Less than 384MiB of RAM may not be sufficient " "for some IOSv feature sets") - elif mebibytes > 3072: - raise ValueTooHighError("RAM", str(mebibytes) + " MiB", "3 GiB") - @classmethod - def validate_nic_count(cls, count): - """IOSv supports up to 16 NICs. - - Args: - count (int): Number of NICs. - Raises: - ValueTooLowError: if ``count`` is less than 0 - ValueTooHighError: if ``count`` is more than 16 - """ - validate_int(count, 0, 16, "NICs") - - @classmethod - def validate_serial_count(cls, count): - """IOSv requires 1-2 serial ports. - - Args: - count (int): Number of serial ports. - - Raises: - ValueTooLowError: if ``count`` is less than 1 - ValueTooHighError: if ``count`` is more than 2 - """ - validate_int(count, 1, 2, "serial ports") +Platform.PRODUCT_PLATFORM_MAP['com.cisco.iosv'] = IOSv diff --git a/COT/platforms/cisco_iosxrv.py b/COT/platforms/cisco_iosxrv.py index ada4a8b..80031bb 100644 --- a/COT/platforms/cisco_iosxrv.py +++ b/COT/platforms/cisco_iosxrv.py @@ -1,5 +1,5 @@ # September 2016, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 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. # @@ -24,15 +24,13 @@ import logging -from COT.platforms.generic import GenericPlatform -from COT.data_validation import ( - ValueTooLowError, ValueTooHighError, validate_int, -) +from COT.platforms.platform import Platform, Hardware +from COT.data_validation import ValidRange logger = logging.getLogger(__name__) -class IOSXRv(GenericPlatform): +class IOSXRv(Platform): """Platform-specific logic for Cisco IOS XRv platform.""" PLATFORM_NAME = "Cisco IOS XRv" @@ -42,8 +40,15 @@ class IOSXRv(GenericPlatform): LITERAL_CLI_STRING = None SUPPORTED_NIC_TYPES = ["E1000", "virtio"] - @classmethod - def guess_nic_name(cls, nic_number): + HARDWARE_LIMITS = Platform.HARDWARE_LIMITS.copy() + HARDWARE_LIMITS.update({ + Hardware.cpus: ValidRange(1, 8), + Hardware.memory: ValidRange(3072, 8192), + Hardware.nic_count: ValidRange(1, None), + Hardware.serial_count: ValidRange(1, 4), + }) + + def guess_nic_name(self, nic_number): """MgmtEth0/0/CPU0/0, GigabitEthernet0/0/0/0, Gig0/0/0/1, etc. Args: @@ -60,68 +65,18 @@ def guess_nic_name(cls, nic_number): else: return "GigabitEthernet0/0/0/" + str(nic_number - 2) - @classmethod - def validate_cpu_count(cls, cpus): - """IOS XRv supports 1-8 CPUs. - - Args: - cpus (int): Number of CPUs - - Raises: - ValueTooLowError: if ``cpus`` is less than 1 - ValueTooHighError: if ``cpus`` is more than 8 - """ - validate_int(cpus, 1, 8, "CPUs") - - @classmethod - def validate_memory_amount(cls, mebibytes): - """Minimum 3 GiB, max 8 GiB of RAM. - - Args: - mebibytes (int): RAM, in MiB. - - Raises: - ValueTooLowError: if ``mebibytes`` is less than 3072 - ValueTooHighError: if ``mebibytes`` is more than 8192 - """ - if mebibytes < 3072: - raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "3 GiB") - elif mebibytes > 8192: - raise ValueTooHighError("RAM", str(mebibytes) + " MiB", " 8GiB") - - @classmethod - def validate_nic_count(cls, count): - """IOS XRv requires at least one NIC. - - Args: - count (int): Number of NICs. - - Raises: - ValueTooLowError: if ``count`` is less than 1 - """ - validate_int(count, 1, None, "NIC count") - - @classmethod - def validate_serial_count(cls, count): - """IOS XRv supports 1-4 serial ports. - - Args: - count (int): Number of serial ports. - - Raises: - ValueTooLowError: if ``count`` is less than 1 - ValueTooHighError: if ``count`` is more than 4 - """ - validate_int(count, 1, 4, "serial ports") - class IOSXRvRP(IOSXRv): """Platform-specific logic for Cisco IOS XRv HA-capable RP.""" PLATFORM_NAME = "Cisco IOS XRv route processor card" - @classmethod - def guess_nic_name(cls, nic_number): + HARDWARE_LIMITS = IOSXRv.HARDWARE_LIMITS.copy() + HARDWARE_LIMITS.update({ + Hardware.nic_count: ValidRange(1, 2), + }) + + def guess_nic_name(self, nic_number): """Fabric and management only. Args: @@ -135,19 +90,6 @@ def guess_nic_name(cls, nic_number): else: return "MgmtEth0/{SLOT}/CPU0/" + str(nic_number - 2) - @classmethod - def validate_nic_count(cls, count): - """Fabric plus an optional management NIC. - - Args: - count (int): Number of NICs. - - Raises: - ValueTooLowError: if ``count`` is less than 1 - ValueTooHighError: if ``count`` is more than 2 - """ - validate_int(count, 1, 2, "NIC count") - class IOSXRvLC(IOSXRv): """Platform-specific logic for Cisco IOS XRv line card.""" @@ -158,8 +100,12 @@ class IOSXRvLC(IOSXRv): CONFIG_TEXT_FILE = None SECONDARY_CONFIG_TEXT_FILE = None - @classmethod - def guess_nic_name(cls, nic_number): + HARDWARE_LIMITS = IOSXRv.HARDWARE_LIMITS.copy() + HARDWARE_LIMITS.update({ + Hardware.serial_count: ValidRange(0, 4), + }) + + def guess_nic_name(self, nic_number): """Fabric interface plus slot-appropriate GigabitEthernet interfaces. Args: @@ -177,15 +123,7 @@ def guess_nic_name(cls, nic_number): else: return "GigabitEthernet0/{SLOT}/0/" + str(nic_number - 2) - @classmethod - def validate_serial_count(cls, count): - """No serial ports are needed but up to 4 can be used for debugging. - Args: - count (int): Number of serial ports. - - Raises: - ValueTooLowError: if ``count`` is less than 0 - ValueTooHighError: if ``count`` is more than 4 - """ - validate_int(count, 0, 4, "serial ports") +Platform.PRODUCT_PLATFORM_MAP['com.cisco.ios-xrv'] = IOSXRv +Platform.PRODUCT_PLATFORM_MAP['com.cisco.ios-xrv.rp'] = IOSXRvRP +Platform.PRODUCT_PLATFORM_MAP['com.cisco.ios-xrv.lc'] = IOSXRvLC diff --git a/COT/platforms/cisco_iosxrv_9000.py b/COT/platforms/cisco_iosxrv_9000.py index b910c5c..a233288 100644 --- a/COT/platforms/cisco_iosxrv_9000.py +++ b/COT/platforms/cisco_iosxrv_9000.py @@ -14,8 +14,9 @@ import logging +from COT.platforms.platform import Platform, Hardware from COT.platforms.cisco_iosxrv import IOSXRv -from COT.data_validation import ValueTooLowError, validate_int +from COT.data_validation import ValidRange logger = logging.getLogger(__name__) @@ -26,8 +27,14 @@ class IOSXRv9000(IOSXRv): PLATFORM_NAME = "Cisco IOS XRv 9000" SUPPORTED_NIC_TYPES = ["E1000", "virtio", "VMXNET3"] - @classmethod - def guess_nic_name(cls, nic_number): + HARDWARE_LIMITS = IOSXRv.HARDWARE_LIMITS.copy() + HARDWARE_LIMITS.update({ + Hardware.cpus: ValidRange(1, 32), + Hardware.memory: ValidRange(8192, None), + Hardware.nic_count: ValidRange(4, None), + }) + + def guess_nic_name(self, nic_number): """MgmtEth0/0/CPU0/0, CtrlEth, DevEth, GigabitEthernet0/0/0/0, etc. Args: @@ -50,40 +57,7 @@ def guess_nic_name(cls, nic_number): else: return "GigabitEthernet0/0/0/" + str(nic_number - 4) - @classmethod - def validate_cpu_count(cls, cpus): - """Minimum 1, maximum 32 CPUs. - - Args: - cpus (int): Number of CPUs - - Raises: - ValueTooLowError: if ``cpus`` is less than 1 - ValueTooHighError: if ``cpus`` is more than 32 - """ - validate_int(cpus, 1, 32, "CPUs") - - @classmethod - def validate_memory_amount(cls, mebibytes): - """Minimum 8 GiB, no known maximum (128GiB+ is permitted). - - Args: - mebibytes (int): RAM, in MiB. - - Raises: - ValueTooLowError: if ``mebibytes`` is less than 8192 - """ - if mebibytes < 8192: - raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "8 GiB") - - @classmethod - def validate_nic_count(cls, count): - """IOS XRv 9000 requires at least 4 NICs. - Args: - count (int): Number of NICs. - - Raises: - ValueTooLowError: if ``count`` is less than 4 - """ - validate_int(count, 4, None, "NIC count") +Platform.PRODUCT_PLATFORM_MAP['com.cisco.ios-xrv9000'] = IOSXRv9000 +# Some early releases of this platform instead used: +Platform.PRODUCT_PLATFORM_MAP['com.cisco.ios-xrv64'] = IOSXRv9000 diff --git a/COT/platforms/cisco_nexus_9000v.py b/COT/platforms/cisco_nexus_9000v.py index 2ca5991..0cde958 100644 --- a/COT/platforms/cisco_nexus_9000v.py +++ b/COT/platforms/cisco_nexus_9000v.py @@ -14,15 +14,13 @@ import logging -from COT.platforms.generic import GenericPlatform -from COT.data_validation import ( - ValueTooLowError, validate_int, -) +from COT.platforms.platform import Platform, Hardware +from COT.data_validation import ValidRange logger = logging.getLogger(__name__) -class Nexus9000v(GenericPlatform): +class Nexus9000v(Platform): """Platform-specific logic for Cisco Nexus 9000v.""" PLATFORM_NAME = "Cisco Nexus 9000v" @@ -31,8 +29,15 @@ class Nexus9000v(GenericPlatform): LITERAL_CLI_STRING = None SUPPORTED_NIC_TYPES = ["E1000", "VMXNET3"] - @classmethod - def guess_nic_name(cls, nic_number): + HARDWARE_LIMITS = Platform.HARDWARE_LIMITS.copy() + HARDWARE_LIMITS.update({ + Hardware.cpus: ValidRange(1, 4), + Hardware.memory: ValidRange(8192, None), + Hardware.nic_count: ValidRange(1, 65), + Hardware.serial_count: ValidRange(1, 1), + }) + + def guess_nic_name(self, nic_number): """The Nexus 9000v has a management NIC and some number of data NICs. Args: @@ -49,54 +54,5 @@ def guess_nic_name(cls, nic_number): else: return "Ethernet1/{0}".format(nic_number - 1) - @classmethod - def validate_cpu_count(cls, cpus): - """The Nexus 9000v requires 1-4 vCPUs. - - Args: - cpus (int): Number of vCPUs - - Raises: - ValueTooLowError: if ``cpus`` is less than 1 - ValueTooHighError: if ``cpus`` is more than 4 - """ - validate_int(cpus, 1, 4, "CPUs") - - @classmethod - def validate_memory_amount(cls, mebibytes): - """The Nexus 9000v requires at least 8 GiB of RAM. - - Args: - mebibytes (int): RAM, in MiB. - - Raises: - ValueTooLowError: if ``mebibytes`` is less than 8192 - """ - if mebibytes < 8192: - raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "8 GiB") - @classmethod - def validate_nic_count(cls, count): - """The Nexus 9000v requires at least 1 and supports at most 65 NICs. - - Args: - count (int): Number of NICs. - - Raises: - ValueTooLowError: if ``count`` is less than 1 - ValueTooHighError: if ``count`` is more than 65 - """ - validate_int(count, 1, 65, "NICs") - - @classmethod - def validate_serial_count(cls, count): - """The Nexus 9000v requires exactly 1 serial port. - - Args: - count (int): Number of serial ports. - - Raises: - ValueTooLowError: if ``count`` is less than 1 - ValueTooHighError: if ``count`` is more than 1 - """ - validate_int(count, 1, 1, "serial ports") +Platform.PRODUCT_PLATFORM_MAP['com.cisco.n9k'] = Nexus9000v diff --git a/COT/platforms/cisco_nxosv.py b/COT/platforms/cisco_nxosv.py index 0c645a9..d965e5a 100644 --- a/COT/platforms/cisco_nxosv.py +++ b/COT/platforms/cisco_nxosv.py @@ -1,5 +1,5 @@ # September 2016, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 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. # @@ -14,15 +14,13 @@ import logging -from COT.platforms.generic import GenericPlatform -from COT.data_validation import ( - ValueTooLowError, ValueTooHighError, validate_int, -) +from COT.platforms.platform import Platform, Hardware +from COT.data_validation import ValidRange logger = logging.getLogger(__name__) -class NXOSv(GenericPlatform): +class NXOSv(Platform): """Platform-specific logic for Cisco NX-OSv (Titanium).""" PLATFORM_NAME = "Cisco NX-OSv" @@ -31,8 +29,14 @@ class NXOSv(GenericPlatform): LITERAL_CLI_STRING = None SUPPORTED_NIC_TYPES = ["E1000", "virtio"] - @classmethod - def guess_nic_name(cls, nic_number): + HARDWARE_LIMITS = Platform.HARDWARE_LIMITS.copy() + HARDWARE_LIMITS.update({ + Hardware.cpus: ValidRange(1, 8), + Hardware.memory: ValidRange(2048, 8192), + Hardware.serial_count: ValidRange(1, 2), + }) + + def guess_nic_name(self, nic_number): """NX-OSv names its NICs a bit interestingly... Args: @@ -54,44 +58,5 @@ def guess_nic_name(cls, nic_number): return ("Ethernet{0}/{1}".format((nic_number - 2) // 48 + 2, (nic_number - 2) % 48 + 1)) - @classmethod - def validate_cpu_count(cls, cpus): - """NX-OSv requires 1-8 CPUs. - - Args: - cpus (int): Number of CPUs - - Raises: - ValueTooLowError: if ``cpus`` is less than 1 - ValueTooHighError: if ``cpus`` is more than 8 - """ - validate_int(cpus, 1, 8, "CPUs") - - @classmethod - def validate_memory_amount(cls, mebibytes): - """NX-OSv requires 2-8 GiB of RAM. - - Args: - mebibytes (int): RAM, in MiB. - - Raises: - ValueTooLowError: if ``mebibytes`` is less than 2048 - ValueTooHighError: if ``mebibytes`` is more than 8192 - """ - if mebibytes < 2048: - raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "2 GiB") - elif mebibytes > 8192: - raise ValueTooHighError("RAM", str(mebibytes) + " MiB", "8 GiB") - - @classmethod - def validate_serial_count(cls, count): - """NX-OSv requires 1-2 serial ports. - Args: - count (int): Number of serial ports. - - Raises: - ValueTooLowError: if ``count`` is less than 1 - ValueTooHighError: if ``count`` is more than 2 - """ - validate_int(count, 1, 2, "serial ports") +Platform.PRODUCT_PLATFORM_MAP['com.cisco.nx-osv'] = NXOSv diff --git a/COT/platforms/generic.py b/COT/platforms/generic.py deleted file mode 100644 index 393c2cb..0000000 --- a/COT/platforms/generic.py +++ /dev/null @@ -1,163 +0,0 @@ -# September 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. - -"""API and generic implementation of platform-specific logic.""" - -from COT.data_validation import ( - validate_int, ValueUnsupportedError, ValueTooLowError, NIC_TYPES, -) - - -class GenericPlatform(object): - """Generic class for operations that depend on guest platform. - - To be used whenever the guest is unrecognized or does not need - special handling. - """ - - PLATFORM_NAME = "(unrecognized platform, generic)" - - # Default file name for text configuration file to embed - CONFIG_TEXT_FILE = 'config.txt' - # Most platforms do not support a secondary configuration file - SECONDARY_CONFIG_TEXT_FILE = None - # Most platforms do not support configuration properties in the environment - LITERAL_CLI_STRING = 'config' - - # Most platforms use a CD-ROM for bootstrap configuration - BOOTSTRAP_DISK_TYPE = 'cdrom' - - SUPPORTED_NIC_TYPES = NIC_TYPES - - # Some of these methods are semi-abstract, so: - # pylint: disable=unused-argument - - @classmethod - def controller_type_for_device(cls, device_type): - """Get the default controller type for the given device type. - - Args: - device_type (str): 'harddisk', 'cdrom', etc. - - Returns: - str: 'ide' unless overridden by subclass. - """ - # For most platforms IDE is the correct default. - return 'ide' - - @classmethod - def guess_nic_name(cls, nic_number): - """Guess the name of the Nth NIC for this platform. - - .. note:: This method counts from 1, not from 0! - - Args: - nic_number (int): Nth NIC to name. - - Returns: - str: "Ethernet1", "Ethernet2", etc. unless overridden by subclass. - """ - return "Ethernet" + str(nic_number) - - @classmethod - def validate_cpu_count(cls, cpus): - """Throw an error if the number of CPUs is not a supported value. - - Args: - cpus (int): Number of CPUs - - Raises: - ValueTooLowError: if ``cpus`` is less than the minimum required - by this platform - ValueTooHighError: if ``cpus`` exceeds the maximum supported - by this platform - """ - validate_int(cpus, 1, None, "CPUs") - - @classmethod - def validate_memory_amount(cls, mebibytes): - """Throw an error if the amount of RAM is not supported. - - Args: - mebibytes (int): RAM, in MiB. - - Raises: - ValueTooLowError: if ``mebibytes`` is less than the minimum - required by this platform - ValueTooHighError: if ``mebibytes`` is more than the maximum - supported by this platform - """ - if mebibytes < 1: - raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "1 MiB") - - @classmethod - def validate_nic_count(cls, count): - """Throw an error if the number of NICs is not supported. - - Args: - count (int): Number of NICs. - - Raises: - ValueTooLowError: if ``count`` is less than the minimum - required by this platform - ValueTooHighError: if ``count`` is more than the maximum - supported by this platform - """ - validate_int(count, 0, None, "NIC count") - - @classmethod - def validate_nic_type(cls, type_string): - """Throw an error if the NIC type string is not supported. - - .. seealso:: - - :func:`COT.data_validation.canonicalize_nic_subtype` - - :data:`COT.data_validation.NIC_TYPES` - - Args: - type_string (str): See :data:`COT.data_validation.NIC_TYPES` - - Raises: - ValueUnsupportedError: if ``type_string`` is not in - :const:`SUPPORTED_NIC_TYPES` - """ - if type_string not in cls.SUPPORTED_NIC_TYPES: - raise ValueUnsupportedError("NIC type", type_string, - cls.SUPPORTED_NIC_TYPES) - - @classmethod - def validate_nic_types(cls, type_list): - """Throw an error if any NIC type string in the list is unsupported. - - Args: - type_list (list): See :data:`COT.data_validation.NIC_TYPES` - - Raises: - ValueUnsupportedError: if any value in ``type_list`` is not in - :const:`SUPPORTED_NIC_TYPES` - """ - for type_string in type_list: - cls.validate_nic_type(type_string) - - @classmethod - def validate_serial_count(cls, count): - """Throw an error if the number of serial ports is not supported. - - Args: - count (int): Number of serial ports. - - Raises: - ValueTooLowError: if ``count`` is less than the minimum - required by this platform - ValueTooHighError: if ``count`` is more than the maximum - supported by this platform - """ - validate_int(count, 0, None, "serial port count") diff --git a/COT/platforms/platform.py b/COT/platforms/platform.py new file mode 100644 index 0000000..3cdf8b6 --- /dev/null +++ b/COT/platforms/platform.py @@ -0,0 +1,262 @@ +# September 2016, Glenn F. Matthews +# Copyright (c) 2013-2017 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. + +"""API and generic implementation of platform-specific logic.""" + +import logging +from collections import defaultdict +from enum import Enum + +from COT.data_validation import ( + validate_int, ValueUnsupportedError, NIC_TYPES, ValidRange, +) + +logger = logging.getLogger(__name__) + + +Hardware = Enum('Hardware', 'cpus memory nic_count serial_count') +"""Enumeration of hardware types with integer values.""" + + +class Platform(object): + """Generic class for operations that depend on guest platform. + + To be used whenever the guest is unrecognized or does not need + special handling. + """ + + PLATFORM_NAME = "(unrecognized platform, generic)" + """String used as a descriptive label for this class of Platform.""" + + CONFIG_TEXT_FILE = 'config.txt' + """When embedding a primary configuration text file, use this filename. + + .. seealso:: + :attr:`COT.inject_config.COTInjectConfig.config_file` + """ + + SECONDARY_CONFIG_TEXT_FILE = None + """When embedding a secondary configuration text file, use this filename. + + Most platforms do not support a secondary configuration file. + + .. seealso:: + :attr:`COT.inject_config.COTInjectConfig.secondary_config_file` + """ + + LITERAL_CLI_STRING = 'config' + """Key prefix for converting text config to OVF environment properties. + + Most platforms do not support configuration properties in the environment, + and so should define this attribute to ``None``. + + .. seealso:: + :meth:`~COT.ovf.ovf.OVF.config_file_to_properties` + """ + + BOOTSTRAP_DISK_TYPE = 'cdrom' + """Type of disk (cdrom/harddisk) to use for bootstrap configuration. + + Most platforms use a CD-ROM for this purpose. + """ + + SUPPORTED_NIC_TYPES = NIC_TYPES + """List of NIC device types supported by this platform.""" + + PRODUCT_PLATFORM_MAP = {} + """Mapping of product strings to product classes.""" + + HARDWARE_LIMITS = { + Hardware.cpus: ValidRange(1, None), + Hardware.memory: ValidRange(1, None), + Hardware.nic_count: ValidRange(0, None), + Hardware.serial_count: ValidRange(0, None), + } + """Range of valid values for various hardware properties.""" + + @classmethod + def for_product_string(cls, product_string): + """Get the class of Platform corresponding to a product string. + + Args: + product_string (str): String such as 'com.cisco.iosxrv' + + Returns: + Platform: Instance of Platform or the appropriate subclass. + + Examples: + :: + + >>> Platform.for_product_string("com.cisco.n9k") + + >>> Platform.for_product_string(None) + + >>> Platform.for_product_string("frobozz") + + """ + if product_string is None: + return Platform() + if product_string in cls.PRODUCT_PLATFORM_MAP: + return cls.PRODUCT_PLATFORM_MAP[product_string]() + logger.warning("Unrecognized product class '%s' - known classes " + "are %s. Treating as a generic platform.", + product_string, cls.PRODUCT_PLATFORM_MAP.keys()) + return Platform() + + def __init__(self): + """Create an instance of this class.""" + self._already_validated = defaultdict(dict) + """Cache of values already validated. + + :: + + _already_validated[Hardware.cpus][value] = True + + Used to avoid raising the same ValueError over and over from various + points in the code. + """ + + def __str__(self): + """String representation - same as :attr:`PLATFORM_NAME`.""" + return self.__class__.PLATFORM_NAME + + # Some of these methods are semi-abstract, so: + # pylint: disable=unused-argument, no-self-use + + def controller_type_for_device(self, device_type): + """Get the default controller type for the given device type. + + Args: + device_type (str): 'harddisk', 'cdrom', etc. + + Returns: + str: 'ide' unless overridden by subclass. + """ + # For most platforms IDE is the correct default. + return 'ide' + + def guess_nic_name(self, nic_number): + """Guess the name of the Nth NIC for this platform. + + .. note:: This method counts from 1, not from 0! + + Args: + nic_number (int): Nth NIC to name. + + Returns: + str: "Ethernet1", "Ethernet2", etc. unless overridden by subclass. + """ + return "Ethernet" + str(nic_number) + + def validate_cpu_count(self, cpus): + """Throw an error if the number of CPUs is not a supported value. + + Args: + cpus (int): Number of CPUs + + Raises: + ValueTooLowError: if ``cpus`` is less than the minimum required + by this platform + ValueTooHighError: if ``cpus`` exceeds the maximum supported + by this platform + """ + if cpus not in self._already_validated[Hardware.cpus]: + self._already_validated[Hardware.cpus][cpus] = True + validate_int(cpus, *self.HARDWARE_LIMITS[Hardware.cpus], + label="CPUs for platform {0}".format(self)) + + def validate_memory_amount(self, mebibytes): + """Throw an error if the amount of RAM is not supported. + + Args: + mebibytes (int): RAM, in MiB. + + Raises: + ValueTooLowError: if ``mebibytes`` is less than the minimum + required by this platform + ValueTooHighError: if ``mebibytes`` is more than the maximum + supported by this platform + """ + if mebibytes not in self._already_validated[Hardware.memory]: + self._already_validated[Hardware.memory][mebibytes] = True + validate_int(mebibytes, *self.HARDWARE_LIMITS[Hardware.memory], + label="MiB of RAM for platform {0}".format(self)) + + def validate_nic_count(self, count): + """Throw an error if the number of NICs is not supported. + + Args: + count (int): Number of NICs. + + Raises: + ValueTooLowError: if ``count`` is less than the minimum + required by this platform + ValueTooHighError: if ``count`` is more than the maximum + supported by this platform + """ + if count not in self._already_validated[Hardware.nic_count]: + self._already_validated[Hardware.nic_count][count] = True + validate_int(count, *self.HARDWARE_LIMITS[Hardware.nic_count], + label="NIC count for platform {0}".format(self)) + + def validate_nic_type(self, type_string): + """Throw an error if the NIC type string is not supported. + + .. seealso:: + - :func:`COT.data_validation.canonicalize_nic_subtype` + - :data:`COT.data_validation.NIC_TYPES` + + Args: + type_string (str): See :data:`COT.data_validation.NIC_TYPES` + + Raises: + ValueUnsupportedError: if ``type_string`` is not in + :const:`SUPPORTED_NIC_TYPES` + """ + if type_string not in self.SUPPORTED_NIC_TYPES: + raise ValueUnsupportedError( + "NIC type for {0}".format(self), + type_string, self.SUPPORTED_NIC_TYPES) + + def validate_nic_types(self, type_list): + """Throw an error if any NIC type string in the list is unsupported. + + Args: + type_list (list): See :data:`COT.data_validation.NIC_TYPES` + + Raises: + ValueUnsupportedError: if any value in ``type_list`` is not in + :const:`SUPPORTED_NIC_TYPES` + """ + for type_string in type_list: + self.validate_nic_type(type_string) + + def validate_serial_count(self, count): + """Throw an error if the number of serial ports is not supported. + + Args: + count (int): Number of serial ports. + + Raises: + ValueTooLowError: if ``count`` is less than the minimum + required by this platform + ValueTooHighError: if ``count`` is more than the maximum + supported by this platform + """ + if count not in self._already_validated[Hardware.serial_count]: + self._already_validated[Hardware.serial_count][count] = True + validate_int(count, *self.HARDWARE_LIMITS[Hardware.serial_count], + label="serial port count for platform {0}" + .format(self)) + + +Platform.PRODUCT_PLATFORM_MAP[None] = Platform diff --git a/COT/platforms/tests/__init__.py b/COT/platforms/tests/__init__.py index 409ece1..5a1e187 100644 --- a/COT/platforms/tests/__init__.py +++ b/COT/platforms/tests/__init__.py @@ -1,5 +1,5 @@ # September 2016, Glenn F. Matthews -# Copyright (c) 2016 the COT project developers. +# Copyright (c) 2016-2017 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. # @@ -11,3 +11,86 @@ # distributed except according to the terms contained in the LICENSE.txt file. """Unit test cases for the COT.platforms package and its submodules.""" + +import unittest +import logging +# Make sure there's always a "no-op" logging handler. +try: + from logging import NullHandler +except ImportError: + # Python 2.6 + class NullHandler(logging.Handler): + """No-op logging handler.""" + + def emit(self, record): + """Do nothing. + + Args: + record (object): Ignored. + """ + pass + + +from COT.data_validation import ValueTooLowError +from COT.platforms import Platform + +logging.getLogger('COT').addHandler(NullHandler()) + + +class PlatformTests(object): + """Wrapper to "hide" the below abstract class from unittest module.""" + + class PlatformTest(unittest.TestCase): + """Abstract base class for testing of Platform subclasses.""" + + cls = None + """The Platform class or subclass being tested by this class.""" + product_string = "" + """The product string for use with Platform.for_product_class().""" + + def setUp(self): + """Test case setup method.""" + super(PlatformTests.PlatformTest, self).setUp() + self.assertNotEqual(self.cls, None) + self.ins = self.cls() # pylint:disable=not-callable + + def test_controller_type_for_device(self): + """Test platform-specific logic for device controllers.""" + self.assertEqual(self.ins.controller_type_for_device('harddisk'), + 'ide') + self.assertEqual(self.ins.controller_type_for_device('cdrom'), + 'ide') + + def test_nic_name(self): + """Test NIC name construction.""" + self.assertEqual(self.ins.guess_nic_name(1), "Ethernet1") + self.assertEqual(self.ins.guess_nic_name(100), "Ethernet100") + + def test_cpu_count(self): + """Test CPU count limits.""" + self.assertRaises(ValueTooLowError, self.ins.validate_cpu_count, 0) + self.ins.validate_cpu_count(1) + + def test_memory_amount(self): + """Test RAM allocation limits.""" + self.assertRaises(ValueTooLowError, + self.ins.validate_memory_amount, 0) + self.ins.validate_memory_amount(1) + + def test_nic_count(self): + """Test NIC range limits.""" + self.assertRaises(ValueTooLowError, + self.ins.validate_nic_count, -1) + self.ins.validate_nic_count(0) + + def test_serial_count(self): + """Test serial port range limits.""" + self.assertRaises(ValueTooLowError, + self.ins.validate_serial_count, -1) + self.ins.validate_serial_count(0) + + def test_for_product_string(self): + """Test Platform.for_product_string lookup of this class.""" + self.assertEqual( + Platform.for_product_string(self.product_string).__class__, + self.cls) diff --git a/COT/platforms/tests/test_cisco_csr1000v.py b/COT/platforms/tests/test_cisco_csr1000v.py index 516a88f..8c82a7c 100644 --- a/COT/platforms/tests/test_cisco_csr1000v.py +++ b/COT/platforms/tests/test_cisco_csr1000v.py @@ -1,7 +1,7 @@ # test_cisco_csr1000v.py - Unit test cases for Cisco CSR1000V platform # # October 2016, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 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. # @@ -14,85 +14,86 @@ """Unit test cases for CSR1000V platform.""" -import unittest from COT.platforms.cisco_csr1000v import CSR1000V from COT.data_validation import ( ValueUnsupportedError, ValueTooLowError, ValueTooHighError ) +from COT.platforms.tests import PlatformTests -class TestCSR1000V(unittest.TestCase): +class TestCSR1000V(PlatformTests.PlatformTest): """Test cases for Cisco CSR 1000V platform handling.""" cls = CSR1000V + product_string = "com.cisco.csr1000v" def test_controller_type_for_device(self): """Test platform-specific logic for device controllers.""" - self.assertEqual(self.cls.controller_type_for_device('harddisk'), + self.assertEqual(self.ins.controller_type_for_device('harddisk'), 'scsi') - self.assertEqual(self.cls.controller_type_for_device('cdrom'), + self.assertEqual(self.ins.controller_type_for_device('cdrom'), 'ide') # fallthrough to parent class - self.assertEqual(self.cls.controller_type_for_device('dvd'), + self.assertEqual(self.ins.controller_type_for_device('dvd'), 'ide') def test_nic_name(self): """Test NIC name construction.""" - self.assertEqual(self.cls.guess_nic_name(1), + self.assertEqual(self.ins.guess_nic_name(1), "GigabitEthernet1") - self.assertEqual(self.cls.guess_nic_name(2), + self.assertEqual(self.ins.guess_nic_name(2), "GigabitEthernet2") - self.assertEqual(self.cls.guess_nic_name(3), + self.assertEqual(self.ins.guess_nic_name(3), "GigabitEthernet3") - self.assertEqual(self.cls.guess_nic_name(4), + self.assertEqual(self.ins.guess_nic_name(4), "GigabitEthernet4") def test_cpu_count(self): """Test CPU count limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_cpu_count, 0) - self.cls.validate_cpu_count(1) - self.cls.validate_cpu_count(2) + self.assertRaises(ValueTooLowError, self.ins.validate_cpu_count, 0) + self.ins.validate_cpu_count(1) + self.ins.validate_cpu_count(2) self.assertRaises(ValueUnsupportedError, - self.cls.validate_cpu_count, 3) - self.cls.validate_cpu_count(4) + self.ins.validate_cpu_count, 3) + self.ins.validate_cpu_count(4) self.assertRaises(ValueUnsupportedError, - self.cls.validate_cpu_count, 5) + self.ins.validate_cpu_count, 5) self.assertRaises(ValueUnsupportedError, - self.cls.validate_cpu_count, 6) + self.ins.validate_cpu_count, 6) self.assertRaises(ValueUnsupportedError, - self.cls.validate_cpu_count, 7) - self.cls.validate_cpu_count(8) - self.assertRaises(ValueTooHighError, self.cls.validate_cpu_count, 9) + self.ins.validate_cpu_count, 7) + self.ins.validate_cpu_count(8) + self.assertRaises(ValueTooHighError, self.ins.validate_cpu_count, 9) def test_memory_amount(self): """Test RAM allocation limits.""" self.assertRaises(ValueTooLowError, - self.cls.validate_memory_amount, 2559) - self.cls.validate_memory_amount(2560) - self.cls.validate_memory_amount(8192) + self.ins.validate_memory_amount, 2559) + self.ins.validate_memory_amount(2560) + self.ins.validate_memory_amount(8192) self.assertRaises(ValueTooHighError, - self.cls.validate_memory_amount, 8193) + self.ins.validate_memory_amount, 8193) def test_nic_count(self): """Test NIC range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_nic_count, 2) - self.cls.validate_nic_count(3) - self.cls.validate_nic_count(26) - self.assertRaises(ValueTooHighError, self.cls.validate_nic_count, 27) + self.assertRaises(ValueTooLowError, self.ins.validate_nic_count, 2) + self.ins.validate_nic_count(3) + self.ins.validate_nic_count(26) + self.assertRaises(ValueTooHighError, self.ins.validate_nic_count, 27) def test_nic_type(self): """Test NIC valid and invalid types.""" self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "E1000e") - self.cls.validate_nic_type("E1000") + self.ins.validate_nic_type, "E1000e") + self.ins.validate_nic_type("E1000") self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "PCNet32") - self.cls.validate_nic_type("virtio") - self.cls.validate_nic_type("VMXNET3") + self.ins.validate_nic_type, "PCNet32") + self.ins.validate_nic_type("virtio") + self.ins.validate_nic_type("VMXNET3") def test_serial_count(self): """Test serial port range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_serial_count, -1) - self.cls.validate_serial_count(0) - self.cls.validate_serial_count(2) - self.assertRaises(ValueTooHighError, self.cls.validate_serial_count, 3) + self.assertRaises(ValueTooLowError, self.ins.validate_serial_count, -1) + self.ins.validate_serial_count(0) + self.ins.validate_serial_count(2) + self.assertRaises(ValueTooHighError, self.ins.validate_serial_count, 3) diff --git a/COT/platforms/tests/test_cisco_iosv.py b/COT/platforms/tests/test_cisco_iosv.py index 8c44746..14b274f 100644 --- a/COT/platforms/tests/test_cisco_iosv.py +++ b/COT/platforms/tests/test_cisco_iosv.py @@ -1,7 +1,7 @@ # test_cisco_iosv.py - Unit test cases for Cisco IOSv platform # # October 2016, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 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. # @@ -14,86 +14,69 @@ """Unit test cases for IOSv platform.""" -import unittest -import logging -# Make sure there's always a "no-op" logging handler. -try: - from logging import NullHandler -except ImportError: - class NullHandler(logging.Handler): - """No-op logging handler.""" - - def emit(self, record): - """Do nothing. - - Args: - record (object): Ignored. - """ - pass - from COT.platforms.cisco_iosv import IOSv from COT.data_validation import ( ValueUnsupportedError, ValueTooLowError, ValueTooHighError ) - -logging.getLogger('COT').addHandler(NullHandler()) +from COT.platforms.tests import PlatformTests # pylint: disable=missing-type-doc,missing-param-doc -class TestIOSv(unittest.TestCase): +class TestIOSv(PlatformTests.PlatformTest): """Test cases for Cisco IOSv platform handling.""" cls = IOSv + product_string = "com.cisco.iosv" def test_nic_name(self): """Test NIC name construction.""" - self.assertEqual(self.cls.guess_nic_name(1), + self.assertEqual(self.ins.guess_nic_name(1), "GigabitEthernet0/0") - self.assertEqual(self.cls.guess_nic_name(2), + self.assertEqual(self.ins.guess_nic_name(2), "GigabitEthernet0/1") - self.assertEqual(self.cls.guess_nic_name(3), + self.assertEqual(self.ins.guess_nic_name(3), "GigabitEthernet0/2") - self.assertEqual(self.cls.guess_nic_name(4), + self.assertEqual(self.ins.guess_nic_name(4), "GigabitEthernet0/3") def test_cpu_count(self): """Test CPU count limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_cpu_count, 0) - self.cls.validate_cpu_count(1) - self.assertRaises(ValueTooHighError, self.cls.validate_cpu_count, 2) + self.assertRaises(ValueTooLowError, self.ins.validate_cpu_count, 0) + self.ins.validate_cpu_count(1) + self.assertRaises(ValueTooHighError, self.ins.validate_cpu_count, 2) def test_memory_amount(self): """Test RAM allocation limits.""" self.assertRaises(ValueTooLowError, - self.cls.validate_memory_amount, 191) - self.cls.validate_memory_amount(192) - self.cls.validate_memory_amount(3072) + self.ins.validate_memory_amount, 191) + self.ins.validate_memory_amount(192) + self.ins.validate_memory_amount(3072) self.assertRaises(ValueTooHighError, - self.cls.validate_memory_amount, 3073) + self.ins.validate_memory_amount, 3073) def test_nic_count(self): """Test NIC range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_nic_count, -1) - self.cls.validate_nic_count(0) - self.cls.validate_nic_count(16) - self.assertRaises(ValueTooHighError, self.cls.validate_nic_count, 17) + self.assertRaises(ValueTooLowError, self.ins.validate_nic_count, -1) + self.ins.validate_nic_count(0) + self.ins.validate_nic_count(16) + self.assertRaises(ValueTooHighError, self.ins.validate_nic_count, 17) def test_nic_type(self): """Test NIC valid and invalid types.""" self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "E1000e") - self.cls.validate_nic_type("E1000") + self.ins.validate_nic_type, "E1000e") + self.ins.validate_nic_type("E1000") self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "PCNet32") + self.ins.validate_nic_type, "PCNet32") self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "virtio") + self.ins.validate_nic_type, "virtio") self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "VMXNET3") + self.ins.validate_nic_type, "VMXNET3") def test_serial_count(self): """Test serial port range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_serial_count, 0) - self.cls.validate_serial_count(1) - self.cls.validate_serial_count(2) - self.assertRaises(ValueTooHighError, self.cls.validate_serial_count, 3) + self.assertRaises(ValueTooLowError, self.ins.validate_serial_count, 0) + self.ins.validate_serial_count(1) + self.ins.validate_serial_count(2) + self.assertRaises(ValueTooHighError, self.ins.validate_serial_count, 3) diff --git a/COT/platforms/tests/test_cisco_iosxrv.py b/COT/platforms/tests/test_cisco_iosxrv.py index bc04559..07e01be 100644 --- a/COT/platforms/tests/test_cisco_iosxrv.py +++ b/COT/platforms/tests/test_cisco_iosxrv.py @@ -1,7 +1,7 @@ # test_cisco_iosxrv.py - Unit test cases for Cisco IOS XRv platform handling # # October 2016, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 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. # @@ -14,75 +14,77 @@ """Unit test cases for IOSXRv class and its subclasses.""" -import unittest from COT.platforms.cisco_iosxrv import IOSXRv, IOSXRvRP, IOSXRvLC from COT.data_validation import ( ValueUnsupportedError, ValueTooLowError, ValueTooHighError ) +from COT.platforms.tests import PlatformTests -class TestIOSXRv(unittest.TestCase): +class TestIOSXRv(PlatformTests.PlatformTest): """Test cases for Cisco IOS XRv platform handling.""" cls = IOSXRv + product_string = "com.cisco.ios-xrv" def test_nic_name(self): """Test NIC name construction.""" - self.assertEqual(self.cls.guess_nic_name(1), + self.assertEqual(self.ins.guess_nic_name(1), "MgmtEth0/0/CPU0/0") - self.assertEqual(self.cls.guess_nic_name(2), + self.assertEqual(self.ins.guess_nic_name(2), "GigabitEthernet0/0/0/0") - self.assertEqual(self.cls.guess_nic_name(3), + self.assertEqual(self.ins.guess_nic_name(3), "GigabitEthernet0/0/0/1") - self.assertEqual(self.cls.guess_nic_name(4), + self.assertEqual(self.ins.guess_nic_name(4), "GigabitEthernet0/0/0/2") def test_cpu_count(self): """Test CPU count limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_cpu_count, 0) - self.cls.validate_cpu_count(1) - self.cls.validate_cpu_count(8) - self.assertRaises(ValueTooHighError, self.cls.validate_cpu_count, 9) + self.assertRaises(ValueTooLowError, self.ins.validate_cpu_count, 0) + self.ins.validate_cpu_count(1) + self.ins.validate_cpu_count(8) + self.assertRaises(ValueTooHighError, self.ins.validate_cpu_count, 9) def test_memory_amount(self): """Test RAM allocation limits.""" self.assertRaises(ValueTooLowError, - self.cls.validate_memory_amount, 3071) - self.cls.validate_memory_amount(3072) - self.cls.validate_memory_amount(8192) + self.ins.validate_memory_amount, 3071) + self.ins.validate_memory_amount(3072) + self.ins.validate_memory_amount(8192) self.assertRaises(ValueTooHighError, - self.cls.validate_memory_amount, 8193) + self.ins.validate_memory_amount, 8193) def test_nic_count(self): """Test NIC range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_nic_count, 0) - self.cls.validate_nic_count(1) - self.cls.validate_nic_count(32) + self.assertRaises(ValueTooLowError, self.ins.validate_nic_count, 0) + self.ins.validate_nic_count(1) + self.ins.validate_nic_count(32) # No upper bound known at present def test_nic_type(self): """Test NIC valid and invalid types.""" self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "E1000e") - self.cls.validate_nic_type("E1000") + self.ins.validate_nic_type, "E1000e") + self.ins.validate_nic_type("E1000") self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "PCNet32") - self.cls.validate_nic_type("virtio") + self.ins.validate_nic_type, "PCNet32") + self.ins.validate_nic_type("virtio") self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "VMXNET3") + self.ins.validate_nic_type, "VMXNET3") def test_serial_count(self): """Test serial port range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_serial_count, 0) - self.cls.validate_serial_count(1) - self.cls.validate_serial_count(4) - self.assertRaises(ValueTooHighError, self.cls.validate_serial_count, 5) + self.assertRaises(ValueTooLowError, self.ins.validate_serial_count, 0) + self.ins.validate_serial_count(1) + self.ins.validate_serial_count(4) + self.assertRaises(ValueTooHighError, self.ins.validate_serial_count, 5) class TestIOSXRvRP(TestIOSXRv): """Test cases for Cisco IOS XRv HA-capable RP platform handling.""" cls = IOSXRvRP + product_string = "com.cisco.ios-xrv.rp" # Inherit all test cases from IOSXRv class, except where overridden below: @@ -92,23 +94,24 @@ def test_nic_name(self): An HA-capable RP has a fabric interface in addition to the usual MgmtEth NIC, but does not have GigabitEthernet NICs. """ - self.assertEqual(self.cls.guess_nic_name(1), + self.assertEqual(self.ins.guess_nic_name(1), "fabric") - self.assertEqual(self.cls.guess_nic_name(2), + self.assertEqual(self.ins.guess_nic_name(2), "MgmtEth0/{SLOT}/CPU0/0") def test_nic_count(self): """Test NIC range limits. Only fabric+MgmtEth is allowed.""" - self.assertRaises(ValueTooLowError, self.cls.validate_nic_count, 0) - self.cls.validate_nic_count(1) - self.cls.validate_nic_count(2) - self.assertRaises(ValueTooHighError, self.cls.validate_nic_count, 3) + self.assertRaises(ValueTooLowError, self.ins.validate_nic_count, 0) + self.ins.validate_nic_count(1) + self.ins.validate_nic_count(2) + self.assertRaises(ValueTooHighError, self.ins.validate_nic_count, 3) class TestIOSXRvLC(TestIOSXRv): """Test cases for Cisco IOS XRv line card platform handling.""" cls = IOSXRvLC + product_string = "com.cisco.ios-xrv.lc" # Inherit all test cases from IOSXRv class, except where overridden below: @@ -117,13 +120,13 @@ def test_nic_name(self): An LC has a fabric but no MgmtEth. """ - self.assertEqual(self.cls.guess_nic_name(1), + self.assertEqual(self.ins.guess_nic_name(1), "fabric") - self.assertEqual(self.cls.guess_nic_name(2), + self.assertEqual(self.ins.guess_nic_name(2), "GigabitEthernet0/{SLOT}/0/0") - self.assertEqual(self.cls.guess_nic_name(3), + self.assertEqual(self.ins.guess_nic_name(3), "GigabitEthernet0/{SLOT}/0/1") - self.assertEqual(self.cls.guess_nic_name(4), + self.assertEqual(self.ins.guess_nic_name(4), "GigabitEthernet0/{SLOT}/0/2") def test_serial_count(self): @@ -131,6 +134,6 @@ def test_serial_count(self): An LC with zero serial ports is valid. """ - self.cls.validate_serial_count(0) - self.cls.validate_serial_count(4) - self.assertRaises(ValueTooHighError, self.cls.validate_serial_count, 5) + self.ins.validate_serial_count(0) + self.ins.validate_serial_count(4) + self.assertRaises(ValueTooHighError, self.ins.validate_serial_count, 5) diff --git a/COT/platforms/tests/test_cisco_iosxrv_9000.py b/COT/platforms/tests/test_cisco_iosxrv_9000.py index df4d6a4..11d254b 100644 --- a/COT/platforms/tests/test_cisco_iosxrv_9000.py +++ b/COT/platforms/tests/test_cisco_iosxrv_9000.py @@ -14,67 +14,68 @@ """Unit test cases for IOSXRv9000 class.""" -import unittest from COT.platforms.cisco_iosxrv_9000 import IOSXRv9000 from COT.data_validation import ( ValueUnsupportedError, ValueTooLowError, ValueTooHighError ) +from COT.platforms.tests import PlatformTests -class TestIOSXRv9000(unittest.TestCase): +class TestIOSXRv9000(PlatformTests.PlatformTest): """Test cases for Cisco IOS XRv 9000 platform handling.""" cls = IOSXRv9000 + product_string = "com.cisco.ios-xrv9000" def test_nic_name(self): """Test NIC name construction.""" - self.assertEqual(self.cls.guess_nic_name(1), + self.assertEqual(self.ins.guess_nic_name(1), "MgmtEth0/0/CPU0/0") - self.assertEqual(self.cls.guess_nic_name(2), + self.assertEqual(self.ins.guess_nic_name(2), "CtrlEth") - self.assertEqual(self.cls.guess_nic_name(3), + self.assertEqual(self.ins.guess_nic_name(3), "DevEth") - self.assertEqual(self.cls.guess_nic_name(4), + self.assertEqual(self.ins.guess_nic_name(4), "GigabitEthernet0/0/0/0") - self.assertEqual(self.cls.guess_nic_name(5), + self.assertEqual(self.ins.guess_nic_name(5), "GigabitEthernet0/0/0/1") def test_cpu_count(self): """Test CPU count limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_cpu_count, 0) - self.cls.validate_cpu_count(1) - self.cls.validate_cpu_count(32) - self.assertRaises(ValueTooHighError, self.cls.validate_cpu_count, 33) + self.assertRaises(ValueTooLowError, self.ins.validate_cpu_count, 0) + self.ins.validate_cpu_count(1) + self.ins.validate_cpu_count(32) + self.assertRaises(ValueTooHighError, self.ins.validate_cpu_count, 33) def test_memory_amount(self): """Test RAM allocation limits.""" self.assertRaises(ValueTooLowError, - self.cls.validate_memory_amount, 8191) - self.cls.validate_memory_amount(8192) - self.cls.validate_memory_amount(32768) - self.cls.validate_memory_amount(128 * 1024) + self.ins.validate_memory_amount, 8191) + self.ins.validate_memory_amount(8192) + self.ins.validate_memory_amount(32768) + self.ins.validate_memory_amount(128 * 1024) def test_nic_count(self): """Test NIC range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_nic_count, 0) - self.assertRaises(ValueTooLowError, self.cls.validate_nic_count, 3) - self.cls.validate_nic_count(4) - self.cls.validate_nic_count(32) + self.assertRaises(ValueTooLowError, self.ins.validate_nic_count, 0) + self.assertRaises(ValueTooLowError, self.ins.validate_nic_count, 3) + self.ins.validate_nic_count(4) + self.ins.validate_nic_count(32) # No upper bound known at present def test_nic_type(self): """Test NIC valid and invalid types.""" self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "E1000e") - self.cls.validate_nic_type("E1000") + self.ins.validate_nic_type, "E1000e") + self.ins.validate_nic_type("E1000") self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "PCNet32") - self.cls.validate_nic_type("virtio") - self.cls.validate_nic_type("VMXNET3") + self.ins.validate_nic_type, "PCNet32") + self.ins.validate_nic_type("virtio") + self.ins.validate_nic_type("VMXNET3") def test_serial_count(self): """Test serial port range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_serial_count, 0) - self.cls.validate_serial_count(1) - self.cls.validate_serial_count(4) - self.assertRaises(ValueTooHighError, self.cls.validate_serial_count, 5) + self.assertRaises(ValueTooLowError, self.ins.validate_serial_count, 0) + self.ins.validate_serial_count(1) + self.ins.validate_serial_count(4) + self.assertRaises(ValueTooHighError, self.ins.validate_serial_count, 5) diff --git a/COT/platforms/tests/test_cisco_nexus_9000v.py b/COT/platforms/tests/test_cisco_nexus_9000v.py index 0fc4e26..667d4f0 100644 --- a/COT/platforms/tests/test_cisco_nexus_9000v.py +++ b/COT/platforms/tests/test_cisco_nexus_9000v.py @@ -14,64 +14,65 @@ """Unit test cases for Nexus 9000v platform.""" -import unittest from COT.platforms.cisco_nexus_9000v import Nexus9000v from COT.data_validation import ( ValueUnsupportedError, ValueTooLowError, ValueTooHighError ) +from COT.platforms.tests import PlatformTests -class TestNexus9000v(unittest.TestCase): +class TestNexus9000v(PlatformTests.PlatformTest): """Test cases for Cisco Nexus 9000v platform handling.""" cls = Nexus9000v + product_string = "com.cisco.n9k" def test_nic_name(self): """Test NIC name construction.""" - self.assertEqual(self.cls.guess_nic_name(1), + self.assertEqual(self.ins.guess_nic_name(1), "mgmt0") - self.assertEqual(self.cls.guess_nic_name(2), + self.assertEqual(self.ins.guess_nic_name(2), "Ethernet1/1") - self.assertEqual(self.cls.guess_nic_name(3), + self.assertEqual(self.ins.guess_nic_name(3), "Ethernet1/2") - self.assertEqual(self.cls.guess_nic_name(4), + self.assertEqual(self.ins.guess_nic_name(4), "Ethernet1/3") def test_cpu_count(self): """Test CPU count limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_cpu_count, 0) - self.cls.validate_cpu_count(1) - self.cls.validate_cpu_count(4) - self.assertRaises(ValueTooHighError, self.cls.validate_cpu_count, 5) + self.assertRaises(ValueTooLowError, self.ins.validate_cpu_count, 0) + self.ins.validate_cpu_count(1) + self.ins.validate_cpu_count(4) + self.assertRaises(ValueTooHighError, self.ins.validate_cpu_count, 5) def test_memory_amount(self): """Test RAM allocation limits.""" self.assertRaises(ValueTooLowError, - self.cls.validate_memory_amount, 8191) - self.cls.validate_memory_amount(8192) - self.cls.validate_memory_amount(16384) + self.ins.validate_memory_amount, 8191) + self.ins.validate_memory_amount(8192) + self.ins.validate_memory_amount(16384) # No upper bound known at present def test_nic_count(self): """Test NIC range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_nic_count, 0) - self.cls.validate_nic_count(1) - self.cls.validate_nic_count(65) - self.assertRaises(ValueTooHighError, self.cls.validate_nic_count, 66) + self.assertRaises(ValueTooLowError, self.ins.validate_nic_count, 0) + self.ins.validate_nic_count(1) + self.ins.validate_nic_count(65) + self.assertRaises(ValueTooHighError, self.ins.validate_nic_count, 66) def test_nic_type(self): """Test NIC valid and invalid types.""" self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "E1000e") - self.cls.validate_nic_type("E1000") + self.ins.validate_nic_type, "E1000e") + self.ins.validate_nic_type("E1000") self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "PCNet32") + self.ins.validate_nic_type, "PCNet32") self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "virtio") - self.cls.validate_nic_type("VMXNET3") + self.ins.validate_nic_type, "virtio") + self.ins.validate_nic_type("VMXNET3") def test_serial_count(self): """Test serial port range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_serial_count, 0) - self.cls.validate_serial_count(1) - self.assertRaises(ValueTooHighError, self.cls.validate_serial_count, 2) + self.assertRaises(ValueTooLowError, self.ins.validate_serial_count, 0) + self.ins.validate_serial_count(1) + self.assertRaises(ValueTooHighError, self.ins.validate_serial_count, 2) diff --git a/COT/platforms/tests/test_cisco_nxosv.py b/COT/platforms/tests/test_cisco_nxosv.py index e252698..385efa2 100644 --- a/COT/platforms/tests/test_cisco_nxosv.py +++ b/COT/platforms/tests/test_cisco_nxosv.py @@ -1,7 +1,7 @@ # test_cisco_nxosv.py - Unit test cases for Cisco NXOSv platform # # October 2016, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 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. # @@ -14,71 +14,72 @@ """Unit test cases for NXOSv platform.""" -import unittest from COT.platforms.cisco_nxosv import NXOSv from COT.data_validation import ( ValueUnsupportedError, ValueTooLowError, ValueTooHighError ) +from COT.platforms.tests import PlatformTests -class TestNXOSv(unittest.TestCase): +class TestNXOSv(PlatformTests.PlatformTest): """Test cases for Cisco NX-OSv platform handling.""" cls = NXOSv + product_string = "com.cisco.nx-osv" def test_nic_name(self): """Test NIC name construction.""" - self.assertEqual(self.cls.guess_nic_name(1), + self.assertEqual(self.ins.guess_nic_name(1), "mgmt0") - self.assertEqual(self.cls.guess_nic_name(2), + self.assertEqual(self.ins.guess_nic_name(2), "Ethernet2/1") - self.assertEqual(self.cls.guess_nic_name(3), + self.assertEqual(self.ins.guess_nic_name(3), "Ethernet2/2") - self.assertEqual(self.cls.guess_nic_name(4), + self.assertEqual(self.ins.guess_nic_name(4), "Ethernet2/3") # ... - self.assertEqual(self.cls.guess_nic_name(49), + self.assertEqual(self.ins.guess_nic_name(49), "Ethernet2/48") - self.assertEqual(self.cls.guess_nic_name(50), + self.assertEqual(self.ins.guess_nic_name(50), "Ethernet3/1") def test_cpu_count(self): """Test CPU count limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_cpu_count, 0) - self.cls.validate_cpu_count(1) - self.cls.validate_cpu_count(8) - self.assertRaises(ValueTooHighError, self.cls.validate_cpu_count, 9) + self.assertRaises(ValueTooLowError, self.ins.validate_cpu_count, 0) + self.ins.validate_cpu_count(1) + self.ins.validate_cpu_count(8) + self.assertRaises(ValueTooHighError, self.ins.validate_cpu_count, 9) def test_memory_amount(self): """Test RAM allocation limits.""" self.assertRaises(ValueTooLowError, - self.cls.validate_memory_amount, 2047) - self.cls.validate_memory_amount(2048) - self.cls.validate_memory_amount(8192) + self.ins.validate_memory_amount, 2047) + self.ins.validate_memory_amount(2048) + self.ins.validate_memory_amount(8192) self.assertRaises(ValueTooHighError, - self.cls.validate_memory_amount, 8193) + self.ins.validate_memory_amount, 8193) def test_nic_count(self): """Test NIC range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_nic_count, -1) - self.cls.validate_nic_count(0) - self.cls.validate_nic_count(32) + self.assertRaises(ValueTooLowError, self.ins.validate_nic_count, -1) + self.ins.validate_nic_count(0) + self.ins.validate_nic_count(32) # No upper bound known at present def test_nic_type(self): """Test NIC valid and invalid types.""" self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "E1000e") - self.cls.validate_nic_type("E1000") + self.ins.validate_nic_type, "E1000e") + self.ins.validate_nic_type("E1000") self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "PCNet32") - self.cls.validate_nic_type("virtio") + self.ins.validate_nic_type, "PCNet32") + self.ins.validate_nic_type("virtio") self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "VMXNET3") + self.ins.validate_nic_type, "VMXNET3") def test_serial_count(self): """Test serial port range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_serial_count, 0) - self.cls.validate_serial_count(1) - self.cls.validate_serial_count(2) - self.assertRaises(ValueTooHighError, self.cls.validate_serial_count, 3) + self.assertRaises(ValueTooLowError, self.ins.validate_serial_count, 0) + self.ins.validate_serial_count(1) + self.ins.validate_serial_count(2) + self.assertRaises(ValueTooHighError, self.ins.validate_serial_count, 3) diff --git a/COT/platforms/tests/test_generic.py b/COT/platforms/tests/test_generic.py deleted file mode 100644 index e50cc17..0000000 --- a/COT/platforms/tests/test_generic.py +++ /dev/null @@ -1,57 +0,0 @@ -# test_generic_platform.py - Unit test cases for COT "generic platform" -# -# October 2016, Glenn F. Matthews -# Copyright (c) 2014-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. - -"""Unit test cases for the GenericPlatform class.""" - -import unittest -from COT.platforms.generic import GenericPlatform -from COT.data_validation import ValueTooLowError - - -class TestGenericPlatform(unittest.TestCase): - """Test cases for generic platform handling.""" - - cls = GenericPlatform - - def test_controller_type_for_device(self): - """Test platform-specific logic for device controllers.""" - self.assertEqual(self.cls.controller_type_for_device('harddisk'), - 'ide') - self.assertEqual(self.cls.controller_type_for_device('cdrom'), - 'ide') - - def test_nic_name(self): - """Test NIC name construction.""" - self.assertEqual(self.cls.guess_nic_name(1), "Ethernet1") - self.assertEqual(self.cls.guess_nic_name(100), "Ethernet100") - - def test_cpu_count(self): - """Test CPU count limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_cpu_count, 0) - self.cls.validate_cpu_count(1) - - def test_memory_amount(self): - """Test RAM allocation limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_memory_amount, 0) - self.cls.validate_memory_amount(1) - - def test_nic_count(self): - """Test NIC range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_nic_count, -1) - self.cls.validate_nic_count(0) - - def test_serial_count(self): - """Test serial port range limits.""" - self.assertRaises(ValueTooLowError, self.cls.validate_serial_count, -1) - self.cls.validate_serial_count(0) diff --git a/COT/platforms/tests/test_platform.py b/COT/platforms/tests/test_platform.py new file mode 100644 index 0000000..7064907 --- /dev/null +++ b/COT/platforms/tests/test_platform.py @@ -0,0 +1,25 @@ +# test_generic_platform.py - Unit test cases for COT "generic platform" +# +# October 2016, Glenn F. Matthews +# Copyright (c) 2014-2017 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. + +"""Unit test cases for the Platform class.""" + +from COT.platforms.tests import PlatformTests +from COT.platforms.platform import Platform + + +class TestPlatform(PlatformTests.PlatformTest): + """Test cases for generic platform handling.""" + + cls = Platform + product_string = "" diff --git a/COT/tests/test_add_disk.py b/COT/tests/test_add_disk.py index d606f7d..d691322 100644 --- a/COT/tests/test_add_disk.py +++ b/COT/tests/test_add_disk.py @@ -3,7 +3,7 @@ # test_add_disk.py - test cases for the COTAddDisk class # # January 2015, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 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. # @@ -26,7 +26,8 @@ from COT.add_disk import COTAddDisk from COT.data_validation import InvalidInputError, ValueMismatchError from COT.data_validation import ValueUnsupportedError, ValueTooHighError -from COT.disks import create_disk, disk_representation_from_file +from COT.disks import DiskRepresentation +from COT.disks.qcow2 import QCOW2 class TestCOTAddDisk(COT_UT): @@ -468,7 +469,7 @@ def test_disk_conversion(self): # Create a qcow2 image and add it as a new disk new_qcow2 = os.path.join(self.temp_dir, "new.qcow2") # Make it a small file to keep the test fast - create_disk('qcow2', path=new_qcow2, capacity="16M") + QCOW2.create_file(path=new_qcow2, capacity="16M") self.instance.package = self.input_ovf self.instance.disk_image = new_qcow2 self.instance.controller = 'scsi' @@ -503,8 +504,8 @@ def test_disk_conversion(self): """.format(cfg_size=self.FILE_SIZE['sample_cfg.txt'], new_size=os.path.getsize(os.path.join(self.temp_dir, "new.vmdk")))) # Make sure the disk was actually converted to the right format - dr = disk_representation_from_file(os.path.join(self.temp_dir, - "new.vmdk")) + dr = DiskRepresentation.from_file(os.path.join(self.temp_dir, + "new.vmdk")) self.assertEqual(dr.disk_format, 'vmdk') self.assertEqual(dr.disk_subformat, "streamOptimized") @@ -513,7 +514,7 @@ def test_disk_conversion_and_replacement(self): # Create a qcow2 image and add it as replacement for the existing vmdk new_qcow2 = os.path.join(self.temp_dir, "input.qcow2") # Keep it small! - create_disk('qcow2', path=new_qcow2, capacity="16M") + QCOW2.create_file(path=new_qcow2, capacity="16M") self.instance.package = self.input_ovf self.instance.disk_image = new_qcow2 self.instance.run() @@ -727,7 +728,7 @@ def test_add_disk_no_room(self): # Create a qcow2 image new_qcow2 = os.path.join(self.temp_dir, "foozle.qcow2") # Keep it small! - create_disk('qcow2', path=new_qcow2, capacity="16M") + QCOW2.create_file(path=new_qcow2, capacity="16M") # Try to add a fifth disk - IDE controllers are full! self.instance.package = self.temp_file self.instance.disk_image = new_qcow2 @@ -748,7 +749,7 @@ def test_overwrite_implicit_file_id(self): self.assertLogged(**self.OVERWRITING_DISK) self.instance.finished() self.assertLogged(**self.invalid_hardware_warning( - "howlongofaprofilenamecanweusehere", "0 MiB", "RAM")) + "howlongofaprofilenamecanweusehere", "0", "MiB of RAM")) self.assertLogged(msg="Removing unused network") self.check_diff(file1=self.invalid_ovf, expected=""" diff --git a/COT/tests/test_doctests.py b/COT/tests/test_doctests.py index 12bf669..2794bca 100644 --- a/COT/tests/test_doctests.py +++ b/COT/tests/test_doctests.py @@ -39,5 +39,7 @@ def load_tests(*_): suite.addTests(DocTestSuite('COT.edit_hardware')) suite.addTests(DocTestSuite('COT.edit_properties')) suite.addTests(DocTestSuite('COT.file_reference')) + suite.addTests(DocTestSuite('COT.logging_')) suite.addTests(DocTestSuite('COT.platforms')) + suite.addTests(DocTestSuite('COT.ui_shared')) return suite diff --git a/COT/tests/test_edit_hardware.py b/COT/tests/test_edit_hardware.py index 5d1a59a..b38643d 100644 --- a/COT/tests/test_edit_hardware.py +++ b/COT/tests/test_edit_hardware.py @@ -128,6 +128,7 @@ def test_invalid_always_args(self): def test_valid_by_platform(self): """Verify that some input values' validity depends on platform.""" self.instance.package = self.input_ovf + self.instance.ui.default_confirm_response = False # IOSv only supports 1 vCPU and up to 3 GB of RAM self.set_vm_platform(IOSv) with self.assertRaises(InvalidInputError): diff --git a/COT/tests/test_edit_product.py b/COT/tests/test_edit_product.py index 82ed155..af6c6c2 100644 --- a/COT/tests/test_edit_product.py +++ b/COT/tests/test_edit_product.py @@ -3,7 +3,7 @@ # edit_product.py - test cases for the COTEditProduct class # # January 2015, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 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. # @@ -85,15 +85,13 @@ def test_edit_product_class(self): self.instance.run() self.instance.finished() self.assertLogged(**self.invalid_hardware_warning( - '1CPU-384MB-2NIC', "384 MiB", "RAM")) + '1CPU-384MB-2NIC', "384", "MiB of RAM")) self.assertLogged(**self.invalid_hardware_warning( '1CPU-384MB-2NIC', "2", "NIC count")) self.assertLogged(**self.invalid_hardware_warning( - '1CPU-1GB-8NIC', "1024 MiB", "RAM")) + '1CPU-1GB-8NIC', "1024", "MiB of RAM")) self.assertLogged(**self.invalid_hardware_warning( - '1CPU-3GB-10NIC', "3072 MiB", "RAM")) - self.assertLogged(**self.invalid_hardware_warning( - '1CPU-3GB-16NIC', "3072 MiB", "RAM")) + '1CPU-3GB-10NIC', "3072", "MiB of RAM")) self.check_diff(file1=self.iosv_ovf, expected=""" diff --git a/COT/tests/test_inject_config.py b/COT/tests/test_inject_config.py index 92b6f5f..897c31f 100644 --- a/COT/tests/test_inject_config.py +++ b/COT/tests/test_inject_config.py @@ -3,7 +3,7 @@ # test_inject_config.py - test cases for the COTInjectConfig class # # December 2014, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 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. # @@ -31,7 +31,7 @@ from COT.data_validation import InvalidInputError, ValueUnsupportedError from COT.platforms import CSR1000V, IOSv, IOSXRv, IOSXRvLC from COT.helpers import helpers -from COT.disks import disk_representation_from_file +from COT.disks import DiskRepresentation from COT.remove_file import COTRemoveFile logger = logging.getLogger(__name__) @@ -139,7 +139,7 @@ def test_inject_config_iso(self): if helpers['isoinfo']: # The sample_cfg.text should be renamed to the platform-specific # file name for bootstrap config - in this case, config.txt - self.assertEqual(disk_representation_from_file(config_iso).files, + self.assertEqual(DiskRepresentation.from_file(config_iso).files, ["config.txt"]) else: logger.info("isoinfo not available, not checking disk contents") @@ -157,11 +157,11 @@ def test_inject_config_iso_secondary(self): self.assertLogged(**self.invalid_hardware_warning( '1CPU-1GB-1NIC', 'VMXNET3', 'NIC type')) self.assertLogged(**self.invalid_hardware_warning( - '1CPU-1GB-1NIC', '1024 MiB', 'RAM')) + '1CPU-1GB-1NIC', '1024', 'MiB of RAM')) self.assertLogged(**self.invalid_hardware_warning( '2CPU-2GB-1NIC', 'VMXNET3', 'NIC type')) self.assertLogged(**self.invalid_hardware_warning( - '2CPU-2GB-1NIC', '2048 MiB', 'RAM')) + '2CPU-2GB-1NIC', '2048', 'MiB of RAM')) config_iso = os.path.join(self.temp_dir, 'config.iso') self.check_diff(""" >> to_string("Hello") + 'Hello' + >>> to_string(27.5) + '27.5' + >>> e = ET.Element('hello', attrib={'key': 'value'}) + >>> print(e) # doctest: +ELLIPSIS + + >>> print(to_string(e)) + + """ + if ET.iselement(obj): + if sys.version_info[0] >= 3: + return ET.tostring(obj, encoding='unicode') + else: + return ET.tostring(obj) + else: + return str(obj) + + +def pretty_bytes(byte_value, base_shift=0): + """Pretty-print the given bytes value. + + Args: + byte_value (float): Value + base_shift (int): Base value of byte_value + (0 = bytes, 1 = KiB, 2 = MiB, etc.) + + Returns: + str: Pretty-printed byte string such as "1.00 GiB" + + Examples: + :: + + >>> pretty_bytes(512) + '512 B' + >>> pretty_bytes(512, 2) + '512 MiB' + >>> pretty_bytes(65536, 2) + '64 GiB' + >>> pretty_bytes(65547) + '64.01 KiB' + >>> pretty_bytes(65530, 3) + '63.99 TiB' + >>> pretty_bytes(1023850) + '999.9 KiB' + >>> pretty_bytes(1024000) + '1000 KiB' + >>> pretty_bytes(1048575) + '1024 KiB' + >>> pretty_bytes(1049200) + '1.001 MiB' + >>> pretty_bytes(2560) + '2.5 KiB' + >>> pretty_bytes(.0001, 3) + '104.9 KiB' + >>> pretty_bytes(.01, 1) + '10 B' + >>> pretty_bytes(.001, 1) + '1 B' + >>> pretty_bytes(.0001, 1) + '0 B' + >>> pretty_bytes(100, -1) + Traceback (most recent call last): + ... + ValueError: base_shift must not be negative + """ + if base_shift < 0: + raise ValueError("base_shift must not be negative") + tags = ["B", "KiB", "MiB", "GiB", "TiB"] + byte_value = float(byte_value) + shift = base_shift + while byte_value >= 1024.0: + byte_value /= 1024.0 + shift += 1 + while byte_value < 1.0 and shift > 0: + byte_value *= 1024.0 + shift -= 1 + # Fractions of a byte should be considered a rounding error: + if shift == 0: + byte_value = round(byte_value) + return "{0:.4g} {1}".format(byte_value, tags[shift]) + + class UI(object): """Abstract user interface functionality. @@ -114,6 +224,23 @@ def confirm_or_die(self, prompt): if not self.confirm(prompt): sys.exit("Aborting.") + def validate_value(self, helper_function, *args): + """Ask the user whether to ignore a ValueError. + + Args: + helper_function (function): Validation function to call, which + may raise a ValueError. + *args: Arguments to pass to `helper_function`. + Raises: + ValueError: if `helper_function` raises a ValueError and the + user declines to ignore it. + """ + try: + helper_function(*args) + except ValueError as err: + if not self.confirm("Warning:\n{0}\nContinue anyway?".format(err)): + raise + def choose_from_list(self, footer, option_list, default_value, header="", info_list=None): """Prompt the user to choose from a list. @@ -188,3 +315,8 @@ def get_password(self, username, host): NotImplementedError: Must be implemented by a subclass. """ raise NotImplementedError("No implementation of get_password()") + + +if __name__ == "__main__": # pragma: no cover + import doctest + doctest.testmod() diff --git a/COT/vm_description.py b/COT/vm_description.py index 35498f0..34b8c47 100644 --- a/COT/vm_description.py +++ b/COT/vm_description.py @@ -3,7 +3,7 @@ # vm_description.py - Abstract class for reading, editing, and writing VMs # # September 2013, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 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. # @@ -156,10 +156,10 @@ def product_class(self, product_class): @property def platform(self): - """The Platform class object associated with this VM. + """The Platform instance object associated with this VM. - :class:`~COT.platforms.GenericPlatform` or a more specific subclass - if recognized as such. + An instance of :class:`~COT.platforms.Platform` or a more specific + subclass if recognized as such. """ raise NotImplementedError("no platform value available.") diff --git a/docs/COT.disks.disk.rst b/docs/COT.disks.disk.rst deleted file mode 100644 index f9b446b..0000000 --- a/docs/COT.disks.disk.rst +++ /dev/null @@ -1,4 +0,0 @@ -``COT.disks.disk`` module -========================= - -.. automodule:: COT.disks.disk diff --git a/docs/COT.logging_.rst b/docs/COT.logging_.rst new file mode 100644 index 0000000..aa52a5b --- /dev/null +++ b/docs/COT.logging_.rst @@ -0,0 +1,4 @@ +``COT.logging_`` module +======================= + +.. automodule:: COT.logging_ diff --git a/docs/COT.ovf.utilities.rst b/docs/COT.ovf.utilities.rst new file mode 100644 index 0000000..5fc95cc --- /dev/null +++ b/docs/COT.ovf.utilities.rst @@ -0,0 +1,4 @@ +``COT.ovf.utilities`` module +============================ + +.. automodule:: COT.ovf.utilities diff --git a/docs/COT.platforms.generic.rst b/docs/COT.platforms.generic.rst deleted file mode 100644 index a9f9506..0000000 --- a/docs/COT.platforms.generic.rst +++ /dev/null @@ -1,4 +0,0 @@ -``COT.platforms.generic`` module -================================ - -.. automodule:: COT.platforms.generic diff --git a/docs/COT.platforms.platform.rst b/docs/COT.platforms.platform.rst new file mode 100644 index 0000000..84e0751 --- /dev/null +++ b/docs/COT.platforms.platform.rst @@ -0,0 +1,4 @@ +``COT.platforms.platform`` module +================================= + +.. automodule:: COT.platforms.platform diff --git a/docs/COT.platforms.rst b/docs/COT.platforms.rst index 3242fb6..06be2a1 100644 --- a/docs/COT.platforms.rst +++ b/docs/COT.platforms.rst @@ -2,3 +2,4 @@ =================================== .. automodule:: COT.platforms + :no-members: diff --git a/requirements.txt b/requirements.txt index cfedd14..0a59d18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ argparse backports.shutil_get_terminal_size colorlog>=2.5.0 +enum34 ; python_version < "3.4" pyvmomi>=5.5.0.2014.1 requests>=2.5.1 verboselogs>=1.2 diff --git a/setup.py b/setup.py index 1ce8522..797b46b 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,9 @@ 'pyvmomi>=5.5.0.2014.1', 'requests>=2.5.1', 'verboselogs>=1.0', + + # enum module is standard in Python 3.4 and later, else use enum34 + 'enum34; python_version < "3.4"', ] # shutil.get_terminal_size is standard in 3.3 and later only. if sys.version_info < (3, 3):