From f5e42c6374073517508ed89dac24521a64fd54f2 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 28 Jun 2016 10:07:14 -0400 Subject: [PATCH 01/59] Enable pylint.extensions.check_docs, fix warnings in COT.helpers module --- .pylintrc | 2 +- COT/helpers/api.py | 2 +- COT/helpers/fatdisk.py | 3 ++- COT/helpers/helper.py | 5 ++++- COT/helpers/qemu_img.py | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.pylintrc b/.pylintrc index 158aaf1..6d678cb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,6 +1,6 @@ [MASTER] -load-plugins=verboselogs.pylint +load-plugins=pylint.extensions.check_docs,verboselogs.pylint # Ignore generated code we don't control ignore=_version.py diff --git a/COT/helpers/api.py b/COT/helpers/api.py index ed93660..7c7a12c 100644 --- a/COT/helpers/api.py +++ b/COT/helpers/api.py @@ -291,7 +291,7 @@ def create_disk_image(file_path, file_format=None, :param str file_format: Desired image format (if not specified, this will be derived from the file extension of :attr:`file_path`) - :param capacity: Disk capacity. A string like '16M' or '1G'. + :param str capacity: Disk capacity. A string like '16M' or '1G'. :param list contents: List of file paths to package into the created image. If not specified, the image will be left blank and unformatted. """ diff --git a/COT/helpers/fatdisk.py b/COT/helpers/fatdisk.py index 898b65a..56b93ea 100644 --- a/COT/helpers/fatdisk.py +++ b/COT/helpers/fatdisk.py @@ -107,7 +107,8 @@ def create_raw_image(self, file_path, contents, capacity=None): :param str file_path: Desired location of new disk image :param list contents: List of file paths to package into the created image. - :param capacity: (optional) Disk capacity. A string like '16M' or '1G'. + :param str capacity: (optional) Disk capacity. A string like '16M' + or '1G'. """ if not capacity: # What size disk do we need to contain the requested file(s)? diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index eb2a0dc..9f12163 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -168,7 +168,8 @@ def yum_install(cls, package): def make_install_dir(cls, directory, permissions=493): # 493 == 0o755 """Check whether the given target directory exists, and create if not. - :param directory: Directory to check/create. + :param str directory: Directory to check/create. + :param int permissions: Permissions to set on the created directory. """ if os.path.isdir(directory): # TODO: permissions check, update permissions if needed @@ -329,6 +330,7 @@ def _check_call(cls, args, require_success=True, retry_with_sudo=False, when the command exits with a return code other than 0 :param boolean retry_with_sudo: If ``True``, if the command gets an exception, prepend ``sudo`` to the command and try again. + :param kwargs: Arguments passed to :func:`subprocess.check_call`. :raise HelperNotFoundError: if the command doesn't exist (instead of a :class:`OSError`) @@ -378,6 +380,7 @@ def _check_output(cls, args, require_success=True, **kwargs): :param list args: Command to invoke and its associated args :param boolean require_success: If ``False``, do not raise an error when the command exits with a return code other than 0 + :param kwargs: Arguments passed to :func:`subprocess.check_output`. :return: Captured stdout/stderr from the command diff --git a/COT/helpers/qemu_img.py b/COT/helpers/qemu_img.py index 49f0e8f..c7a6094 100644 --- a/COT/helpers/qemu_img.py +++ b/COT/helpers/qemu_img.py @@ -163,7 +163,7 @@ def create_blank_disk(self, file_path, capacity, file_format=None): """Create an unformatted disk image at the requested location. :param str file_path: Desired location of new disk image - :param capacity: Disk capacity. A string like '16M' or '1G'. + :param str capacity: Disk capacity. A string like '16M' or '1G'. :param str file_format: Desired image format (if not specified, this will be derived from the file extension of :attr:`file_path`) """ From c3e2c40c8a2149cffb08ed156bd209efb5ae19fa Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 30 Jun 2016 21:52:17 -0400 Subject: [PATCH 02/59] Fix various docstring warnings --- CHANGELOG.rst | 2 +- COT/add_disk.py | 54 +++++- COT/add_file.py | 9 +- COT/cli.py | 26 ++- COT/data_validation.py | 50 ++++-- COT/deploy.py | 43 ++++- COT/deploy_esxi.py | 105 ++++++++---- COT/edit_hardware.py | 9 +- COT/edit_product.py | 9 +- COT/edit_properties.py | 9 +- COT/file_reference.py | 107 ++++++++++-- COT/help.py | 9 +- COT/helpers/api.py | 1 + COT/helpers/helper.py | 35 +++- COT/info.py | 9 +- COT/inject_config.py | 9 +- COT/install_helpers.py | 23 ++- COT/ovf/hardware.py | 38 ++++- COT/ovf/item.py | 76 +++++++-- COT/ovf/name_helper.py | 31 +++- COT/ovf/ovf.py | 136 ++++++++++++--- COT/platforms.py | 281 +++++++++++++++++-------------- COT/remove_file.py | 9 +- COT/submodule.py | 27 ++- COT/tests/test_file_reference.py | 16 ++ COT/tests/test_platforms.py | 7 +- COT/tests/ut.py | 93 ++++++++-- COT/ui_shared.py | 21 ++- COT/vm_context_manager.py | 8 +- COT/vm_description.py | 72 ++++---- COT/xml_file.py | 53 ++++-- docs/conf.py | 9 + 32 files changed, 1057 insertions(+), 329 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 632212e..d964deb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -28,6 +28,7 @@ This project adheres to `Semantic Versioning`_. **Fixed** +- CSR1000v platform supports 8 CPUs as a valid option. - ``ValueMismatchError`` exceptions are properly caught by the CLI wrapper so as to result in a graceful exit rather than a stack trace. - ``cot remove-file`` now errors if the user specifies both file-id and @@ -36,7 +37,6 @@ This project adheres to `Semantic Versioning`_. - Better handling of exceptions and usage of ``sudo`` when installing helpers. - Manual pages are now correctly included in the distribution. Oops! - `1.6.0`_ - 2016-06-30 --------------------- diff --git a/COT/add_disk.py b/COT/add_disk.py index c0bc4e6..dc7e0e6 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -75,6 +75,9 @@ def validate_controller_address(controller, address): class COTAddDisk(COTSubmodule): """Add or replace a disk in a virtual machine. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, @@ -92,7 +95,11 @@ class COTAddDisk(COTSubmodule): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTAddDisk, self).__init__(ui) self._disk_image = None self.disk_type = None @@ -254,7 +261,11 @@ def create_subparser(self): def guess_disk_type_from_extension(disk_file): - """Guess the disk type (harddisk/cdrom) from the disk file name.""" + """Guess the disk type (harddisk/cdrom) from the disk file name. + + :param str disk_file: File name or file path. + :return: "cdrom" or "harrdisk" + """ disk_extension = os.path.splitext(disk_file)[1] ext_type_map = { '.iso': 'cdrom', @@ -299,6 +310,13 @@ def search_for_elements(vm, disk_file, file_id, controller, address): of the above arguments - in which case we need to make sure that all relevant approaches agree on what sections we're talking about... + :param vm: Virtual machine object + :type vm: :class:`~COT.vm_description.VMDescription` + :param str disk_file: Disk file name or path + :param str file_id: File identifier + :param str controller: controller type, "ide" or "scsi" + :param str address: device address, such as "1:0" + :raises ValueMismatchError: if the criteria select a non-unique set. :return: (file_object, disk_object, controller_item, disk_item) """ @@ -331,7 +349,13 @@ def search_for_elements(vm, disk_file, file_id, controller, address): def guess_controller_type(vm, ctrl_item, disk_type): - """If a controller type wasn't specified, try to guess from context.""" + """If a controller type wasn't specified, try to guess from context. + + :param vm: Virtual machine object + :type vm: :class:`~COT.vm_description.VMDescription` + :param object ctrl_item: Any known controller object + :param str disk_type: "cdrom" or "harddisk" + """ if ctrl_item is None: # If the user didn't tell us which controller type they wanted, # and we didn't find a controller item based on existing file/disk, @@ -357,6 +381,14 @@ def validate_elements(vm, file_obj, disk_obj, disk_item, ctrl_item, """Validate any existing file, disk, controller item, and disk item. :raises ValueMismatchError: if the search criteria select a non-unique set. + :param vm: Virtual machine object + :type vm: :class:`~COT.vm_description.VMDescription` + :param object file_obj: Known file object + :param object disk_obj: Known disk object + :param object disk_item: Known disk device object + :param object ctrl_item: Known controller device object + :param str file_id: File identifier string + :param str ctrl_type: Controller type ("ide" or "scsi") """ # Ok, we now have confirmed that we have at most one of each of these # four objects. Now it's time for some sanity checking... @@ -394,7 +426,21 @@ def validate_elements(vm, file_obj, disk_obj, disk_item, ctrl_item, def confirm_elements(vm, ui, file_obj, disk_image, disk_obj, disk_item, disk_type, controller, ctrl_item, subtype): - """Get user confirmation of any risky or unusual operations.""" + """Get user confirmation of any risky or unusual operations. + + :param vm: Virtual machine object + :type vm: :class:`~COT.vm_description.VMDescription` + :param ui: User interface object + :type ui: :class:`~COT.ui_shared.UI` + :param object file_obj: Known file object + :param str disk_image: Filename or path for disk file + :param object disk_obj: Known disk object + :param object disk_item: Known disk device object + :param str disk_type: "harddisk" or "cdrom" + :param str controller: Controller type ("ide" or "scsi") + :param object ctrl_item: Known controller device object + :param str subtype: Controller subtype (such as "virtio") + """ # TODO: more refactoring! if file_obj is not None: ui.confirm_or_die("Replace existing file {0} with {1}?" diff --git a/COT/add_file.py b/COT/add_file.py index 209ca2d..6f9a8fe 100644 --- a/COT/add_file.py +++ b/COT/add_file.py @@ -32,6 +32,9 @@ class COTAddFile(COTSubmodule): """Add a file (such as a README) to the package. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, @@ -43,7 +46,11 @@ class COTAddFile(COTSubmodule): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTAddFile, self).__init__(ui) self._file = None self.file_id = None diff --git a/COT/cli.py b/COT/cli.py index af46f10..13012ca 100644 --- a/COT/cli.py +++ b/COT/cli.py @@ -68,7 +68,7 @@ def formatter(verbosity=logging.INFO): We offer different (more verbose) formatting when debugging is enabled, hence this need. - :param verbosity: Logging level as defined by :mod:`logging`. + :param int verbosity: Logging level as defined by :mod:`logging`. :return: Formatter object for use with :mod:`logging`. :rtype: instance of :class:`colorlog.ColoredFormatter` """ @@ -98,6 +98,9 @@ def formatter(verbosity=logging.INFO): class CLI(UI): """Command-line user interface for COT. + :param int terminal_width: (optional) Set the terminal width for this CLI, + independent of the actual terminal in use. + .. autosummary:: :nosignatures: @@ -116,7 +119,11 @@ class CLI(UI): """ def __init__(self, terminal_width=None): - """Create CLI handler instance.""" + """Create CLI handler instance. + + :param int terminal_width: (optional) Set the terminal width for this + CLI, independent of the actual terminal in use. + """ super(CLI, self).__init__(force=True) # In python 2.7, we want raw_input, but in python 3 we want input. try: @@ -303,7 +310,7 @@ def set_verbosity(self, level): Will call :func:`formatter` and associate the resulting formatter with logging. - :param level: Logging level as defined by :mod:`logging` + :param int level: Logging level as defined by :mod:`logging` """ if not self.handler: self.handler = logging.StreamHandler() @@ -500,6 +507,7 @@ def add_subparser(self, title, in Python 3.x. :param str lookup_prefix: String to prepend to ``title`` and each alias in ``aliases`` for lookup purposes. + :param kwargs: Passed through to :meth:`parent.add_parser` """ # Subparser aliases are only supported by argparse in Python 3.2+ if sys.hexversion >= 0x03020000 and aliases: @@ -522,6 +530,7 @@ def parse_args(self, argv): :param list argv: List of CLI arguments, not including argv0 :return: Parser namespace object + :rtype: :class:`argparse.Namespace` """ # Parse the user input args = self.parser.parse_args(argv) @@ -535,7 +544,12 @@ def parse_args(self, argv): @staticmethod def args_to_dict(args): - """Convert args to a dict and perform any needed cleanup.""" + """Convert args to a dict and perform any needed cleanup. + + :param args: Namespace returned from :meth:`parse_args`. + :type args: :class:`argparse.Namespace` + :rtype: dict + """ arg_dict = vars(args) del arg_dict["_verbosity"] del arg_dict["_force"] @@ -553,8 +567,9 @@ def args_to_dict(args): @staticmethod def set_instance_attributes(arg_dict): - """Pass the CLI argument dictionary to the instance attributes TODO. + """Set attributes of the :attr:`instance` based on the given arg_dict. + :param dict arg_dict: Dictionary of (attribute, value). :raise InvalidInputError: if attributes are not validly set. """ # Set mandatory (CAPITALIZED) args first, then optional args @@ -582,6 +597,7 @@ def main(self, args): * Catches various exceptions and handles them appropriately. :param args: Parser namespace object returned from :func:`parse_args`. + :type args: object :rtype: int :return: Exit code for the COT executable. diff --git a/COT/data_validation.py b/COT/data_validation.py index a746e4c..1983eb8 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -58,7 +58,10 @@ def to_string(obj): - """Get string representation of an object, special-case for XML Element.""" + """Get string representation of an object, special-case for XML Element. + + :param object obj: Object to represent as a string. + """ if ET.iselement(obj): return ET.tostring(obj) else: @@ -76,11 +79,19 @@ def natural_sort(l): :return: Sorted list """ def convert(text): - """Convert number strings to ints, leave other strings as text.""" + """Convert number strings to ints, leave other strings as text. + + :param text: Input to convert + :type text: str, int + :rtype: str, int + """ return int(text) if text.isdigit() else text def alphanum_key(key): - """Split the key into a list of [text, int, text, int, ...].""" + """Split the key into a list of [text, int, text, int, ...]. + + :param str key: String to split. + """ return [convert(c) for c in re.split('([0-9]+)', key)] # Sort based on alphanum_key @@ -91,9 +102,9 @@ def match_or_die(first_label, first, second_label, second): """Make sure "first" and "second" are equal or raise an error. :param str first_label: Descriptive label for :attr:`first` - :param first: First object to compare + :param object first: First object to compare :param str second_label: Descriptive label for :attr:`second` - :param second: Second object to compare + :param object second: Second object to compare :raise ValueMismatchError: if ``first != second`` """ if first != second: @@ -110,7 +121,7 @@ def canonicalize_helper(label, user_input, mappings, re_flags=0): :param str label: Label to use in any error raised :param str user_input: User-provided string :param list mappings: List of ``(expr, canonical)`` pairs for mapping. - :param re_flags: ``re.IGNORECASE``, etc. if desired + :param int re_flags: ``re.IGNORECASE``, etc. if desired :returns: The canonical string :raise ValueUnsupportedError: If no ``expr`` in ``mappings`` matches ``input``. @@ -226,7 +237,7 @@ def mac_address(string): * xx-xx-xx-xx-xx-xx * xxxx.xxxx.xxxx - :param string: String to validate + :param str string: String to validate :raise InvalidInputError: if string is not a valid MAC address :return: Validated string(with leading/trailing whitespace stripped) """ @@ -299,6 +310,8 @@ def non_negative_int(string): """Parser helper function for integer arguments that must be 0 or more. Alias for :func:`validate_int` setting :attr:`minimum` to 0. + + :param str string: String to validate. """ return validate_int(string, minimum=0) @@ -307,6 +320,8 @@ def positive_int(string): """Parser helper function for integer arguments that must be 1 or more. Alias for :func:`validate_int` setting :attr:`minimum` to 1. + + :param str string: String to validate. """ return validate_int(string, minimum=1) @@ -327,16 +342,23 @@ class InvalidInputError(ValueError): class ValueUnsupportedError(InvalidInputError): """An unsupported value was provided. - :ivar value_type: descriptive string - :ivar actual_value: invalid value that was provided - :ivar expected_value: expected (valid) value or values (item or list) + :param str value_type: descriptive string + :param str actual_value: invalid value that was provided + :param expected_value: expected (valid) value or values (item or list) + :type expected_value: str, int, list """ - def __init__(self, value_type, actual, expected): - """Create an instance of this class.""" + def __init__(self, value_type, actual_value, expected_value): + """Create an instance of this class. + + :param str value_type: descriptive string + :param str actual_value: invalid value that was provided + :param expected_value: expected (valid) value or values (item or list) + :type expected_value: str, int, list + """ self.value_type = value_type - self.actual_value = actual - self.expected_value = expected + self.actual_value = actual_value + self.expected_value = expected_value super(ValueUnsupportedError, self).__init__(str(self)) def __str__(self): diff --git a/COT/deploy.py b/COT/deploy.py index d971f92..3b005d8 100644 --- a/COT/deploy.py +++ b/COT/deploy.py @@ -33,13 +33,29 @@ class SerialConnection(object): - """Generic class defining a serial port connection.""" + """Generic class defining a serial port connection. + + :param str kind: Connection type string, possibly in need of munging. + :param str value: Connection value such as '/dev/ttyS0' or '1.1.1.1:80' + :param dict options: Input options dictionary. + """ @classmethod def from_cli_string(cls, cli_string): """Parse a string 'kind:value[,opts]' to build a SerialConnection. Based on the QEMU CLI for serial ports. + + :param str cli_string: String of the form 'kind:value[,opts]' + + :: + + >>> str(SerialConnection.from_cli_string('/dev/ttyS0')) + '' + >>> str(SerialConnection.from_cli_string('tcp::22,server')) + "" + >>> str(SerialConnection.from_cli_string('telnet://1.1.1.1:1111')) + '' """ if cli_string is None: return None @@ -135,7 +151,12 @@ def validate_options(cls, kind, _value, options): return options def __init__(self, kind, value, options): - """Construct a SerialConnection object of the given kind and value.""" + """Construct a SerialConnection object of the given kind and value. + + :param str kind: Connection type string, possibly in need of munging. + :param str value: Connection value such as '/dev/ttyS0' or '1.1.1.1:80' + :param dict options: Input options dictionary. + """ logger.debug("Creating SerialConnection: " "kind: %s, value: %s, options: %s", kind, value, options) @@ -155,9 +176,12 @@ class COTDeploy(COTReadOnlySubmodule): Provides some baseline parameters and input validation that are expected to be common across all concrete subclasses. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: - :attr:`~COTGenericSubmodule.UI`, - :attr:`~COTReadOnlySubmodule.package`, + :attr:`~COT.submodule.COTGenericSubmodule.UI`, + :attr:`~COT.submodule.COTReadOnlySubmodule.package`, Attributes: :attr:`generic_parser`, @@ -173,7 +197,11 @@ class COTDeploy(COTReadOnlySubmodule): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTDeploy, self).__init__(ui) # User inputs self._hypervisor = None @@ -413,3 +441,8 @@ def create_subparser(self): help="Set connectivity for a serial port defined in the OVF. " "This argument may be repeated to specify more port connections. " "Each entry should be structured as 'kind:value[,options]'.") + + +if __name__ == "__main__": + import doctest # pylint: disable=wrong-import-position,wrong-import-order + doctest.testmod() diff --git a/COT/deploy_esxi.py b/COT/deploy_esxi.py index fffbcc0..40f31f7 100644 --- a/COT/deploy_esxi.py +++ b/COT/deploy_esxi.py @@ -52,17 +52,29 @@ class SmarterConnection(SmartConnection): - """A smarter version of pyVmomi's SmartConnection context manager.""" + """A smarter version of pyVmomi's SmartConnection context manager. - def __init__(self, ui, server, username, password, port=443): - """Create a connection to the given server.""" + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + + For the other parameters, see :class:`pyVim.connect.SmartConnection` + """ + + def __init__(self, ui, host, user, pwd, port=443): + """Create a connection to the given server. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + + For the other parameters, see :class:`pyVim.connect.SmartConnection` + """ self.UI = ui - self.server = server - self.username = username - self.password = password + self.server = host + self.username = user + self.password = pwd self.port = port - super(SmarterConnection, self).__init__(host=server, user=username, - pwd=password, port=port) + super(SmarterConnection, self).__init__(host=host, user=user, + pwd=pwd, port=port) def __enter__(self): """Establish a connection and use it as the context manager object. @@ -100,7 +112,10 @@ def __enter__(self): def __exit__(self, # pylint: disable=arguments-differ exc_type, exc_value, trace): - """Disconnect from the server.""" + """Disconnect from the server. + + For the parameters, see :module:`contextlib`. + """ super(SmarterConnection, self).__exit__() if exc_type is not None: logger.error("Session failed - %s", exc_value) @@ -144,7 +159,13 @@ def unwrap_connection_error(outer_e): def get_object_from_connection(conn, vimtype, name): - """Look up an object by name.""" + """Look up an object by name. + + :param conn: Connection to ESXi. + :type conn: :class:`SmarterConnection` + :param object vimtype: currently only `vim.VirtualMachine`` + :param str name: Name of the object to look up. + """ obj = None content = conn.RetrieveContent() container = content.viewManager.CreateContainerView( @@ -157,10 +178,20 @@ def get_object_from_connection(conn, vimtype, name): class PyVmomiVMReconfigSpec(object): - """Context manager for reconfiguring an ESXi VM using PyVmomi.""" + """Context manager for reconfiguring an ESXi VM using PyVmomi. + + :param conn: Connection to ESXi. + :type conn: :class:`SmarterConnection` + :param str vm_name: Virtual machine name. + """ def __init__(self, conn, vm_name): - """Use the given name to look up a VM using the given connection.""" + """Use the given name to look up a VM using the given connection. + + :param conn: Connection to ESXi. + :type conn: :class:`SmarterConnection` + :param str name: Virtual machine name. + """ self.vm = get_object_from_connection(conn, vim.VirtualMachine, vm_name) assert self.vm self.spec = vim.vm.ConfigSpec() @@ -170,7 +201,10 @@ def __enter__(self): return self.spec def __exit__(self, exc_type, exc_value, trace): - """If the block exited cleanly, apply the ConfigSpec to the VM.""" + """If the block exited cleanly, apply the ConfigSpec to the VM. + + For the parameters, see :module:`contextlib`. + """ # Did we exit cleanly? if exc_type is None: logger.verbose("Reconfiguring VM...") @@ -180,20 +214,23 @@ def __exit__(self, exc_type, exc_value, trace): class COTDeployESXi(COTDeploy): """Submodule for deploying VMs on ESXi and VMware vCenter/vSphere. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: - :attr:`~COTGenericSubmodule.UI`, - :attr:`~COTReadOnlySubmodule.package`, - :attr:`generic_parser`, - :attr:`parser`, - :attr:`subparsers`, - :attr:`hypervisor`, - :attr:`configuration`, - :attr:`username`, - :attr:`password`, - :attr:`power_on`, - :attr:`vm_name`, - :attr:`network_map` - :attr:`serial_connection` + :attr:`~COT.submodule.COTGenericSubmodule.UI`, + :attr:`~COT.submodule.COTReadOnlySubmodule.package`, + :attr:`~COT.deploy.COTDeploy.generic_parser`, + :attr:`~COT.deploy.COTDeploy.parser`, + :attr:`~COT.deploy.COTDeploy.subparsers`, + :attr:`~COT.deploy.COTDeploy.hypervisor`, + :attr:`~COT.deploy.COTDeploy.configuration`, + :attr:`~COT.deploy.COTDeploy.username`, + :attr:`~COT.deploy.COTDeploy.password`, + :attr:`~COT.deploy.COTDeploy.power_on`, + :attr:`~COT.deploy.COTDeploy.vm_name`, + :attr:`~COT.deploy.COTDeploy.network_map` + :attr:`~COT.deploy.COTDeploy.serial_connection` Attributes: :attr:`locator`, @@ -202,7 +239,11 @@ class COTDeployESXi(COTDeploy): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTDeployESXi, self).__init__(ui) self.datastore = None """ESXi datastore to deploy to.""" @@ -241,7 +282,10 @@ def locator(self, value): @COTDeploy.serial_connection.setter # pylint: disable=no-member def serial_connection(self, value): - """Override parent property setter to add ESXi validation.""" + """Override parent property setter to add ESXi validation. + + For the parameters, see :meth:`~COTDeploy.serial_connection` + """ if len(value) > 4: raise ValueUnsupportedError( 'serial port connection list', value, @@ -375,7 +419,10 @@ def run(self): # TODO: only now power on VM if power_on was requested def fixup_serial_ports(self, serial_count): - """Use PyVmomi to create and configure serial ports for the new VM.""" + """Use PyVmomi to create and configure serial ports for the new VM. + + :param int serial_count: Number of serial ports desired. + """ if serial_count > len(self.serial_connection): logger.warning("No serial connectivity information is " "available for %d serial port(s) - " diff --git a/COT/edit_hardware.py b/COT/edit_hardware.py index d74b6c6..2dbf0a4 100644 --- a/COT/edit_hardware.py +++ b/COT/edit_hardware.py @@ -52,6 +52,9 @@ class COTEditHardware(COTSubmodule): """Edit hardware information (CPUs, RAM, NICs, etc.). + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, @@ -76,7 +79,11 @@ class COTEditHardware(COTSubmodule): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTEditHardware, self).__init__(ui) self.profiles = None """Configuration profile(s) to edit.""" diff --git a/COT/edit_product.py b/COT/edit_product.py index c2277ca..46940a3 100644 --- a/COT/edit_product.py +++ b/COT/edit_product.py @@ -34,6 +34,9 @@ class COTEditProduct(COTSubmodule): """Edit product, vendor, and version information strings. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, @@ -51,7 +54,11 @@ class COTEditProduct(COTSubmodule): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTEditProduct, self).__init__(ui) self.product_class = None """Product class identifier.""" diff --git a/COT/edit_properties.py b/COT/edit_properties.py index e21816a..87132d3 100644 --- a/COT/edit_properties.py +++ b/COT/edit_properties.py @@ -37,6 +37,9 @@ class COTEditProperties(COTSubmodule): """Edit OVF environment XML properties. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, @@ -49,7 +52,11 @@ class COTEditProperties(COTSubmodule): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTEditProperties, self).__init__(ui) self._config_file = None self._properties = {} diff --git a/COT/file_reference.py b/COT/file_reference.py index d77d7e4..bb277aa 100644 --- a/COT/file_reference.py +++ b/COT/file_reference.py @@ -27,10 +27,29 @@ class FileOnDisk(object): - """Wrapper for a 'real' file on disk.""" + """Wrapper for a 'real' file on disk. + + :param str file_path: File path or directory path + :param str filename: If specified, file_path is considered to be + a directory containing this filename. If not specified, the + final element in file_path is considered the filename. + """ def __init__(self, file_path, filename=None): - """Create a reference to a file on disk.""" + """Create a reference to a file on disk. + + :param str file_path: File path or directory path + :param str filename: If specified, file_path is considered to be + a directory containing this filename. If not specified, the + final element in file_path is considered the filename. + + :: + + >>> a = FileOnDisk('/etc/resolv.conf') + >>> b = FileOnDisk('/etc', 'resolv.conf') + >>> a == b + True + """ if filename is None: self.file_path = file_path self.filename = os.path.basename(file_path) @@ -41,6 +60,22 @@ def __init__(self, file_path, filename=None): raise IOError("File {0} does not exist!".format(self.file_path)) self.obj = None + def __eq__(self, other): + """FileOnDisk instances are equal if they point to the same path. + + No attempt is made to check file equivalence, symlinks, etc. + + :param object other: Other object to compare against + """ + return type(other) is type(self) and self.file_path == other.file_path + + def __ne__(self, other): + """FileOnDisk instances are not equal if they have different paths. + + :param object other: Other object to compare against + """ + return not self.__eq__(other) + def exists(self): """Check whether the file exists on disk.""" return os.path.exists(self.file_path) @@ -50,7 +85,10 @@ def size(self): return os.path.getsize(self.file_path) def open(self, mode): - """Open the file and return a reference to the file object.""" + """Open the file and return a reference to the file object. + + :param str mode: Mode such as 'r', 'w', 'a', 'w+', etc. + """ self.obj = open(self.file_path, mode) return self.obj @@ -59,24 +97,39 @@ def close(self): self.obj.close() def copy_to(self, dest_dir): - """Copy this file to the given destination directory.""" + """Copy this file to the given destination directory. + + :param str dest_dir: Destination directory or filename. + """ if self.file_path == os.path.join(dest_dir, self.filename): return logger.info("Copying %s to %s", self.file_path, dest_dir) shutil.copy(self.file_path, dest_dir) def add_to_archive(self, tarf): - """Copy this file into the given tarfile object.""" + """Copy this file into the given tarfile object. + + :param tarf: Add this file to that archive. + :type tarf: :class:`tarfile.TarFile` + """ logger.info("Adding %s to TAR file as %s", self.file_path, self.filename) tarf.add(self.file_path, self.filename) class FileInTAR(object): - """Wrapper for a file inside a TAR archive or OVA.""" + """Wrapper for a file inside a TAR archive or OVA. + + :param str tarfile_path: Path to TAR archive to read + :param str filename: File name in the TAR archive. + """ def __init__(self, tarfile_path, filename): - """Create a reference to a file contained in a TAR archive.""" + """Create a reference to a file contained in a TAR archive. + + :param str tarfile_path: Path to TAR archive to read + :param str filename: File name in the TAR archive. + """ if not tarfile.is_tarfile(tarfile_path): raise IOError("{0} is not a valid TAR file.".format(tarfile_path)) self.tarfile_path = tarfile_path @@ -88,6 +141,25 @@ def __init__(self, tarfile_path, filename): self.tarf = None self.obj = None + def __eq__(self, other): + """FileInTAR are equal if they have the same filename and tarfile. + + No attempt is made to check file equivalence, symlinks, etc. + + :param object other: Other object to compare against + """ + if type(other) is type(self): + return (self.tarfile_path == other.tarfile_path and + self.filename == other.filename) + return False + + def __ne__(self, other): + """FileInTar are not equal if they have different paths or names. + + :param object other: Other object to compare against + """ + return not self.__eq__(other) + def exists(self): """Check whether the file exists in the TAR archive.""" with closing(tarfile.open(self.tarfile_path, 'r')) as tarf: @@ -103,7 +175,10 @@ def size(self): return tarf.getmember(self.filename).size def open(self, mode): - """Open the TAR and return a reference to the relevant file object.""" + """Open the TAR and return a reference to the relevant file object. + + :param str mode: Only 'r' and 'rb' modes are supported. + """ # We can only extract a file object from a TAR file in read mode. if mode != 'r' and mode != 'rb': raise ValueError("FileInTar.open() only supports 'r'/'rb' mode") @@ -121,14 +196,21 @@ def close(self): self.obj = None def copy_to(self, dest_dir): - """Extract this file to the given destination directory.""" + """Extract this file to the given destination directory. + + :param str dest_dir: Destination directory or filename. + """ with closing(tarfile.open(self.tarfile_path, 'r')) as tarf: logger.info("Extracting %s from %s to %s", self.filename, self.tarfile_path, dest_dir) tarf.extract(self.filename, dest_dir) def add_to_archive(self, tarf): - """Copy this file into the given tarfile object.""" + """Copy this file into the given tarfile object. + + :param tarf: Add this file to that archive. + :type tarf: :class:`tarfile.TarFile` + """ self.open('r') try: logger.info("Copying %s directly from %s to TAR file", @@ -136,3 +218,8 @@ def add_to_archive(self, tarf): tarf.addfile(self.tarf.getmember(self.filename), self.obj) finally: self.close() + + +if __name__ == "__main__": + import doctest # pylint: disable=wrong-import-position,wrong-import-order + doctest.testmod() diff --git a/COT/help.py b/COT/help.py index 4161aea..4ca351f 100644 --- a/COT/help.py +++ b/COT/help.py @@ -27,6 +27,9 @@ class COTHelp(COTGenericSubmodule): """Provide 'help ' syntax. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: :attr:`~COTGenericSubmodule.UI` @@ -35,7 +38,11 @@ class COTHelp(COTGenericSubmodule): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTHelp, self).__init__(ui) self._subcommand = None diff --git a/COT/helpers/api.py b/COT/helpers/api.py index 7c7a12c..71fa2b1 100644 --- a/COT/helpers/api.py +++ b/COT/helpers/api.py @@ -79,6 +79,7 @@ def TemporaryDirectory(suffix='', # noqa: N802 """Create a temporary directory and make sure it's deleted later. Reimplementation of Python 3's ``tempfile.TemporaryDirectory``. + For the parameters, see :class:`tempfile.TemporaryDirectory`. """ tempdir = tempfile.mkdtemp(suffix, prefix, dirpath) try: diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 9f12163..a47bc44 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -37,7 +37,10 @@ def guess_file_format_from_path(file_path): - """Guess the preferred file format based on file path/extension.""" + """Guess the preferred file format based on file path/extension. + + :param str file_path: Filename or file path. + """ file_format = os.path.splitext(file_path)[1][1:] if not file_format: raise RuntimeError( @@ -60,6 +63,12 @@ class HelperError(EnvironmentError): class Helper(object): """A provider of a non-Python helper program. + :param str name: Name of helper executable + :param list version_args: Args to pass to the helper to + get its version. Defaults to ``['--version']`` if unset. + :param str version_regexp: Regexp to get the version number from + the output of the command. + **Class Properties** .. autosummary:: @@ -119,7 +128,10 @@ def confirm(cls, _prompt): @staticmethod def find_executable(name): - """Wrapper for :func:`distutils.spawn.find_executable`.""" + """Wrapper for :func:`distutils.spawn.find_executable`. + + :param str name: Executable name. + """ return distutils.spawn.find_executable(name) _apt_updated = False @@ -127,7 +139,10 @@ def find_executable(name): @classmethod def apt_install(cls, package): - """Try to use ``apt-get`` to install a package.""" + """Try to use ``apt-get`` to install a package. + + :param str package: Package name. + """ if not cls.PACKAGE_MANAGERS['apt-get']: return False # check if it's already installed @@ -146,7 +161,10 @@ def apt_install(cls, package): @classmethod def port_install(cls, package): - """Try to use ``port`` to install a package.""" + """Try to use ``port`` to install a package. + + :param str package: Package name. + """ if not cls.PACKAGE_MANAGERS['port']: return False if not cls._port_updated: @@ -157,7 +175,10 @@ def port_install(cls, package): @classmethod def yum_install(cls, package): - """Try to use ``yum`` to install a package.""" + """Try to use ``yum`` to install a package. + + :param str package: Package name. + """ if not cls.PACKAGE_MANAGERS['yum']: return False cls._check_call(['yum', '--quiet', 'install', package], @@ -217,10 +238,10 @@ def __init__(self, name, version_args=None, version_regexp="([0-9.]+"): """Initializer. - :param name: Name of helper executable + :param str name: Name of helper executable :param list version_args: Args to pass to the helper to get its version. Defaults to ``['--version']`` if unset. - :param version_regexp: Regexp to get the version number from + :param str version_regexp: Regexp to get the version number from the output of the command. """ self._name = name diff --git a/COT/info.py b/COT/info.py index 2b00386..8483eb4 100644 --- a/COT/info.py +++ b/COT/info.py @@ -31,6 +31,9 @@ class COTInfo(COTGenericSubmodule): """Display VM information string. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: :attr:`~COTGenericSubmodule.UI` @@ -40,7 +43,11 @@ class COTInfo(COTGenericSubmodule): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTInfo, self).__init__(ui) self._package_list = None self._verbosity = None diff --git a/COT/inject_config.py b/COT/inject_config.py index 40df48c..a920235 100644 --- a/COT/inject_config.py +++ b/COT/inject_config.py @@ -31,6 +31,9 @@ class COTInjectConfig(COTSubmodule): """Wrap configuration file(s) into a disk image embedded into the VM. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, @@ -42,7 +45,11 @@ class COTInjectConfig(COTSubmodule): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTInjectConfig, self).__init__(ui) self._config_file = None self._secondary_config_file = None diff --git a/COT/install_helpers.py b/COT/install_helpers.py index 33631d5..8c39ee4 100644 --- a/COT/install_helpers.py +++ b/COT/install_helpers.py @@ -53,6 +53,7 @@ def guess_manpath(): def verify_manpages(man_dir): """Verify installation of COT's manual pages. + :param str man_dir: Base directory where manpages should be found. :return: (result, message) """ for f in resource_listdir("COT", "docs/man"): @@ -79,6 +80,8 @@ def verify_manpages(man_dir): def _install_manpage(src_path, man_dir): """Install the given manual page for COT. + :param str src_path: Path to manual page file. + :param str man_dir: Base directory where page should be installed. :return: (page_previously_installed, page_updated) :raise IOError: if installation fails under some circumstances :raise OSError: if installation fails under other circumstances @@ -104,6 +107,7 @@ def _install_manpage(src_path, man_dir): def install_manpages(man_dir): """Install COT's manual pages. + :param str man_dir: Base directory where manpages should be installed. :return: (result, message) """ installed_any = False @@ -130,10 +134,23 @@ def install_manpages(man_dir): class COTInstallHelpers(COTGenericSubmodule): - """Install all helper tools that COT requires.""" + """Install all helper tools that COT requires. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + + Inherited attributes: + :attr:`~COTGenericSubmodule.UI`, + :attr:`~COTSubmodule.package`, + :attr:`~COTSubmodule.output` + """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTInstallHelpers, self).__init__(ui) self.ignore_errors = False self.verify_only = False @@ -141,6 +158,8 @@ def __init__(self, ui): def install_helper(self, helper): """Install the given helper module. + :param helper: Helper module to install. + :type helper: :class:`~COT.helpers.helper.Helper` :return: (result, message) """ if helper.path: diff --git a/COT/ovf/hardware.py b/COT/ovf/hardware.py index f9acbfb..f0d7094 100644 --- a/COT/ovf/hardware.py +++ b/COT/ovf/hardware.py @@ -40,18 +40,23 @@ class OVFHardwareDataError(Exception): class OVFHardware(object): - """Helper class for :class:`OVF`. + """Helper class for :class:`~COT.ovf.ovf.OVF`. Represents all hardware items defined by this OVF; i.e., the contents of all Items in the VirtualHardwareSection. - Fundamentally it's just a dict of :class:`OVFItem` objects with a bunch of - helper methods. + Fundamentally it's just a dict of :class:`~COT.ovf.item.OVFItem` objects + with a bunch of helper methods. + + :param ovf: OVF instance to extract hardware information from. + :type ovf: :class:`~COT.ovf.ovf.OVF` """ def __init__(self, ovf): """Construct an OVFHardware object describing all Items in the OVF. + :param ovf: OVF instance to extract hardware information from. + :type ovf: :class:`~COT.ovf.ovf.OVF` :raise OVFHardwareDataError: if any data errors are seen """ self.ovf = ovf @@ -172,7 +177,11 @@ def new_item(self, resource_type, profile_list=None): return (instance, ovfitem) def delete_item(self, item): - """Delete the given :class:`OVFItem`.""" + """Delete the given Item from the hardware. + + :param item: Item to delete + :type item: :class:`~COT.ovf.item.OVFItem` + """ instance = item.get_value(self.ovf.INSTANCE_ID) if self.item_dict[instance] == item: del self.item_dict[instance] @@ -197,7 +206,15 @@ def clone_item(self, parent_item, profile_list): return (instance, ovfitem) def item_match(self, item, resource_type, properties, profile_list): - """Check whether the given item matches the given filters.""" + """Check whether the given item matches the given filters. + + :param item: Item to validate + :type item: :class:`~COT.ovf.item.OVFItem` + :param str resource_type: Resource type string like 'scsi' or 'serial' + :param properties: Property values to match + :type properties: dict[property, value] + :param list profile_list: List of profiles to filter on + """ if resource_type and (self.ovf.RES_MAP[resource_type] != item.get_value(self.ovf.RESOURCE_TYPE)): return False @@ -292,6 +309,10 @@ def update_existing_item_count_per_profile(self, resource_type, Helper method for :meth:`set_item_count_per_profile`. + :param str resource_type: 'cpu', 'harddisk', etc. + :param int count: Desired number of items + :param list profile_list: List of profiles to filter on + (default: apply across all profiles) :return: (count_dict, items_to_add, last_item) """ count_dict = self.get_item_count_per_profile(resource_type, @@ -333,6 +354,13 @@ def _update_cloned_item(self, new_item, new_item_profiles, item_count): """Update a cloned item to make it distinct from its parent. Helper method for :meth:`set_item_count_per_profile`. + + :param new_item: Newly cloned Item + :type new_item: :class:`~COT.ovf.item.OVFItem` + :param list new_item_profiles: Profiles new_item should belong to + :param int item_count: How many Items of this type (including this + item) now exist. Used with + :meth:`COT.platform.GenericPlatform.guess_nic_name` """ resource_type = self.ovf.get_type_from_device(new_item) address = new_item.get(self.ovf.ADDRESS) diff --git a/COT/ovf/item.py b/COT/ovf/item.py index 31c7695..72b3483 100644 --- a/COT/ovf/item.py +++ b/COT/ovf/item.py @@ -48,7 +48,10 @@ def list_union(*lists): - """Get union of lists.""" + """Get union of lists. + + :param list lists: List of lists to unify. + """ result = [] for l in lists: result.extend([x for x in l if x not in result]) @@ -70,6 +73,10 @@ class OVFItem(object): * a dict of ``Item`` properties (indexed by element name) * each of which is a dict of sets of profiles (indexed by element value) + + :param OVF ovf: OVF instance that owns the Item (optional) + :param item: 'Item' element (optional) + :type item: :class:`xml.etree.ElementTree.Element` """ # Magic strings @@ -80,7 +87,8 @@ def __init__(self, ovf, item=None): """Create a new OVFItem with contents based on the given Item element. :param OVF ovf: OVF instance that owns the Item (optional) - :param xml.etree.ElementTree.Element item: 'Item' element (optional) + :param item: 'Item' element (optional) + :type item: :class:`xml.etree.ElementTree.Element` """ self.ovf = ovf if ovf is not None: @@ -105,7 +113,10 @@ def __str__(self): return ret def __getattr__(self, name): - """Transparently pass attribute lookups off to OVF/OVFNameHelper.""" + """Transparently pass attribute lookups off to OVF/OVFNameHelper. + + :param str name: Attribute name. + """ # Don't pass 'special' attributes through to the helper if re.match(r"^__", name): raise AttributeError("'OVFItem' object has no attribute '{0}'" @@ -119,15 +130,27 @@ def property_names(self): return list(self.properties.keys()) def property_values(self, name): - """Get list of values known for a given property name.""" + """Get list of values known for a given property name. + + :param str name: Property name. + """ return list(self.properties[name].keys()) def property_profiles(self, name, value): - """Get set of profiles associated with a property name and value.""" + """Get set of profiles associated with a property name and value. + + :param str name: Property name. + :param value: Property value of interest. + :type value: str, tuple + """ return self.properties[name][value] def all_profiles(self, name, default=None): - """Superset of all profiles for which this name has a value.""" + """Superset of all profiles for which this name has a value. + + :param str name: Property name. + :param object default: Default value to return if there are no matches + """ value_dict = self.properties.get(name, None) if not value_dict: return default @@ -136,7 +159,8 @@ def all_profiles(self, name, default=None): def add_item(self, item): """Add the given ``Item`` element to this OVFItem. - :param xml.etree.ElementTree.Element item: XML ``Item`` element + :param item: XML ``Item`` element + :type item: :class:`xml.etree.ElementTree.Element` :raise OVFItemDataError: if the new Item conflicts with existing data already in the OVFItem. """ @@ -202,11 +226,17 @@ def add_item(self, item): self.validate() def value_add_wildcards(self, name, value, profiles): - """Add wildcard placeholders to a string that may need updating.""" - # If the ElementName or Description references the VirtualQuantity, - # Connection, or ResourceSubType, replace that reference with a - # placeholder that we can regenerate at output time. That way, if the - # VirtualQuantity or ResourceSubType changes, these can change too. + """Add wildcard placeholders to a string that may need updating. + + If the ElementName or Description references the VirtualQuantity, + Connection, or ResourceSubType, replace that reference with a + placeholder that we can regenerate at output time. That way, if the + VirtualQuantity or ResourceSubType changes, these can change too. + + :param str name: Property name + :param str value: Value to add wildcards to. + :param list profiles: Profiles to which this (name, value) applies. + """ if name == self.ELEMENT_NAME or name == self.ITEM_DESCRIPTION: vq_val = self.get_value(self.VIRTUAL_QUANTITY, profiles) if vq_val is not None: @@ -227,7 +257,12 @@ def value_add_wildcards(self, name, value, profiles): return value def value_replace_wildcards(self, name, value, profiles): - """Replace wildcards with actual values.""" + """Replace wildcards with actual values. + + :param str name: Property name + :param str value: Value to replace wildcards from. + :param list profiles: Profiles to which this (name, value) applies. + """ if not value: return value if name == self.ELEMENT_NAME or name == self.ITEM_DESCRIPTION: @@ -250,7 +285,12 @@ def value_replace_wildcards(self, name, value, profiles): return value def _set_new_property(self, name, value, profiles): - """Helper for :meth:`set_property`. Create a new property entry.""" + """Helper for :meth:`set_property`. Create a new property entry. + + :param str name: Property name + :param str value: Value to store for this property. + :param list profiles: Profiles to which this (name, value) applies. + """ if not value: return @@ -261,7 +301,13 @@ def _set_new_property(self, name, value, profiles): self.modified = True def _set_existing_property(self, name, value, profiles, overwrite): - """Helper for :meth:`set_property`. Update an existing property.""" + """Helper for :meth:`set_property`. Update an existing property. + + :param str name: Property name + :param str value: Value to store for this property. + :param list profiles: Profiles to which this (name, value) applies. + :param bool overwrite: Whether to permit overwriting existing values. + """ for (known_value, profile_set) in list(self.properties[name].items()): if not overwrite and profile_set.intersection(profiles): raise OVFItemDataError( diff --git a/COT/ovf/name_helper.py b/COT/ovf/name_helper.py index c843704..13380f9 100644 --- a/COT/ovf/name_helper.py +++ b/COT/ovf/name_helper.py @@ -51,9 +51,18 @@ def name_helper(version): class _Tag(object): - """Helper class representing a named XML namespace and associated tag.""" + """Helper class representing a named XML namespace and associated tag. + + :param str namespace_name: XML namespace name + :param str tag: XML tag + """ def __init__(self, namespace_name, tag): + """Store namespace name and tag. + + :param str namespace_name: XML namespace name + :param str tag: XML tag + """ self.namespace_name = namespace_name.upper() self.tag = tag @@ -250,7 +259,10 @@ class OVFNameHelper1(object): ) def __getattr__(self, name): - """Transparently pass attribute lookups to _raw and _cache.""" + """Transparently pass attribute lookups to _raw and _cache. + + :param str name: Attribute name to look up. + """ if name in self._item_children: return self._item_children[name] if name not in self._cache: @@ -305,7 +317,10 @@ def __init__(self): self.VIRTUAL_HW_SECTION_ATTRIB = {} def namespace_for_item_tag(self, tag): - """Get the XML namespace for the given item tag.""" + """Get the XML namespace for the given item tag. + + :param str tag: Un-namespaced XML tag. + """ if tag == self.ITEM: return self.RASD elif tag == self.STORAGE_ITEM: @@ -315,7 +330,10 @@ def namespace_for_item_tag(self, tag): return None def namespace_for_resource_type(self, resource_type): - """Get the XML namespace for the given ResourceType.""" + """Get the XML namespace for the given ResourceType. + + :param str resource_type: ResourceType value string. + """ if resource_type == self.RES_MAP['ethernet']: return self.EPASD elif (resource_type == self.RES_MAP['harddisk'] or @@ -325,7 +343,10 @@ def namespace_for_resource_type(self, resource_type): return self.RASD def item_tag_for_namespace(self, ns): - """Get the item tag for the given XML namespace.""" + """Get the Item tag for the given XML namespace. + + :param str ns: XML namespace + """ if ns == self.RASD: return self.ITEM elif ns == self.SASD: diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index 7bf2910..7097a75 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -177,6 +177,12 @@ def byte_string(byte_value, base_shift=0): class OVF(VMDescription, XML): """Representation of the contents of an OVF or OVA. + :param str input_file: Data file to read in. + :param str output_file: File name to write to. If this VM is read-only, + (there will never be an output file) this value should be ``None``; + if the output filename is not yet known, use ``""`` and subsequently + set :attr:`output_file` when it is determined. + **Properties** .. autosummary:: @@ -209,6 +215,7 @@ def detect_type_from_name(filename): Does not check file contents, as the given filename may not yet exist. + :param str filename: File name/path :return: '.ovf' or '.ova' :raise ValueUnsupportedError: if filename doesn't match ovf/ova """ @@ -240,6 +247,8 @@ def _ovf_descriptor_from_name(self, input_file): 1. The file may be an OVF descriptor itself. 2. The file may be an OVA, in which case we need to untar it and return the path to the extracted OVF descriptor. + + :param str input_file: Path to an OVF descriptor or OVA file. """ extension = self.detect_type_from_name(input_file) if extension == '.ova': @@ -475,7 +484,12 @@ def validate_hardware(self): plat = self.platform def _validate_helper(label, fn, *args): - """Call validation function, catch errors and warn user instead.""" + """Call validation function, catch errors and warn user instead. + + :param str label: Label to prepend to any warning messages + :param function fn: Validation function to call. + :param args: Arguments to validation function. + """ try: fn(*args) return True @@ -719,7 +733,10 @@ def application_url(self, app_url_string): self.set_product_section_child(self.APPLICATION_URL, app_url_string) def __getattr__(self, name): - """Transparently pass attribute lookups off to name_helper.""" + """Transparently pass attribute lookups off to name_helper. + + :param str name: Attribute being looked up. + """ # Don't pass 'special' attributes through to the helper if re.match(r"^__", name): raise AttributeError("'OVF' object has no attribute '{0}'" @@ -855,7 +872,10 @@ def validate_and_update_networks(self): self.network_section = None def _info_string_header(self, width): - """Generate OVF/OVA file header for :meth:`info_string`.""" + """Generate OVF/OVA file header for :meth:`info_string`. + + :param int width: Line length to wrap to where possible. + """ str_list = [] str_list.append('-' * width) str_list.append(self.input_file) @@ -866,7 +886,13 @@ def _info_string_header(self, width): return '\n'.join(str_list) def _info_string_product(self, verbosity_option, wrapper): - """Generate product information as part of :meth:`info_string`.""" + """Generate product information as part of :meth:`info_string`. + + :param str verbosity_option: ``'brief'``, ``None`` (default), + or ``'verbose'`` + :param wrapper: Helper object for wrapping text lines if needed. + :type wrapper: :class:`textwrap.TextWrapper` + """ if ((not any([self.product, self.vendor, self.version_short])) and (verbosity_option == 'brief' or not any([ self.product_url, self.vendor_url, self.version_long]))): @@ -894,7 +920,11 @@ def _info_string_product(self, verbosity_option, wrapper): return "\n".join(str_list) def _info_string_annotation(self, wrapper): - """Generate annotation information as part of :meth:`info_string`.""" + """Generate annotation information as part of :meth:`info_string`. + + :param wrapper: Helper object for wrapping text lines if needed. + :type wrapper: :class:`textwrap.TextWrapper` + """ if self.annotation_section is None: return None ann = self.annotation_section.find(self.ANNOTATION) @@ -915,7 +945,13 @@ def _info_string_annotation(self, wrapper): return "\n".join(str_list) def _info_string_eula(self, verbosity_option, wrapper): - """Generate EULA information as part of :meth:`info_string`.""" + """Generate EULA information as part of :meth:`info_string`. + + :param str verbosity_option: ``'brief'``, ``None`` (default), + or ``'verbose'`` + :param wrapper: Helper object for wrapping text lines if needed. + :type wrapper: :class:`textwrap.TextWrapper` + """ # An OVF may have zero, one, or more eula_header = False str_list = [] @@ -962,6 +998,8 @@ def _info_strings_for_file(self, file_obj): Helper for :meth:`_info_string_files_disks`. + :param file_obj: File to inspect + :type file_obj: :class:`xml.etree.ElementTree.Element` :return: (file_id, file_size, disk_id, disk_capacity, device_info) """ # FILE_SIZE is optional @@ -991,7 +1029,12 @@ def _info_strings_for_file(self, file_obj): device_str) def _info_string_files_disks(self, width, verbosity_option): - """Describe files and disks as part of :meth:`info_string`.""" + """Describe files and disks as part of :meth:`info_string`. + + :param int width: Line length to wrap to where possible. + :param str verbosity_option: ``'brief'``, ``None`` (default), + or ``'verbose'`` + """ file_list = self.references.findall(self.FILE) disk_list = (self.disk_section.findall(self.DISK) if self.disk_section is not None else []) @@ -1048,7 +1091,11 @@ def _info_string_files_disks(self, width, verbosity_option): return "\n".join(str_list) def _info_string_hardware(self, wrapper): - """Describe hardware subtypes as part of :meth:`info_string`.""" + """Describe hardware subtypes as part of :meth:`info_string`. + + :param wrapper: Helper object for wrapping text lines if needed. + :type wrapper: :class:`textwrap.TextWrapper` + """ virtual_system_types = self.system_types scsi_subtypes = list_union( *[scsi_ctrl.get_all_values(self.RESOURCE_SUB_TYPE) for @@ -1079,11 +1126,18 @@ def _info_string_hardware(self, wrapper): return "\n".join(str_list) return None - def _info_string_networks(self, width, verbosity_option, wrapper): - """Describe virtual networks as part of :meth:`info_string`.""" + def _info_string_networks(self, verbosity_option, wrapper): + """Describe virtual networks as part of :meth:`info_string`. + + :param str verbosity_option: ``'brief'``, ``None`` (default), + or ``'verbose'`` + :param wrapper: Helper object for wrapping text lines if needed. + :type wrapper: :class:`textwrap.TextWrapper` + """ if self.network_section is None: return None str_list = ["Networks:"] + width = wrapper.width names = [] descs = [] for network in self.network_section.findall(self.NETWORK): @@ -1110,7 +1164,13 @@ def _info_string_networks(self, width, verbosity_option, wrapper): return "\n".join(str_list) def _info_string_nics(self, verbosity_option, wrapper): - """Describe NICs as part of :meth:`info_string`.""" + """Describe NICs as part of :meth:`info_string`. + + :param str verbosity_option: ``'brief'``, ``None`` (default), + or ``'verbose'`` + :param wrapper: Helper object for wrapping text lines if needed. + :type wrapper: :class:`textwrap.TextWrapper` + """ if verbosity_option == 'brief': return None nics = self.hardware.find_all_items('ethernet') @@ -1141,7 +1201,11 @@ def _info_string_nics(self, verbosity_option, wrapper): return "\n".join(str_list) def _info_string_environment(self, wrapper): - """Describe environment for :meth:`info_string`.""" + """Describe environment for :meth:`info_string`. + + :param wrapper: Helper object for wrapping text lines if needed. + :type wrapper: :class:`textwrap.TextWrapper` + """ if not self.environment_transports: return None str_list = ["Environment:"] @@ -1152,8 +1216,14 @@ def _info_string_environment(self, wrapper): .format(" ".join(self.environment_transports)))) return "\n".join(str_list) - def _info_string_properties(self, width, verbosity_option, wrapper): - """Describe config properties for :meth:`info_string`.""" + def _info_string_properties(self, verbosity_option, wrapper): + """Describe config properties for :meth:`info_string`. + + :param str verbosity_option: ``'brief'``, ``None`` (default), + or ``'verbose'`` + :param wrapper: Helper object for wrapping text lines if needed. + :type wrapper: :class:`textwrap.TextWrapper` + """ properties = self.environment_properties if not properties: return None @@ -1161,6 +1231,7 @@ def _info_string_properties(self, width, verbosity_option, wrapper): max_key = 2 + max([len(str(ph['key'])) for ph in properties]) max_label = max([len(str(ph['label'])) for ph in properties]) max_value = max([len(str(ph['value'])) for ph in properties]) + width = wrapper.width if all(ph['label'] for ph in properties): max_width = max_label else: @@ -1229,10 +1300,10 @@ def info_string(self, width=79, verbosity_option=None): self._info_string_files_disks(width, verbosity_option), self._info_string_hardware(wrapper), self.profile_info_string(width, verbosity_option), - self._info_string_networks(width, verbosity_option, wrapper), + self._info_string_networks(verbosity_option, wrapper), self._info_string_nics(verbosity_option, wrapper), self._info_string_environment(wrapper), - self._info_string_properties(width, verbosity_option, wrapper) + self._info_string_properties(verbosity_option, wrapper) ] # Discard empty sections section_list = [s for s in section_list if s] @@ -1382,7 +1453,10 @@ def create_configuration_profile(self, pid, label, description): self._configuration_profiles = None def delete_configuration_profile(self, profile): - """Delete the profile with the given ID.""" + """Delete the profile with the given ID. + + :param str profile: Profile ID to delete. + """ cfg = self.find_child(self.deploy_opt_section, self.CONFIG, attrib={self.CONFIG_ID: profile}) if cfg is None: @@ -1540,6 +1614,7 @@ def set_nic_names(self, name_list, profile_list): def get_serial_count(self, profile_list): """Get the number of serial ports under the given profile(s). + :param list profile_list: Profile(s) of interest. :rtype: dict :return: ``{ profile_name : serial_count }`` """ @@ -1619,6 +1694,7 @@ def _validate_value_for_property(self, prop, value): it knows nothing of the property's actual meaning. :param prop: Existing Property element. + :type prop: :class:`xml.etree.ElementTree.Element` :param str value: Proposed value to set for this property. :raise ValueUnsupportedError: if the value does not meet criteria. :return: the value, potentially canonicalized. @@ -1783,7 +1859,7 @@ def search_from_file_id(self, file_id): ``File`` in the OVF, then using that to find a matching ``Disk`` and ``Item`` entries. - :param str file_id: Filename to search from + :param str file_id: File ID to search from :return: ``(file, disk, ctrl_item, disk_item)``, any or all of which may be ``None`` """ @@ -1938,7 +2014,8 @@ def find_open_controller(self, controller_type): def get_id_from_file(self, file_obj): """Get the file ID from the given opaque file object. - :param xml.etree.ElementTree.Element file_obj: 'File' element + :param file_obj: 'File' element + :type file_obj: xml.etree.ElementTree.Element :return: 'id' attribute value of this element """ return file_obj.get(self.FILE_ID) @@ -1946,7 +2023,8 @@ def get_id_from_file(self, file_obj): def get_path_from_file(self, file_obj): """Get the file path from the given opaque file object. - :param xml.etree.ElementTree.Element file_obj: 'File' element + :param file_obj: 'File' element + :type file_obj: xml.etree.ElementTree.Element :return: 'href' attribute value of this element """ return file_obj.get(self.FILE_HREF) @@ -1954,7 +2032,8 @@ def get_path_from_file(self, file_obj): def get_file_ref_from_disk(self, disk): """Get the file reference from the given opaque disk object. - :param xml.etree.ElementTree.Element disk: 'Disk' element + :param disk: 'Disk' element + :type disk: xml.etree.ElementTree.Element :return: 'fileRef' attribute value of this element """ return disk.get(self.DISK_FILE_REF) @@ -2190,6 +2269,7 @@ def add_controller_device(self, device_type, subtype, address, :param str device_type: ``'ide'`` or ``'scsi'`` :param subtype: Subtype such as ``'virtio'`` (optional), or list of subtype values + :type subtype: string, list of strings :param int address: Controller address such as 0 or 1 (optional) :param OVFItem ctrl_item: Existing controller device to update (optional) @@ -2224,7 +2304,13 @@ def add_controller_device(self, device_type, subtype, address, return ctrl_item def _create_new_disk_device(self, disk_type, address, name, ctrl_item): - """Helper for :meth:`add_disk_device`, in the case of no prior Item.""" + """Helper for :meth:`add_disk_device`, in the case of no prior Item. + + :param str disk_type: ``'harddisk'`` or ``'cdrom'`` + :param str address: Address on controller, such as "1:0" (optional) + :param str name: Device name string (optional) + :param OVFItem ctrl_item: Controller object to serve as parent + """ ctrl_instance = ctrl_item.get_value(self.INSTANCE_ID) if address is None: logger.debug("Working to identify address of new disk") @@ -2520,7 +2606,8 @@ def find_parent_from_item(self, item): def find_item_from_disk(self, disk): """Find the disk Item that references the given Disk. - :param xml.etree.ElementTree.Element disk: Disk element + :param disk: Disk element + :type disk: :class:`xml.etree.ElementTree.Element` :return: :class:`OVFItem` instance, or None """ if disk is None: @@ -2542,7 +2629,8 @@ def find_item_from_disk(self, disk): def find_item_from_file(self, file_obj): """Find the disk Item that references the given File. - :param xml.etree.ElementTree.Element file_obj: File element + :param file_obj: File element + :type file_obj: :class:`xml.etree.ElementTree.Element` :return: :class:`OVFItem` instance, or None. """ if file_obj is None: diff --git a/COT/platforms.py b/COT/platforms.py index b135f00..757d853 100644 --- a/COT/platforms.py +++ b/COT/platforms.py @@ -54,12 +54,19 @@ def is_known_product_class(product_class): - """Determine if the given product class string is a known one.""" + """Determine if the given product class string is a known one. + + :param str product_class: String like 'com.cisco.csr1000v' + """ 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.""" + """Get the class of Platform corresponding to a product class string. + + :param str product_class: String like 'com.cisco.csr1000v' + :rtype: Instance of :class:`GenericPlatform` or subclass thereof. + """ if product_class is None: return GenericPlatform if is_known_product_class(product_class): @@ -71,7 +78,13 @@ def platform_from_product_class(product_class): def valid_range(label, value, min_val, max_val): - """Raise an exception if the value is not in the valid range.""" + """Raise an exception if the value is not in the valid range. + + :param str label: Label to include in any exception raised + :param int value: Value to validate + :param int min_val: Minimum valid value (or None if no minimum) + :param int max_val: Maximum valid value (or None if no maximum) + """ if min_val is not None and value < min_val: raise ValueTooLowError(label, value, min_val) elif max_val is not None and value > max_val: @@ -100,9 +113,22 @@ class GenericPlatform(object): SUPPORTED_NIC_TYPES = NIC_TYPES + # Valid value ranges - may be overridden by subclasses + CPU_MIN = 1 + CPU_MAX = None + RAM_MIN = 1 + RAM_MAX = None + NIC_MIN = 0 + NIC_MAX = None + SER_MIN = 0 + SER_MAX = None + @classmethod def controller_type_for_device(cls, _device_type): - """Get the default controller type for the given device type.""" + """Get the default controller type for the given device type. + + :param str _device_type: 'harddisk', 'cdrom', etc. + """ # For most platforms IDE is the correct default. return 'ide' @@ -111,23 +137,47 @@ 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! + + :param int nic_number: Nth NIC to name. """ 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.""" - valid_range("CPUs", cpus, 1, None) + """Throw an error if the number of CPUs is not a supported value. + + :param int cpus: Number of CPUs + """ + valid_range("CPUs", cpus, cls.CPU_MIN, cls.CPU_MAX) @classmethod def validate_memory_amount(cls, mebibytes): - """Throw an error if the amount of RAM is not supported.""" - valid_range("RAM", mebibytes, 1, None) + """Throw an error if the amount of RAM is not supported. + + :param int mebibytes: RAM, in MiB. + """ + if cls.RAM_MIN is not None and mebibytes < cls.RAM_MIN: + if cls.RAM_MIN > 1024 and cls.RAM_MIN % 1024 == 0: + raise ValueTooLowError("RAM", str(mebibytes) + " MiB", + str(cls.RAM_MIN / 1024) + " GiB") + else: + raise ValueTooLowError("RAM", str(mebibytes) + " MiB", + str(cls.RAM_MIN) + " MiB") + if cls.RAM_MAX is not None and mebibytes > cls.RAM_MAX: + if cls.RAM_MAX > 1024 and cls.RAM_MAX % 1024 == 0: + raise ValueTooHighError("RAM", str(mebibytes) + " MiB", + str(cls.RAM_MAX / 1024) + " GiB") + else: + raise ValueTooHighError("RAM", str(mebibytes) + " MiB", + str(cls.RAM_MAX) + " MiB") @classmethod def validate_nic_count(cls, count): - """Throw an error if the number of NICs is not supported.""" - valid_range("NIC count", count, 0, None) + """Throw an error if the number of NICs is not supported. + + :param int count: Number of NICs. + """ + valid_range("NIC count", count, cls.NIC_MIN, cls.NIC_MAX) @classmethod def validate_nic_type(cls, type_string): @@ -136,6 +186,8 @@ def validate_nic_type(cls, type_string): .. seealso:: - :func:`COT.data_validation.canonicalize_nic_subtype` - :data:`COT.data_validation.NIC_TYPES` + + :param str type_string: See :data:`COT.data_validation.NIC_TYPES` """ if type_string not in cls.SUPPORTED_NIC_TYPES: raise ValueUnsupportedError("NIC type", type_string, @@ -143,14 +195,20 @@ def validate_nic_type(cls, type_string): @classmethod def validate_nic_types(cls, type_list): - """Throw an error if any NIC type string in the list is unsupported.""" + """Throw an error if any NIC type string in the list is unsupported. + + :param list type_list: See :data:`COT.data_validation.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.""" - valid_range("serial port count", count, 0, None) + """Throw an error if the number of serial ports is not supported. + + :param int count: Number of serial ports. + """ + valid_range("serial port count", count, cls.SER_MIN, cls.SER_MAX) class IOSXRv(GenericPlatform): @@ -163,60 +221,52 @@ class IOSXRv(GenericPlatform): LITERAL_CLI_STRING = None SUPPORTED_NIC_TYPES = ["E1000", "virtio"] + # IOS XRv supports 1-8 CPUs. + CPU_MAX = 8 + # Minimum 3 GiB, max 8 GiB of RAM. + RAM_MIN = 3072 + RAM_MAX = 8192 + # IOS XRv requires at least one NIC. + NIC_MIN = 1 + # IOS XRv supports 1-4 serial ports. + SER_MIN = 1 + SER_MAX = 4 + @classmethod def guess_nic_name(cls, nic_number): - """MgmtEth0/0/CPU0/0, GigabitEthernet0/0/0/0, Gig0/0/0/1, etc.""" + """MgmtEth0/0/CPU0/0, GigabitEthernet0/0/0/0, Gig0/0/0/1, etc. + + :param int nic_number: Nth NIC to name. + """ if nic_number == 1: return "MgmtEth0/0/CPU0/0" else: return "GigabitEthernet0/0/0/" + str(nic_number - 2) - @classmethod - def validate_cpu_count(cls, cpus): - """IOS XRv supports 1-8 CPUs.""" - valid_range("CPUs", cpus, 1, 8) - - @classmethod - def validate_memory_amount(cls, mebibytes): - """Minimum 3 GiB, max 8 GiB of RAM.""" - 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.""" - valid_range("NIC count", count, 1, None) - - @classmethod - def validate_serial_count(cls, count): - """IOS XRv supports 1-4 serial ports.""" - valid_range("serial ports", count, 1, 4) - class IOSXRvRP(IOSXRv): """Platform-specific logic for Cisco IOS XRv HA-capable RP.""" PLATFORM_NAME = "Cisco IOS XRv route processor card" + # Fabric plus an optional management NIC. + NIC_MIN = 1 + NIC_MAX = 2 + @classmethod def guess_nic_name(cls, nic_number): """Fabric and management only. * fabric * MgmtEth0/{SLOT}/CPU0/0 + + :param int nic_number: Nth NIC to name. """ if nic_number == 1: return "fabric" else: return "MgmtEth0/{SLOT}/CPU0/" + str(nic_number - 2) - @classmethod - def validate_nic_count(cls, count): - """Fabric plus an optional management NIC.""" - valid_range("NIC count", count, 1, 2) - class IOSXRvLC(IOSXRv): """Platform-specific logic for Cisco IOS XRv line card.""" @@ -227,6 +277,9 @@ class IOSXRvLC(IOSXRv): CONFIG_TEXT_FILE = None SECONDARY_CONFIG_TEXT_FILE = None + # No serial ports are needed but up to 4 can be used for debugging. + SER_MIN = 0 + @classmethod def guess_nic_name(cls, nic_number): """Fabric interface plus slot-appropriate GigabitEthernet interfaces. @@ -235,17 +288,14 @@ def guess_nic_name(cls, nic_number): * GigabitEthernet0/{SLOT}/0/0 * GigabitEthernet0/{SLOT}/0/1 * etc. + + :param int nic_number: Nth NIC to name. """ if nic_number == 1: return "fabric" 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.""" - valid_range("serial ports", count, 0, 4) - class IOSXRv9000(IOSXRv): """Platform-specific logic for Cisco IOS XRv 9000 platform.""" @@ -253,9 +303,20 @@ class IOSXRv9000(IOSXRv): PLATFORM_NAME = "Cisco IOS XRv 9000" SUPPORTED_NIC_TYPES = ["E1000", "virtio", "VMXNET3"] + # Minimum 1, maximum 32 CPUs. + CPU_MAX = 32 + # Minimum 8 GiB, maximum 32 GiB. + RAM_MIN = 8192 + RAM_MAX = 32768 + # IOS XRv 9000 requires at least 4 NICs. + NIC_MIN = 4 + @classmethod def guess_nic_name(cls, nic_number): - """MgmtEth0/0/CPU0/0, CtrlEth, DevEth, GigabitEthernet0/0/0/0, etc.""" + """MgmtEth0/0/CPU0/0, CtrlEth, DevEth, GigabitEthernet0/0/0/0, etc. + + :param int nic_number: Nth NIC to name. + """ if nic_number == 1: return "MgmtEth0/0/CPU0/0" elif nic_number == 2: @@ -265,24 +326,6 @@ 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.""" - valid_range("CPUs", cpus, 1, 32) - - @classmethod - def validate_memory_amount(cls, mebibytes): - """Minimum 8 GiB, maximum 32 GiB.""" - if mebibytes < 8192: - raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "8 GiB") - elif mebibytes > 32768: - raise ValueTooHighError("RAM", str(mebibytes) + " MiB", "32 GiB") - - @classmethod - def validate_nic_count(cls, count): - """IOS XRv 9000 requires at least 4 NICs.""" - valid_range("NIC count", count, 4, None) - class CSR1000V(GenericPlatform): """Platform-specific logic for Cisco CSR1000V platform.""" @@ -294,9 +337,23 @@ class CSR1000V(GenericPlatform): # CSR1000v doesn't 'officially' support E1000, but it mostly works SUPPORTED_NIC_TYPES = ["E1000", "virtio", "VMXNET3"] + # CSR1000V supports 1, 2, 4, or 8 CPUs. + CPU_MAX = 8 + # Minimum 2.5 GiB, max 8 GiB. + RAM_MIN = 2560 + RAM_MAX = 8192 + # CSR1000V requires 3 NICs and supports up to 26. + NIC_MIN = 3 + NIC_MAX = 26 + # CSR1000V supports 0-2 serial ports. + SER_MAX = 2 + @classmethod def controller_type_for_device(cls, device_type): - """CSR1000V uses SCSI for hard disks and IDE for CD-ROMs.""" + """CSR1000V uses SCSI for hard disks and IDE for CD-ROMs. + + :param str device_type: 'harddisk' or 'cdrom' + """ if device_type == 'harddisk': return 'scsi' elif device_type == 'cdrom': @@ -312,33 +369,20 @@ def guess_nic_name(cls, nic_number): In all current CSR releases, NIC names start at "GigabitEthernet1". Some early versions started at "GigabitEthernet0" but we don't support that. + + :param int nic_number: Nth NIC to name. """ return "GigabitEthernet" + str(nic_number) @classmethod def validate_cpu_count(cls, cpus): - """CSR1000V supports 1, 2, or 4 CPUs.""" - valid_range("CPUs", cpus, 1, 4) - if cpus != 1 and cpus != 2 and cpus != 4: - raise ValueUnsupportedError("CPUs", cpus, [1, 2, 4]) - - @classmethod - def validate_memory_amount(cls, mebibytes): - """Minimum 2.5 GiB, max 8 GiB.""" - 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.""" - valid_range("NICs", count, 3, 26) + """CSR1000V supports 1, 2, 4, or 8 CPUs. - @classmethod - def validate_serial_count(cls, count): - """CSR1000V supports 0-2 serial ports.""" - valid_range("serial ports", count, 0, 2) + :param int cpus: Number of CPUs. + """ + valid_range("CPUs", cpus, 1, 8) + if cpus not in [1, 2, 4, 8]: + raise ValueUnsupportedError("CPUs", cpus, [1, 2, 4, 8]) class IOSv(GenericPlatform): @@ -352,19 +396,28 @@ class IOSv(GenericPlatform): BOOTSTRAP_DISK_TYPE = 'harddisk' SUPPORTED_NIC_TYPES = ["E1000"] + # IOSv only supports a single CPU. + CPU_MAX = 1 + # IOSv supports up to 16 NICs. + NIC_MAX = 16 + # IOSv requires 1-2 serial ports. + SER_MIN = 1 + SER_MAX = 2 + @classmethod def guess_nic_name(cls, nic_number): - """GigabitEthernet0/0, GigabitEthernet0/1, etc.""" - return "GigabitEthernet0/" + str(nic_number - 1) + """GigabitEthernet0/0, GigabitEthernet0/1, etc. - @classmethod - def validate_cpu_count(cls, cpus): - """IOSv only supports a single CPU.""" - valid_range("CPUs", cpus, 1, 1) + :param int nic_number: Nth NIC to name. + """ + return "GigabitEthernet0/" + str(nic_number - 1) @classmethod def validate_memory_amount(cls, mebibytes): - """IOSv has minimum 192 MiB (with minimal feature set), max 3 GiB.""" + """IOSv has minimum 192 MiB (with minimal feature set), max 3 GiB. + + :param int mebibytes: RAM amount, in MiB. + """ if mebibytes < 192: raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "192 MiB") elif mebibytes < 384: @@ -374,16 +427,6 @@ def validate_memory_amount(cls, mebibytes): elif mebibytes > 3072: raise ValueTooHighError("RAM", str(mebibytes) + " MiB", "3 GiB") - @classmethod - def validate_nic_count(cls, count): - """IOSv supports up to 16 NICs.""" - valid_range("NICs", count, 0, 16) - - @classmethod - def validate_serial_count(cls, count): - """IOSv requires 1-2 serial ports.""" - valid_range("serial ports", count, 1, 2) - class NXOSv(GenericPlatform): """Platform-specific logic for Cisco NX-OSv (Titanium).""" @@ -394,6 +437,15 @@ class NXOSv(GenericPlatform): LITERAL_CLI_STRING = None SUPPORTED_NIC_TYPES = ["E1000", "virtio"] + # NX-OSv requires 1-8 CPUs. + CPU_MAX = 8 + # NX-OSv requires 2-8 GiB of RAM. + RAM_MIN = 2048 + RAM_MAX = 8192 + # NX-OSv requires 1-2 serial ports. + SER_MIN = 1 + SER_MAX = 2 + @classmethod def guess_nic_name(cls, nic_number): """NX-OSv names its NICs a bit interestingly... @@ -406,6 +458,8 @@ def guess_nic_name(cls, nic_number): * Ethernet3/1 * Ethernet3/2 * ... + + :param int nic_number: Nth NIC to name. """ if nic_number == 1: return "mgmt0" @@ -413,23 +467,6 @@ 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.""" - valid_range("CPUs", cpus, 1, 8) - - @classmethod - def validate_memory_amount(cls, mebibytes): - """NX-OSv requires 2-8 GiB of RAM.""" - 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.""" - valid_range("serial ports", count, 1, 2) PRODUCT_PLATFORM_MAP = { 'com.cisco.csr1000v': CSR1000V, diff --git a/COT/remove_file.py b/COT/remove_file.py index 95de2c8..6acea99 100644 --- a/COT/remove_file.py +++ b/COT/remove_file.py @@ -32,6 +32,9 @@ class COTRemoveFile(COTSubmodule): """Remove a file (such as a README) from the package. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: :attr:`~COTGenericSubmodule.UI`, @@ -44,7 +47,11 @@ class COTRemoveFile(COTSubmodule): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTRemoveFile, self).__init__(ui) self.file_path = None """File name or path to be removed from the package.""" diff --git a/COT/submodule.py b/COT/submodule.py index 596f8bf..af1614b 100644 --- a/COT/submodule.py +++ b/COT/submodule.py @@ -38,6 +38,9 @@ class COTGenericSubmodule(object): """Abstract interface for COT command submodules. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Attributes: :attr:`vm`, :attr:`UI` @@ -48,7 +51,11 @@ class COTGenericSubmodule(object): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ self.vm = None """Virtual machine description (:class:`VMDescription`).""" self.UI = ui @@ -94,6 +101,9 @@ def create_subparser(self): class COTReadOnlySubmodule(COTGenericSubmodule): """Class for submodules that do not modify the OVF, such as 'deploy'. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: :attr:`vm`, :attr:`UI` @@ -103,7 +113,11 @@ class COTReadOnlySubmodule(COTGenericSubmodule): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTReadOnlySubmodule, self).__init__(ui) self._package = None @@ -143,6 +157,9 @@ def ready_to_run(self): class COTSubmodule(COTGenericSubmodule): """Class for submodules that read and write the OVF. + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + Inherited attributes: :attr:`vm`, :attr:`UI` @@ -153,7 +170,11 @@ class COTSubmodule(COTGenericSubmodule): """ def __init__(self, ui): - """Instantiate this submodule with the given UI.""" + """Instantiate this submodule with the given UI. + + :param ui: User interface instance. + :type ui: :class:`~COT.ui_shared.UI` + """ super(COTSubmodule, self).__init__(ui) self._package = None # Default to an unspecified output rather than no output diff --git a/COT/tests/test_file_reference.py b/COT/tests/test_file_reference.py index fd5bfcf..fb4660c 100644 --- a/COT/tests/test_file_reference.py +++ b/COT/tests/test_file_reference.py @@ -71,6 +71,15 @@ def test_add_to_archive(self): tarf.extract('input.ovf', self.temp_dir) self.check_diff("", file2=os.path.join(self.temp_dir, 'input.ovf')) + def test_equality(self): + """Test the __eq__ and __ne__ operators.""" + a = FileOnDisk(self.input_ovf) + b = FileOnDisk(os.path.dirname(self.input_ovf), + os.path.basename(self.input_ovf)) + self.assertEqual(a, b) + c = FileOnDisk(self.input_vmdk) + self.assertNotEqual(a, c) + class TestFileInTAR(COT_UT): """Test cases for FileInTAR class.""" @@ -141,3 +150,10 @@ def test_add_to_archive(self): self.check_diff("", file1=resource_filename(__name__, 'sample_cfg.txt'), file2=os.path.join(self.temp_dir, 'sample_cfg.txt')) + + def test_equality(self): + """Test the __eq__ and __ne__ operators.""" + same_ref = FileInTAR(self.tarfile, "sample_cfg.txt") + self.assertEqual(self.valid_ref, same_ref) + another_ref = FileInTAR(self.tarfile, "input.mf") + self.assertNotEqual(self.valid_ref, another_ref) diff --git a/COT/tests/test_platforms.py b/COT/tests/test_platforms.py index d379138..515d803 100644 --- a/COT/tests/test_platforms.py +++ b/COT/tests/test_platforms.py @@ -269,7 +269,12 @@ def test_cpu_count(self): self.assertRaises(ValueUnsupportedError, self.cls.validate_cpu_count, 3) self.cls.validate_cpu_count(4) - self.assertRaises(ValueTooHighError, self.cls.validate_cpu_count, 5) + for val in [5, 6, 7]: + self.assertRaises(ValueUnsupportedError, + self.cls.validate_cpu_count, val) + self.cls.validate_cpu_count(8) + self.assertRaises(ValueTooHighError, self.cls.validate_cpu_count, 9) + self.assertRaises(ValueTooHighError, self.cls.validate_cpu_count, 16) def test_memory_amount(self): """Test RAM allocation limits.""" diff --git a/COT/tests/ut.py b/COT/tests/ut.py index e672a7e..af0d152 100644 --- a/COT/tests/ut.py +++ b/COT/tests/ut.py @@ -37,7 +37,10 @@ class NullHandler(logging.Handler): """No-op logging handler.""" def emit(self, record): - """Do nothing.""" + """Do nothing. + + :param LogRecord record: Record to ignore. + """ pass import traceback @@ -63,24 +66,41 @@ def emit(self, record): class UTLoggingHandler(BufferingHandler): - """Captures log messages to a buffer so we can inspect them for testing.""" + """Captures log messages to a buffer so we can inspect them for testing. + + :param testcase: Test case owning this logging handler. + :type testcase: :class:`unittest.TestCase` + """ def __init__(self, testcase): - """Create a logging handler for the given test case.""" + """Create a logging handler for the given test case. + + :param testcase: Test case owning this logging handler. + :type testcase: :class:`unittest.TestCase` + """ BufferingHandler.__init__(self, capacity=0) self.setLevel(logging.DEBUG) self.testcase = testcase def emit(self, record): - """Add the given log record to our internal buffer.""" + """Add the given log record to our internal buffer. + + :param LogRecord record: Record to store. + """ self.buffer.append(record.__dict__) def shouldFlush(self, record): # noqa: N802 - """Return False - we only flush manually.""" + """Return False - we only flush manually. + + :param LogRecord record: Record to ignore. + """ return False def logs(self, **kwargs): - """Look for log entries matching the given dict.""" + """Look for log entries matching the given dict. + + :param kwargs: logging arguments to match against. + """ matches = [] for record in self.buffer: found_match = True @@ -103,7 +123,10 @@ def logs(self, **kwargs): return matches def assertLogged(self, info='', **kwargs): # noqa: N802 - """Fail unless the given log messages were each seen exactly once.""" + """Fail unless the given log messages were each seen exactly once. + + :param kwargs: logging arguments to match against. + """ matches = self.logs(**kwargs) if not matches: self.testcase.fail( @@ -117,7 +140,10 @@ def assertLogged(self, info='', **kwargs): # noqa: N802 self.buffer.remove(r) def assertNoLogsOver(self, max_level, info=''): # noqa: N802 - """Fail if any logs are logged higher than the given level.""" + """Fail if any logs are logged higher than the given level. + + :param int max_level: Highest logging level to permit. + """ for level in (logging.CRITICAL, logging.ERROR, logging.WARNING, logging.INFO, logging.VERBOSE, logging.DEBUG): if level <= max_level: @@ -135,7 +161,10 @@ def assertNoLogsOver(self, max_level, info=''): # noqa: N802 class COT_UT(unittest.TestCase): # noqa: N801 - """Subclass of unittest.TestCase adding some additional behaviors.""" + """Subclass of unittest.TestCase adding some additional behaviors. + + For the parameters, see :class:`unittest.TestCase`. + """ from COT.helpers.ovftool import OVFTool @@ -214,12 +243,20 @@ class COT_UT(unittest.TestCase): # noqa: N801 @staticmethod def localfile(name): - """Get the absolute path to a local resource file.""" + """Get the absolute path to a local resource file. + + :param str name: File name. + """ return os.path.abspath(resource_filename(__name__, name)) @staticmethod def invalid_hardware_warning(profile, value, kind): - """Warning log message for invalid hardware.""" + """Warning log message for invalid hardware. + + :param str profile: Config profile, or "". + :param object value: Invalid value + :param str kind: Label for this hardware kind. + """ msg = "" if profile: msg += "In profile '{0}':".format(profile) @@ -230,18 +267,28 @@ def invalid_hardware_warning(profile, value, kind): } def __init__(self, method_name='runTest'): - """Add logging handler to generic UT initialization.""" + """Add logging handler to generic UT initialization. + + For the parameters, see :class:`unittest.TestCase`. + """ super(COT_UT, self).__init__(method_name) self.logging_handler = UTLoggingHandler(self) self.instance = None def set_vm_platform(self, plat): - """Force the VM under test to use a particular Platform class.""" + """Force the VM under test to use a particular Platform class. + + :param plat: Platform class to use + :type plat: :class:`~COT.platforms.GenericPlatform` + """ # pylint: disable=protected-access self.instance.vm._platform = plat def check_cot_output(self, expected): - """Grab the output from COT and check it against expected output.""" + """Grab the output from COT and check it against expected output. + + :param str expected: Expected output + """ # pylint: disable=redefined-variable-type with mock.patch('sys.stdout', new_callable=StringIO.StringIO) as so: try: @@ -261,6 +308,10 @@ def check_diff(self, expected, file1=None, file2=None): Note that comparison of OVF files is currently skipped when running under Python 2.6, as it produces different XML output than later Python versions. + + :param str expected: Expected diff output + :param str file1: File path to compare + :param str file2: File path to compare """ if file1 is None: file1 = self.input_ovf @@ -268,6 +319,7 @@ def check_diff(self, expected, file1=None, file2=None): file2 = self.temp_file if re.search("ovf", file1) and sys.hexversion < 0x02070000: + # TODO: logging or something, this is noisy! print("OVF file diff comparison skipped " "due to old Python version ({0})" .format(platform.python_version())) @@ -374,7 +426,11 @@ def tearDown(self): .format(self.id(), delta_t)) def validate_with_ovftool(self, filename=None): - """Use OVFtool to validate the given OVF/OVA file.""" + """Use OVFtool to validate the given OVF/OVA file. + + :param str filename: File name to validate (optional, default is + :attr:`temp_file`). + """ if filename is None: filename = self.temp_file if (self.OVFTOOL.path and self.validate_output_with_ovftool and @@ -390,10 +446,13 @@ def validate_with_ovftool(self, filename=None): def assertLogged(self, info='', **kwargs): # noqa: N802 """Fail unless the given logs were generated. - See :meth:`UTLoggingHandler.assertLogged`. + For the parameters, see :meth:`UTLoggingHandler.assertLogged`. """ self.logging_handler.assertLogged(info=info, **kwargs) def assertNoLogsOver(self, max_level, info=''): # noqa: N802 - """Fail if any logs were logged higher than the given level.""" + """Fail if any logs were logged higher than the given level. + + For the parameters, see :meth:`UTLoggingHandler.assertNoLogsOver`. + """ self.logging_handler.assertNoLogsOver(max_level, info=info) diff --git a/COT/ui_shared.py b/COT/ui_shared.py index 4ec1a92..1dfd0c4 100644 --- a/COT/ui_shared.py +++ b/COT/ui_shared.py @@ -31,10 +31,15 @@ class UI(object): """Abstract user interface functionality. Can also be used in test code as a stub that autoconfirms everything. + + :param bool force: See :attr:`force`. """ def __init__(self, force=False): - """Constructor.""" + """Constructor. + + :param bool force: See :attr:`force`. + """ self.force = force """Whether to automatically select the default value in all cases. @@ -95,6 +100,8 @@ def confirm_or_die(self, prompt): A simple wrapper for :meth:`confirm` that calls :func:`sys.exit` if :meth:`confirm` returns ``False``. + + :param str prompt: Message to prompt the user with """ if not self.confirm(prompt): sys.exit("Aborting.") @@ -103,11 +110,13 @@ def choose_from_list(self, footer, option_list, default_value, header="", info_list=None): """Prompt the user to choose from a list. - :param footer: Prompt string to display following the list - :param option_list: List of strings to choose amongst - :param default_value: Default value to select if user declines - :param header: String to display prior to the list - :param info_list: Verbose strings to display instead of option_list + :param str footer: Prompt string to display following the list + :param list option_list: List of strings to choose amongst + :param str default_value: Default value to select if user declines + :param str header: String to display prior to the list + :param list info_list: Verbose strings to display in place of + :attr:`option_list` + :return: :attr:`default_value` or an item from :attr:`option_list`. """ if not info_list: info_list = option_list diff --git a/COT/vm_context_manager.py b/COT/vm_context_manager.py index d67b86b..6ec6ae1 100644 --- a/COT/vm_context_manager.py +++ b/COT/vm_context_manager.py @@ -36,10 +36,15 @@ class VMContextManager(object): with VMContextManager(input_file, output_file) as vm: vm.foo() vm.bar() + + For the parameters, see :class:`~COT.vm_description.VMDescription`. """ def __init__(self, input_file, output_file): - """Create a VM instance.""" + """Create a VM instance. + + For the parameters, see :class:`~COT.vm_description.VMDescription`. + """ self.obj = VMFactory.create(input_file, output_file) def __enter__(self): @@ -50,6 +55,7 @@ def __exit__(self, exc_type, exc_value, trace): """If the block exited cleanly, write the VM out to disk. In any case, destroy the VM. + For the parameters, see :module:`contextlib`. """ # Did we exit cleanly? try: diff --git a/COT/vm_description.py b/COT/vm_description.py index b8f78fb..92f6475 100644 --- a/COT/vm_description.py +++ b/COT/vm_description.py @@ -46,6 +46,12 @@ class VMInitError(EnvironmentError): class VMDescription(object): """Abstract class for reading, editing, and writing VM definitions. + :param str input_file: Data file to read in. + :param str output_file: File name to write to. If this VM is read-only, + (there will never be an output file) this value should be ``None``; + if the output filename is not yet known, use ``""`` and subsequently + set :attr:`output` when it is determined. + **Properties** .. autosummary:: @@ -70,6 +76,7 @@ def detect_type_from_name(cls, filename): Does not check file contents, as the given filename may not yet exist. + :param str filename: File name or path :return: A string representing a recognized and supported type of file :raise ValueUnsupportedError: if we don't know how to handle this file. """ @@ -259,7 +266,7 @@ def search_from_filename(self, filename): def search_from_file_id(self, file_id): """From the given file ID, try to find any existing objects. - :param str filename: Filename to search from + :param str file_id: File ID to search from :return: ``(file, disk, controller_device, disk_device)``, opaque objects of which any or all may be ``None`` """ @@ -286,7 +293,7 @@ def find_open_controller(self, controller_type): def get_id_from_file(self, file_obj): """Get the file ID from the given opaque file object. - :param file_obj: File object to query + :param object file_obj: File object to query :return: Identifier string associated with this object """ raise NotImplementedError("get_id_from_file not implemented") @@ -294,7 +301,7 @@ def get_id_from_file(self, file_obj): def get_path_from_file(self, file_obj): """Get the file path from the given opaque file object. - :param file_obj: File object to query + :param object file_obj: File object to query :return: Relative path to the file associated with this object """ raise NotImplementedError("get_path_from_file not implemented") @@ -302,7 +309,7 @@ def get_path_from_file(self, file_obj): def get_file_ref_from_disk(self, disk): """Get the file reference from the given opaque disk object. - :param disk: Disk object to query + :param object disk: Disk object to query :return: String that can be used to identify the file associated with this disk """ @@ -311,7 +318,7 @@ def get_file_ref_from_disk(self, disk): def get_id_from_disk(self, disk): """Get the identifier string associated with the given Disk object. - :param disk: Disk object + :param object disk: Disk object :rtype: string """ raise NotImplementedError("get_id_from_disk not implemented") @@ -319,7 +326,7 @@ def get_id_from_disk(self, disk): def get_type_from_device(self, device): """Get the type of the given opaque device object. - :param device: Device object to query + :param object device: Device object to query :return: string such as 'ide' or 'memory' """ raise NotImplementedError("get_type_from_device not implemented") @@ -327,7 +334,7 @@ def get_type_from_device(self, device): def get_subtype_from_device(self, device): """Get the sub-type of the given opaque device object. - :param device: Device object to query + :param object device: Device object to query :return: ``None``, or string such as 'virtio' or 'lsilogic' """ raise NotImplementedError("get_subtype_from_device not implemented") @@ -346,13 +353,13 @@ def check_sanity_of_disk_device(self, disk, file_obj, disk_item, ctrl_item): """Check if the given disk is linked properly to the other objects. - :param disk: Disk object to validate - :param file_obj: File object which this disk should be linked to - (optional) - :param disk_item: Disk device object which should link to this disk + :param object disk: Disk object to validate + :param object file_obj: File object which this disk should be linked to (optional) - :param ctrl_item: Controller device object which should link to the - :attr:`disk_item` + :param object disk_item: Disk device object which should link to + this disk (optional) + :param object ctrl_item: Controller device object which should link to + the :attr:`disk_item` :raise ValueMismatchError: if the given items are not linked properly. """ raise NotImplementedError( @@ -363,8 +370,8 @@ def add_file(self, file_path, file_id, file_obj=None, disk=None): :param str file_path: Path to file to add :param str file_id: Identifier string for the file in the VM - :param file_obj: Existing file object to overwrite - :param disk: Existing disk object referencing :attr:`file`. + :param object file_obj: Existing file object to overwrite + :param object disk: Existing disk object referencing :attr:`file`. :return: New or updated file object """ @@ -373,9 +380,9 @@ def add_file(self, file_path, file_id, file_obj=None, disk=None): def remove_file(self, file_obj, disk=None, disk_drive=None): """Remove the given file object from the VM. - :param file_obj: File object to remove - :param disk: Disk object referencing :attr:`file` - :param disk_drive: Disk drive mapping :attr:`file` to a device + :param object file_obj: File object to remove + :param object disk: Disk object referencing :attr:`file` + :param object disk_drive: Disk drive mapping :attr:`file` to a device """ raise NotImplementedError("remove_file not implemented") @@ -385,7 +392,7 @@ def add_disk(self, file_path, file_id, disk_type, disk=None): :param str file_path: Path to disk image file :param str file_id: Identifier string for the file/disk mapping :param str disk_type: 'harddisk' or 'cdrom' - :param disk: Existing disk object to overwrite + :param object disk: Existing disk object to overwrite :return: New or updated disk object """ @@ -398,7 +405,8 @@ def add_controller_device(self, device_type, subtype, address, :param str device_type: ``'ide'`` or ``'scsi'`` :param str subtype: Subtype such as ``'virtio'`` (optional) :param int address: Controller address such as 0 or 1 (optional) - :param ctrl_item: Existing controller device to update (optional) + :param object ctrl_item: Existing controller device to + update (optional) :return: New or updated controller device object """ @@ -412,11 +420,11 @@ def add_disk_device(self, disk_type, address, name, description, :param str address: Address on controller, such as "1:0" (optional) :param str name: Device name string (optional) :param str description: Description string (optional) - :param disk: Disk object to map to this device - :param file_obj: File object to map to this device - :param ctrl_item: Controller object to serve as parent - :param disk_item: Existing disk device to update instead of making - a new device. + :param object disk: Disk object to map to this device + :param object file_obj: File object to map to this device + :param object ctrl_item: Controller object to serve as parent + :param object disk_item: Existing disk device to update instead of + making a new device. :return: New or updated disk device object. """ @@ -426,7 +434,7 @@ def add_disk_device(self, disk_type, address, name, description, def create_configuration_profile(self, pid, label, description): """Create/update a configuration profile with the given ID. - :param pid: Profile identifier + :param str pid: Profile identifier :param str label: Brief descriptive label for the profile :param str description: Verbose description of the profile """ @@ -434,7 +442,10 @@ def create_configuration_profile(self, pid, label, description): "not implemented!") def delete_configuration_profile(self, profile): - """Delete the configuration profile with the given ID.""" + """Delete the configuration profile with the given ID. + + :param str profile: Profile identifier + """ raise NotImplementedError("delete_configuration_profile " "not implemented") @@ -568,6 +579,7 @@ def set_nic_names(self, name_list, profile_list): def get_serial_count(self, profile_list): """Get the number of serial ports under the given profile(s). + :param list profile_list: Change only the given profiles :rtype: dict :return: ``{ profile_name : serial_count }`` """ @@ -632,7 +644,7 @@ def set_ide_subtype(self, subtype, profile_list): def set_ide_subtypes(self, type_list, profile_list): """Set the device subtype list for the IDE controller(s). - :param list type: IDE subtype string list + :param list type_list: IDE subtype string list :param list profile_list: Change only the given profiles """ raise NotImplementedError("set_ide_subtypes not implemented!") @@ -651,7 +663,7 @@ def set_property_value(self, key, value): """Set the value of the given property (converting value if needed). :param str key: Property identifier - :param value: Value to set for this property + :param object value: Value to set for this property :return: the (converted) value that was set. """ raise NotImplementedError("set_property_value not implemented") @@ -704,7 +716,7 @@ def find_empty_drive(self, disk_type): def find_device_location(self, device): """Find the controller type and address of a given device object. - :param device: Hardware device object. + :param object device: Hardware device object. :returns: ``(type, address)``, such as ``("ide", "1:0")``. """ raise NotImplementedError("find_device_location not implemented") diff --git a/COT/xml_file.py b/COT/xml_file.py index 1da1616..7665a6b 100644 --- a/COT/xml_file.py +++ b/COT/xml_file.py @@ -39,11 +39,20 @@ def register_namespace(prefix, uri): class XML(object): - """Class capable of reading, editing, and writing XML files.""" + """Class capable of reading, editing, and writing XML files. + + :param str xml_file: XML file path to instantiate from. + """ @classmethod def get_ns(cls, text): - """Get the namespace prefix from an XML element or attribute name.""" + """Get the namespace prefix from an XML element or attribute name. + + :param str text: Element name or attribute name, such as + "{http://schemas.dmtf.org/ovf/envelope/1}Element". + :return: Namespace prefix, such as + "http://schemas.dmtf.org/ovf/envelope/1", or "" if no prefix present. + """ match = re.match(r"\{(.*)\}", str(text)) if not match: logger.error("No namespace prefix on %s??", text) @@ -52,7 +61,12 @@ def get_ns(cls, text): @classmethod def strip_ns(cls, text): - """Remove a namespace prefix from an XML element or attribute name.""" + """Remove a namespace prefix from an XML element or attribute name. + + :param str text: Element name or attribute name, such as + "{http://schemas.dmtf.org/ovf/envelope/1}Element". + :return: Bare name, such as "Element". + """ match = re.match(r"\{.*\}(.*)", str(text)) if match is None: logger.error("No namespace prefix on %s??", text) @@ -63,17 +77,20 @@ def strip_ns(cls, text): def __init__(self, xml_file): """Read the given XML file and store it in memory. - The memory representation is available as :attr:`self.tree` and - :attr:`self.root`. + The memory representation is available as properties :attr:`tree` and + :attr:`root`. + + :param str xml_file: File path to read. :raise xml.etree.ElementTree.ParseError: if parsing fails under Python 2.7 or later :raise xml.parsers.expat.ExpatError: if parsing fails under Python 2.6 - :param str xml_file: File path to read. """ # Parse the XML into memory self.tree = ET.parse(xml_file) + """:class:`xml.etree.ElementTree.ElementTree` describing this file.""" self.root = self.tree.getroot() + """Root :class:`xml.etree.ElementTree.Element` instance of the tree.""" def write_xml(self, xml_file): """Write pretty XML out to the given file. @@ -104,7 +121,8 @@ def write_xml(self, xml_file): def xml_reindent(self, parent, depth): """Recursively add indentation to XML to make it look nice. - :param xml.etree.ElementTree.Element parent: Current parent element + :param parent: Current parent element + :type parent: :class:`xml.etree.ElementTree.Element` :param int depth: How far down the rabbit hole we have recursed. Increments by 2 for each successive level of nesting. """ @@ -133,11 +151,12 @@ def find_child(cls, parent, tag, attrib=None, required=False): :raises LookupError: if more than one matching child is found :raises KeyError: if no matching child is found and :attr:`required` is True - :param xml.etree.ElementTree.Element parent: Parent element + :param parent: Parent element + :type parent: :class:`xml.etree.ElementTree.Element` :param str tag: Child tag to match on :param dict attrib: Child attributes to match on :param boolean required: Whether to raise an error if no child exists - :rtype: xml.etree.ElementTree.Element + :rtype: :class:`xml.etree.ElementTree.Element` """ matches = cls.find_all_children(parent, tag, attrib) if len(matches) > 1: @@ -161,11 +180,12 @@ def find_child(cls, parent, tag, attrib=None, required=False): def find_all_children(cls, parent, tag, attrib=None): """Find all matching child elements under the specified parent element. - :param xml.etree.ElementTree.Element parent: Parent element + :param parent: Parent element + :type parent: :class:`xml.etree.ElementTree.Element` :param tag: Child tag (or list of tags) to match on :type tag: string or iterable :param dict attrib: Child attributes to match on - :rtype: list of xml.etree.ElementTree.Element instances + :rtype: list of :class:`xml.etree.ElementTree.Element` instances """ assert parent is not None if isinstance(tag, str): @@ -202,8 +222,10 @@ def add_child(cls, parent, new_child, ordering=None, known_namespaces=None): """Add the given child element under the given parent element. - :param xml.etree.ElementTree.Element parent: Parent element - :param xml.etree.ElementTree.Element new_child: Child element to attach + :param parent: Parent element + :type parent: :class:`xml.etree.ElementTree.Element` + :param new_child: Child element to attach + :type new_child: :class:`xml.etree.ElementTree.Element` :param list ordering: (Optional) List describing the expected ordering of child tags under the parent; if a new child element is created, its placement under the parent will respect this sequence. @@ -258,7 +280,8 @@ def set_or_make_child(cls, parent, tag, text=None, attrib=None, ordering=None, known_namespaces=None): """Update or create a child element under the specified parent element. - :param xml.etree.ElementTree.Element parent: Parent element + :param parent: Parent element + :type parent: :class:`xml.etree.ElementTree.Element` :param str tag: Child element text tag to find or create :param str text: Value to set the child's text attribute to :param dict attrib: Dict of child attributes to match on @@ -266,7 +289,7 @@ def set_or_make_child(cls, parent, tag, text=None, attrib=None, :param list ordering: See :meth:`add_child` :param list known_namespaces: See :meth:`add_child` :return: New or updated child Element. - :rtype: xml.etree.ElementTree.Element + :rtype: :class:`xml.etree.ElementTree.Element` """ assert parent is not None if attrib is None: diff --git a/docs/conf.py b/docs/conf.py index f431e88..267be88 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -256,6 +256,12 @@ def help_text_to_rst(help, dirpath): # show-inheritance autodoc_default_flags = ['members', 'undoc-members', 'show-inheritance'] +def autodoc_skip_member(app, what, name, obj, skip, options): + """Always document __init__ method.""" + if name == "__init__": + return False + return skip + # -- General configuration, continued --------- # Add any paths that contain templates here, relative to this directory. @@ -558,3 +564,6 @@ def help_text_to_rst(help, dirpath): # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False + +def setup(app): + app.connect("autodoc-skip-member", autodoc_skip_member) From c0804c98aa5ca70aef942dd1a226095533884fb9 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 4 Jul 2016 14:39:41 -0400 Subject: [PATCH 03/59] Fix loads of doc warnings - clean run now --- .pylintrc | 10 ++++++++-- COT/add_disk.py | 3 --- COT/add_file.py | 3 --- COT/cli.py | 3 --- COT/data_validation.py | 8 +------- COT/deploy.py | 15 +-------------- COT/deploy_esxi.py | 20 +++----------------- COT/edit_hardware.py | 3 --- COT/edit_product.py | 3 --- COT/edit_properties.py | 3 --- COT/file_reference.py | 19 ++----------------- COT/help.py | 3 --- COT/helpers/helper.py | 6 ------ COT/helpers/tests/test_api.py | 2 ++ COT/helpers/tests/test_fatdisk.py | 7 ++++++- COT/helpers/tests/test_helper.py | 24 ++++++++++++++++++++---- COT/helpers/tests/test_mkisofs.py | 4 ++++ COT/helpers/tests/test_ovftool.py | 2 ++ COT/helpers/tests/test_qemu_img.py | 2 ++ COT/helpers/tests/test_vmdktool.py | 4 ++++ COT/info.py | 3 --- COT/inject_config.py | 3 --- COT/install_helpers.py | 3 --- COT/ovf/hardware.py | 3 --- COT/ovf/item.py | 4 ---- COT/ovf/name_helper.py | 6 +----- COT/ovf/ovf.py | 6 ------ COT/ovf/tests/test_ovf.py | 1 + COT/remove_file.py | 3 --- COT/submodule.py | 9 --------- COT/tests/test_add_disk.py | 2 +- COT/tests/test_cli.py | 2 ++ COT/tests/test_deploy_esxi.py | 2 ++ COT/tests/test_doctests.py | 7 ++++++- COT/tests/test_edit_product.py | 3 ++- COT/tests/test_edit_properties.py | 5 ++++- COT/tests/test_install_helpers.py | 14 ++++++++++++-- COT/tests/ut.py | 8 +++----- COT/ui_shared.py | 2 -- COT/vm_description.py | 10 ++++------ COT/xml_file.py | 5 +---- 41 files changed, 94 insertions(+), 151 deletions(-) diff --git a/.pylintrc b/.pylintrc index 6d678cb..8b324bc 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,6 +1,6 @@ [MASTER] -load-plugins=pylint.extensions.check_docs,verboselogs.pylint +load-plugins=pylint.extensions.docparams,verboselogs.pylint # Ignore generated code we don't control ignore=_version.py @@ -31,6 +31,8 @@ disable=bad-continuation, locally-disabled, too-few-public-methods, trailing-whitespace, + missing-returns-doc, + missing-raises-doc [BASIC] @@ -61,11 +63,15 @@ max-public-methods=75 # default: max-returns=6 # default: max-statements=50 +[DOCS] + +accept-no-param-doc=no + [FORMAT] # default: max-module-lines: 1000 # current worst offender: OVF -max-module-lines=2700 +max-module-lines=2800 [LOGGING] diff --git a/COT/add_disk.py b/COT/add_disk.py index dc7e0e6..6221571 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -75,9 +75,6 @@ def validate_controller_address(controller, address): class COTAddDisk(COTSubmodule): """Add or replace a disk in a virtual machine. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, diff --git a/COT/add_file.py b/COT/add_file.py index 6f9a8fe..a782f8e 100644 --- a/COT/add_file.py +++ b/COT/add_file.py @@ -32,9 +32,6 @@ class COTAddFile(COTSubmodule): """Add a file (such as a README) to the package. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, diff --git a/COT/cli.py b/COT/cli.py index 13012ca..fcc62f2 100644 --- a/COT/cli.py +++ b/COT/cli.py @@ -98,9 +98,6 @@ def formatter(verbosity=logging.INFO): class CLI(UI): """Command-line user interface for COT. - :param int terminal_width: (optional) Set the terminal width for this CLI, - independent of the actual terminal in use. - .. autosummary:: :nosignatures: diff --git a/COT/data_validation.py b/COT/data_validation.py index 1983eb8..07e30ab 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -349,13 +349,7 @@ class ValueUnsupportedError(InvalidInputError): """ def __init__(self, value_type, actual_value, expected_value): - """Create an instance of this class. - - :param str value_type: descriptive string - :param str actual_value: invalid value that was provided - :param expected_value: expected (valid) value or values (item or list) - :type expected_value: str, int, list - """ + """Create an instance of this class.""" self.value_type = value_type self.actual_value = actual_value self.expected_value = expected_value diff --git a/COT/deploy.py b/COT/deploy.py index 3b005d8..a9374d4 100644 --- a/COT/deploy.py +++ b/COT/deploy.py @@ -33,12 +33,7 @@ class SerialConnection(object): - """Generic class defining a serial port connection. - - :param str kind: Connection type string, possibly in need of munging. - :param str value: Connection value such as '/dev/ttyS0' or '1.1.1.1:80' - :param dict options: Input options dictionary. - """ + """Generic class defining a serial port connection.""" @classmethod def from_cli_string(cls, cli_string): @@ -176,9 +171,6 @@ class COTDeploy(COTReadOnlySubmodule): Provides some baseline parameters and input validation that are expected to be common across all concrete subclasses. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`~COT.submodule.COTGenericSubmodule.UI`, :attr:`~COT.submodule.COTReadOnlySubmodule.package`, @@ -441,8 +433,3 @@ def create_subparser(self): help="Set connectivity for a serial port defined in the OVF. " "This argument may be repeated to specify more port connections. " "Each entry should be structured as 'kind:value[,options]'.") - - -if __name__ == "__main__": - import doctest # pylint: disable=wrong-import-position,wrong-import-order - doctest.testmod() diff --git a/COT/deploy_esxi.py b/COT/deploy_esxi.py index 40f31f7..ec6fde6 100644 --- a/COT/deploy_esxi.py +++ b/COT/deploy_esxi.py @@ -52,13 +52,7 @@ class SmarterConnection(SmartConnection): - """A smarter version of pyVmomi's SmartConnection context manager. - - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - - For the other parameters, see :class:`pyVim.connect.SmartConnection` - """ + """A smarter version of pyVmomi's SmartConnection context manager.""" def __init__(self, ui, host, user, pwd, port=443): """Create a connection to the given server. @@ -178,19 +172,14 @@ def get_object_from_connection(conn, vimtype, name): class PyVmomiVMReconfigSpec(object): - """Context manager for reconfiguring an ESXi VM using PyVmomi. - - :param conn: Connection to ESXi. - :type conn: :class:`SmarterConnection` - :param str vm_name: Virtual machine name. - """ + """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. :param conn: Connection to ESXi. :type conn: :class:`SmarterConnection` - :param str name: Virtual machine name. + :param str vm_name: Virtual machine name. """ self.vm = get_object_from_connection(conn, vim.VirtualMachine, vm_name) assert self.vm @@ -214,9 +203,6 @@ def __exit__(self, exc_type, exc_value, trace): class COTDeployESXi(COTDeploy): """Submodule for deploying VMs on ESXi and VMware vCenter/vSphere. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`~COT.submodule.COTGenericSubmodule.UI`, :attr:`~COT.submodule.COTReadOnlySubmodule.package`, diff --git a/COT/edit_hardware.py b/COT/edit_hardware.py index 2dbf0a4..977a9cc 100644 --- a/COT/edit_hardware.py +++ b/COT/edit_hardware.py @@ -52,9 +52,6 @@ class COTEditHardware(COTSubmodule): """Edit hardware information (CPUs, RAM, NICs, etc.). - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, diff --git a/COT/edit_product.py b/COT/edit_product.py index 46940a3..0832760 100644 --- a/COT/edit_product.py +++ b/COT/edit_product.py @@ -34,9 +34,6 @@ class COTEditProduct(COTSubmodule): """Edit product, vendor, and version information strings. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, diff --git a/COT/edit_properties.py b/COT/edit_properties.py index 87132d3..56765ed 100644 --- a/COT/edit_properties.py +++ b/COT/edit_properties.py @@ -37,9 +37,6 @@ class COTEditProperties(COTSubmodule): """Edit OVF environment XML properties. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, diff --git a/COT/file_reference.py b/COT/file_reference.py index bb277aa..8a44708 100644 --- a/COT/file_reference.py +++ b/COT/file_reference.py @@ -27,13 +27,7 @@ class FileOnDisk(object): - """Wrapper for a 'real' file on disk. - - :param str file_path: File path or directory path - :param str filename: If specified, file_path is considered to be - a directory containing this filename. If not specified, the - final element in file_path is considered the filename. - """ + """Wrapper for a 'real' file on disk.""" def __init__(self, file_path, filename=None): """Create a reference to a file on disk. @@ -118,11 +112,7 @@ def add_to_archive(self, tarf): class FileInTAR(object): - """Wrapper for a file inside a TAR archive or OVA. - - :param str tarfile_path: Path to TAR archive to read - :param str filename: File name in the TAR archive. - """ + """Wrapper for a file inside a TAR archive or OVA.""" def __init__(self, tarfile_path, filename): """Create a reference to a file contained in a TAR archive. @@ -218,8 +208,3 @@ def add_to_archive(self, tarf): tarf.addfile(self.tarf.getmember(self.filename), self.obj) finally: self.close() - - -if __name__ == "__main__": - import doctest # pylint: disable=wrong-import-position,wrong-import-order - doctest.testmod() diff --git a/COT/help.py b/COT/help.py index 4ca351f..63af7e4 100644 --- a/COT/help.py +++ b/COT/help.py @@ -27,9 +27,6 @@ class COTHelp(COTGenericSubmodule): """Provide 'help ' syntax. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`~COTGenericSubmodule.UI` diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index a47bc44..0bb9c3b 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -63,12 +63,6 @@ class HelperError(EnvironmentError): class Helper(object): """A provider of a non-Python helper program. - :param str name: Name of helper executable - :param list version_args: Args to pass to the helper to - get its version. Defaults to ``['--version']`` if unset. - :param str version_regexp: Regexp to get the version number from - the output of the command. - **Class Properties** .. autosummary:: diff --git a/COT/helpers/tests/test_api.py b/COT/helpers/tests/test_api.py index f907749..22bddea 100644 --- a/COT/helpers/tests/test_api.py +++ b/COT/helpers/tests/test_api.py @@ -31,6 +31,8 @@ logger = logging.getLogger(__name__) +# pylint: disable=missing-type-doc,missing-param-doc + class TestGetChecksum(COT_UT): """Test cases for get_checksum() function.""" diff --git a/COT/helpers/tests/test_fatdisk.py b/COT/helpers/tests/test_fatdisk.py index bd8c726..360f3cb 100644 --- a/COT/helpers/tests/test_fatdisk.py +++ b/COT/helpers/tests/test_fatdisk.py @@ -28,6 +28,8 @@ logger = logging.getLogger(__name__) +# pylint: disable=missing-type-doc,missing-param-doc + @mock.patch('COT.helpers.download_and_expand', side_effect=HelperUT.stub_download_and_expand) @@ -70,6 +72,7 @@ def test_install_helper_apt_get(self, mock_copy, mock_find_executable, *_): + # pylint: disable=missing-param-doc,missing-type-doc """Test installation via 'apt-get'.""" self.enable_apt_install() mock_find_executable.side_effect = [ @@ -148,6 +151,7 @@ def test_install_helper_yum(self, mock_copy, mock_find_executable, *_): + # pylint: disable=missing-param-doc,missing-type-doc """Test installation via 'yum'.""" self.enable_yum_install() mock_find_executable.side_effect = [ @@ -181,7 +185,8 @@ def test_install_helper_linux_need_make_no_package_manager(self, *_): self.helper.install_helper() @staticmethod - def _find_make_only(name): # pylint: disable=no-self-use + def _find_make_only(name): + # pylint: disable=missing-param-doc,missing-type-doc """Stub for distutils.spawn.find_executable - only finds 'make'.""" logger.info("stub_find_executable(%s)", name) if name == 'make': diff --git a/COT/helpers/tests/test_helper.py b/COT/helpers/tests/test_helper.py index 14a3c03..8768081 100644 --- a/COT/helpers/tests/test_helper.py +++ b/COT/helpers/tests/test_helper.py @@ -32,6 +32,8 @@ logger = logging.getLogger(__name__) +# pylint: disable=missing-type-doc,missing-param-doc + class HelperUT(COT_UT): """Generic class for testing Helper and subclasses thereof.""" @@ -43,7 +45,10 @@ class HelperUT(COT_UT): } def __init__(self, method_name='runTest'): - """Add helper instance variable.""" + """Add helper instance variable to generic UT initialization. + + For the parameters, see :class:`unittest.TestCase`. + """ self.helper = None super(HelperUT, self).__init__(method_name) @@ -60,10 +65,13 @@ def assertSubprocessCalls(self, mock_function, args_list): # noqa: N802 [a[0][0] for a in mock_function.call_args_list]) def set_helper_version(self, ver): + # pylint: disable=missing-param-doc,missing-type-doc """Override the version number of the helper class.""" self.helper._version = ver # pylint: disable=protected-access - def select_package_manager(self, name): # pylint: disable=no-self-use + @staticmethod + def select_package_manager(name): + # pylint: disable=missing-param-doc,missing-type-doc """Select the specified installer program for Helper to use.""" for pm in Helper.PACKAGE_MANAGERS: Helper.PACKAGE_MANAGERS[pm] = (pm == name) @@ -90,7 +98,12 @@ def assertAptUpdated(self): # noqa: N802 @mock.patch('distutils.spawn.find_executable', return_value=None) def apt_install_test(self, pkgname, helpername, *_): - """Test installation with 'dpkg' and 'apt-get'.""" + """Test installation with 'dpkg' and 'apt-get'. + + :param str pkgname: Apt package to test installation for. + :param str helpername: Expected value of + :attr:`~COT.helpers.helper.name`, if different from :attr:`pkgname`. + """ # Python 2.6 doesn't let us do multiple mocks in one 'with' with mock.patch.object(self.helper, '_path', new=None): with mock.patch('subprocess.check_call') as mock_check_call: @@ -122,7 +135,10 @@ def apt_install_test(self, pkgname, helpername, *_): @mock.patch('distutils.spawn.find_executable', return_value=None) def port_install_test(self, portname, *_): - """Test installation with 'port'.""" + """Test installation with 'port'. + + :param str portname: MacPorts package name to test. + """ # pylint: disable=protected-access self.select_package_manager('port') Helper._port_updated = False diff --git a/COT/helpers/tests/test_mkisofs.py b/COT/helpers/tests/test_mkisofs.py index ce84e79..f30b7e6 100644 --- a/COT/helpers/tests/test_mkisofs.py +++ b/COT/helpers/tests/test_mkisofs.py @@ -25,6 +25,8 @@ from COT.helpers.tests.test_helper import HelperUT from COT.helpers.mkisofs import MkIsoFS +# pylint: disable=missing-type-doc,missing-param-doc + class TestMkIsoFS(HelperUT): """Test cases for MkIsoFS helper class.""" @@ -73,6 +75,7 @@ def test_get_version_xorriso(self, _): def test_find_mkisofs(self, mock_call_helper, mock_find_executable): """If mkisofs is found, use it.""" def find_one(name): + # pylint: disable=missing-param-doc,missing-type-doc """Find mkisofs but no other.""" if name == "mkisofs": return "/mkisofs" @@ -91,6 +94,7 @@ def find_one(name): def test_find_genisoimage(self, mock_call_helper, mock_find_executable): """If mkisofs is not found, but genisoimage is, use that.""" def find_one(name): + # pylint: disable=missing-param-doc,missing-type-doc """Find genisoimage but no other.""" if name == "genisoimage": return "/genisoimage" diff --git a/COT/helpers/tests/test_ovftool.py b/COT/helpers/tests/test_ovftool.py index df9f591..bce41fa 100644 --- a/COT/helpers/tests/test_ovftool.py +++ b/COT/helpers/tests/test_ovftool.py @@ -21,6 +21,8 @@ from COT.helpers.tests.test_helper import HelperUT from COT.helpers.ovftool import OVFTool +# pylint: disable=missing-type-doc,missing-param-doc + class TestOVFTool(HelperUT): """Test cases for OVFTool helper class.""" diff --git a/COT/helpers/tests/test_qemu_img.py b/COT/helpers/tests/test_qemu_img.py index 81cfc3b..117f3d5 100644 --- a/COT/helpers/tests/test_qemu_img.py +++ b/COT/helpers/tests/test_qemu_img.py @@ -25,6 +25,8 @@ from COT.helpers import HelperError from COT.helpers.qemu_img import QEMUImg +# pylint: disable=missing-type-doc,missing-param-doc + class TestQEMUImg(HelperUT): """Test cases for QEMUImg helper class.""" diff --git a/COT/helpers/tests/test_vmdktool.py b/COT/helpers/tests/test_vmdktool.py index 23d2a60..c8770c5 100644 --- a/COT/helpers/tests/test_vmdktool.py +++ b/COT/helpers/tests/test_vmdktool.py @@ -25,6 +25,8 @@ from COT.helpers.vmdktool import VmdkTool +# pylint: disable=missing-type-doc,missing-param-doc + @mock.patch('COT.helpers.download_and_expand', side_effect=HelperUT.stub_download_and_expand) class TestVmdkTool(HelperUT): @@ -63,6 +65,7 @@ def test_install_helper_apt_get(self, mock_check_output, mock_find_executable, *_): + # pylint: disable=missing-param-doc,missing-type-doc """Test installation via 'apt-get'.""" self.enable_apt_install() mock_find_executable.side_effect = [ @@ -132,6 +135,7 @@ def test_install_helper_yum(self, mock_check_call, mock_find_executable, *_): + # pylint: disable=missing-param-doc,missing-type-doc """Test installation via 'yum'.""" self.enable_yum_install() mock_find_executable.side_effect = [ diff --git a/COT/info.py b/COT/info.py index 8483eb4..078630b 100644 --- a/COT/info.py +++ b/COT/info.py @@ -31,9 +31,6 @@ class COTInfo(COTGenericSubmodule): """Display VM information string. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`~COTGenericSubmodule.UI` diff --git a/COT/inject_config.py b/COT/inject_config.py index a920235..7ca0eba 100644 --- a/COT/inject_config.py +++ b/COT/inject_config.py @@ -31,9 +31,6 @@ class COTInjectConfig(COTSubmodule): """Wrap configuration file(s) into a disk image embedded into the VM. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, diff --git a/COT/install_helpers.py b/COT/install_helpers.py index 8c39ee4..eb5fc86 100644 --- a/COT/install_helpers.py +++ b/COT/install_helpers.py @@ -136,9 +136,6 @@ def install_manpages(man_dir): class COTInstallHelpers(COTGenericSubmodule): """Install all helper tools that COT requires. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`~COTGenericSubmodule.UI`, :attr:`~COTSubmodule.package`, diff --git a/COT/ovf/hardware.py b/COT/ovf/hardware.py index f0d7094..37598fc 100644 --- a/COT/ovf/hardware.py +++ b/COT/ovf/hardware.py @@ -47,9 +47,6 @@ class OVFHardware(object): Fundamentally it's just a dict of :class:`~COT.ovf.item.OVFItem` objects with a bunch of helper methods. - - :param ovf: OVF instance to extract hardware information from. - :type ovf: :class:`~COT.ovf.ovf.OVF` """ def __init__(self, ovf): diff --git a/COT/ovf/item.py b/COT/ovf/item.py index 72b3483..02a5225 100644 --- a/COT/ovf/item.py +++ b/COT/ovf/item.py @@ -73,10 +73,6 @@ class OVFItem(object): * a dict of ``Item`` properties (indexed by element name) * each of which is a dict of sets of profiles (indexed by element value) - - :param OVF ovf: OVF instance that owns the Item (optional) - :param item: 'Item' element (optional) - :type item: :class:`xml.etree.ElementTree.Element` """ # Magic strings diff --git a/COT/ovf/name_helper.py b/COT/ovf/name_helper.py index 13380f9..16cceff 100644 --- a/COT/ovf/name_helper.py +++ b/COT/ovf/name_helper.py @@ -51,11 +51,7 @@ def name_helper(version): class _Tag(object): - """Helper class representing a named XML namespace and associated tag. - - :param str namespace_name: XML namespace name - :param str tag: XML tag - """ + """Helper class representing a named XML namespace and associated tag.""" def __init__(self, namespace_name, tag): """Store namespace name and tag. diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index 7097a75..db26f0f 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -177,12 +177,6 @@ def byte_string(byte_value, base_shift=0): class OVF(VMDescription, XML): """Representation of the contents of an OVF or OVA. - :param str input_file: Data file to read in. - :param str output_file: File name to write to. If this VM is read-only, - (there will never be an output file) this value should be ``None``; - if the output filename is not yet known, use ``""`` and subsequently - set :attr:`output_file` when it is determined. - **Properties** .. autosummary:: diff --git a/COT/ovf/tests/test_ovf.py b/COT/ovf/tests/test_ovf.py index 559cf36..2083b6c 100644 --- a/COT/ovf/tests/test_ovf.py +++ b/COT/ovf/tests/test_ovf.py @@ -372,6 +372,7 @@ def test_invalid_ovf_file(self): @mock.patch("COT.ovf.OVF.detect_type_from_name", return_value=".vbox") def test_unknown_extension(self, mock_type): + # pylint: disable=missing-type-doc,missing-param-doc """Test handling of unexpected behavior in detect_type_from_name.""" # unsupported input file type with self.assertRaises(VMInitError) as cm: diff --git a/COT/remove_file.py b/COT/remove_file.py index 6acea99..0ef791f 100644 --- a/COT/remove_file.py +++ b/COT/remove_file.py @@ -32,9 +32,6 @@ class COTRemoveFile(COTSubmodule): """Remove a file (such as a README) from the package. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`~COTGenericSubmodule.UI`, diff --git a/COT/submodule.py b/COT/submodule.py index af1614b..012dc89 100644 --- a/COT/submodule.py +++ b/COT/submodule.py @@ -38,9 +38,6 @@ class COTGenericSubmodule(object): """Abstract interface for COT command submodules. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Attributes: :attr:`vm`, :attr:`UI` @@ -101,9 +98,6 @@ def create_subparser(self): class COTReadOnlySubmodule(COTGenericSubmodule): """Class for submodules that do not modify the OVF, such as 'deploy'. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`vm`, :attr:`UI` @@ -157,9 +151,6 @@ def ready_to_run(self): class COTSubmodule(COTGenericSubmodule): """Class for submodules that read and write the OVF. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` - Inherited attributes: :attr:`vm`, :attr:`UI` diff --git a/COT/tests/test_add_disk.py b/COT/tests/test_add_disk.py index a95992e..51bbba8 100644 --- a/COT/tests/test_add_disk.py +++ b/COT/tests/test_add_disk.py @@ -748,7 +748,7 @@ def test_overwrite_implicit_file_id(self): self.assertLogged(**self.OVERWRITING_DISK) self.instance.finished() self.assertLogged(**self.invalid_hardware_warning( - "howlongofaprofilenamecanweusehere", "0", "RAM")) + "howlongofaprofilenamecanweusehere", "0 MiB", "RAM")) self.assertLogged(msg="Removing unused network") self.check_diff(file1=self.invalid_ovf, expected=""" diff --git a/COT/tests/test_cli.py b/COT/tests/test_cli.py index 9b25f29..4e2abb2 100644 --- a/COT/tests/test_cli.py +++ b/COT/tests/test_cli.py @@ -35,6 +35,8 @@ from COT.cli import CLI from COT.data_validation import InvalidInputError +# pylint: disable=missing-param-doc,missing-type-doc + class TestCOTCLI(COT_UT): """Parent class for CLI test cases.""" diff --git a/COT/tests/test_deploy_esxi.py b/COT/tests/test_deploy_esxi.py index f099301..99054c4 100644 --- a/COT/tests/test_deploy_esxi.py +++ b/COT/tests/test_deploy_esxi.py @@ -36,6 +36,8 @@ logger = logging.getLogger(__name__) +# pylint: disable=missing-param-doc,missing-type-doc + class TestCOTDeployESXi(COT_UT): """Test cases for COTDeployESXi class.""" diff --git a/COT/tests/test_doctests.py b/COT/tests/test_doctests.py index fdf70c0..bbe85bd 100644 --- a/COT/tests/test_doctests.py +++ b/COT/tests/test_doctests.py @@ -21,9 +21,14 @@ def load_tests(*_): - """Load doctests as unittest test suite.""" + """Load doctests as unittest test suite. + + For the parameters, see :mod:`unittest`. The parameters are unused here. + """ suite = TestSuite() suite.addTests(DocTestSuite('COT.cli')) + suite.addTests(DocTestSuite('COT.deploy')) suite.addTests(DocTestSuite('COT.edit_hardware')) + suite.addTests(DocTestSuite('COT.file_reference')) suite.addTests(DocTestSuite('COT.ovf.ovf')) return suite diff --git a/COT/tests/test_edit_product.py b/COT/tests/test_edit_product.py index da3fdcc..82ed155 100644 --- a/COT/tests/test_edit_product.py +++ b/COT/tests/test_edit_product.py @@ -67,7 +67,8 @@ def test_edit_product_class_no_existing(self): self.instance.product_class = "com.cisco.csr1000v" self.instance.run() self.instance.finished() - self.assertLogged(**self.invalid_hardware_warning(None, '0', 'NICs')) + self.assertLogged( + **self.invalid_hardware_warning(None, '0', 'NIC count')) self.check_diff(file1=self.minimal_ovf, expected=""" diff --git a/COT/tests/test_edit_properties.py b/COT/tests/test_edit_properties.py index e715f22..754b7e1 100644 --- a/COT/tests/test_edit_properties.py +++ b/COT/tests/test_edit_properties.py @@ -366,7 +366,10 @@ def test_edit_interactive(self): def custom_input(prompt, default_value): # pylint: disable=unused-argument - """Mock for get_input.""" + """Mock for :meth:`COT.ui_shared.UI.get_input`. + + For the parameters, see get_input. + """ if self.counter > 0: log = expected[self.counter-1][msgs_idx] if log is not None: diff --git a/COT/tests/test_install_helpers.py b/COT/tests/test_install_helpers.py index 59e8daa..10ec5db 100644 --- a/COT/tests/test_install_helpers.py +++ b/COT/tests/test_install_helpers.py @@ -28,9 +28,15 @@ from COT.helpers import HelperError from COT.helpers.helper import Helper +# pylint: disable=missing-param-doc,missing-type-doc + def stub_check_output(arg_list, *_args, **_kwargs): - """Stub to ensure fixed version number strings.""" + """Stub to ensure fixed version number strings. + + :param list arg_list: arg_list[0] is script being called, + others are ignored. + """ versions = { "fatdisk": "fatdisk, version 1.0.0-beta", "genisoimage": "genisoimage 1.1.11 (Linux)", @@ -45,7 +51,11 @@ def stub_check_output(arg_list, *_args, **_kwargs): def stub_dir_exists_but_not_file(path): - """Stub for os.path.exists; return true for man dir, false for man file.""" + """Stub for :func:`os.path.exists`. + + :param str path: Path to check. + :return: True for man dir, False for man file. + """ return os.path.basename(path) != "cot.1" diff --git a/COT/tests/ut.py b/COT/tests/ut.py index af0d152..d47d78d 100644 --- a/COT/tests/ut.py +++ b/COT/tests/ut.py @@ -66,11 +66,7 @@ def emit(self, record): class UTLoggingHandler(BufferingHandler): - """Captures log messages to a buffer so we can inspect them for testing. - - :param testcase: Test case owning this logging handler. - :type testcase: :class:`unittest.TestCase` - """ + """Captures log messages to a buffer so we can inspect them for testing.""" def __init__(self, testcase): """Create a logging handler for the given test case. @@ -125,6 +121,7 @@ def logs(self, **kwargs): def assertLogged(self, info='', **kwargs): # noqa: N802 """Fail unless the given log messages were each seen exactly once. + :param str info: Optional string to prepend to any failure messages. :param kwargs: logging arguments to match against. """ matches = self.logs(**kwargs) @@ -143,6 +140,7 @@ def assertNoLogsOver(self, max_level, info=''): # noqa: N802 """Fail if any logs are logged higher than the given level. :param int max_level: Highest logging level to permit. + :param str info: Optional string to prepend to any failure messages. """ for level in (logging.CRITICAL, logging.ERROR, logging.WARNING, logging.INFO, logging.VERBOSE, logging.DEBUG): diff --git a/COT/ui_shared.py b/COT/ui_shared.py index 1dfd0c4..4191159 100644 --- a/COT/ui_shared.py +++ b/COT/ui_shared.py @@ -31,8 +31,6 @@ class UI(object): """Abstract user interface functionality. Can also be used in test code as a stub that autoconfirms everything. - - :param bool force: See :attr:`force`. """ def __init__(self, force=False): diff --git a/COT/vm_description.py b/COT/vm_description.py index 92f6475..f02f55c 100644 --- a/COT/vm_description.py +++ b/COT/vm_description.py @@ -46,12 +46,6 @@ class VMInitError(EnvironmentError): class VMDescription(object): """Abstract class for reading, editing, and writing VM definitions. - :param str input_file: Data file to read in. - :param str output_file: File name to write to. If this VM is read-only, - (there will never be an output file) this value should be ``None``; - if the output filename is not yet known, use ``""`` and subsequently - set :attr:`output` when it is determined. - **Properties** .. autosummary:: @@ -70,6 +64,10 @@ class VMDescription(object): version_long """ + # Pylint wants to complain about returns documentation for functions that + # raise a NotImplementedError - shush it. + # pylint: disable=redundant-returns-doc + @classmethod def detect_type_from_name(cls, filename): """Check the given filename to see if it looks like a type we support. diff --git a/COT/xml_file.py b/COT/xml_file.py index 7665a6b..26e8120 100644 --- a/COT/xml_file.py +++ b/COT/xml_file.py @@ -39,10 +39,7 @@ def register_namespace(prefix, uri): class XML(object): - """Class capable of reading, editing, and writing XML files. - - :param str xml_file: XML file path to instantiate from. - """ + """Class capable of reading, editing, and writing XML files.""" @classmethod def get_ns(cls, text): From 7214f550c944aca3faf2298b38d5ddd395103f44 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 14 Jul 2016 10:50:42 -0400 Subject: [PATCH 04/59] Add returns documentation --- COT/add_disk.py | 1 + COT/cli.py | 3 + COT/data_validation.py | 5 ++ COT/deploy.py | 2 + COT/deploy_esxi.py | 1 + COT/file_reference.py | 22 +++++-- COT/helpers/api.py | 5 +- COT/helpers/helper.py | 10 ++++ COT/helpers/qemu_img.py | 1 + COT/helpers/tests/test_ovftool.py | 7 --- COT/ovf/hardware.py | 8 ++- COT/ovf/item.py | 28 ++++++++- COT/ovf/name_helper.py | 4 ++ COT/ovf/ovf.py | 59 ++++++++++++------ COT/ovf/tests/test_doctests.py | 31 ++++++++++ COT/platforms.py | 99 ++++++++++++++++++------------- COT/tests/test_doctests.py | 1 - COT/tests/test_file_reference.py | 16 ++--- COT/tests/test_install_helpers.py | 1 + COT/tests/ut.py | 5 +- COT/vm_factory.py | 1 + COT/xml_file.py | 2 + docs/COT.platforms.rst | 9 --- 23 files changed, 222 insertions(+), 99 deletions(-) create mode 100644 COT/ovf/tests/test_doctests.py diff --git a/COT/add_disk.py b/COT/add_disk.py index 6221571..0ada8cb 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -352,6 +352,7 @@ def guess_controller_type(vm, ctrl_item, disk_type): :type vm: :class:`~COT.vm_description.VMDescription` :param object ctrl_item: Any known controller object :param str disk_type: "cdrom" or "harddisk" + :return: 'ide' or 'scsi' """ if ctrl_item is None: # If the user didn't tell us which controller type they wanted, diff --git a/COT/cli.py b/COT/cli.py index fcc62f2..4b564fa 100644 --- a/COT/cli.py +++ b/COT/cli.py @@ -391,6 +391,7 @@ def get_password(self, username, host): :param str host: Host the password is associated with :raise InvalidInputError: if :attr:`force` is ``True`` (as there is no "default" password value) + :return: Password string """ if self.force: raise InvalidInputError("No password specified for {0}@{1}" @@ -505,6 +506,7 @@ def add_subparser(self, title, :param str lookup_prefix: String to prepend to ``title`` and each alias in ``aliases`` for lookup purposes. :param kwargs: Passed through to :meth:`parent.add_parser` + :return: Subparser object """ # Subparser aliases are only supported by argparse in Python 3.2+ if sys.hexversion >= 0x03020000 and aliases: @@ -545,6 +547,7 @@ def args_to_dict(args): :param args: Namespace returned from :meth:`parse_args`. :type args: :class:`argparse.Namespace` + :return: Dictionary of arg to value :rtype: dict """ arg_dict = vars(args) diff --git a/COT/data_validation.py b/COT/data_validation.py index 07e30ab..a1d1765 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -61,6 +61,7 @@ def to_string(obj): """Get string representation of an object, special-case for XML Element. :param object obj: Object to represent as a string. + :return: string representation """ if ET.iselement(obj): return ET.tostring(obj) @@ -83,6 +84,7 @@ def convert(text): :param text: Input to convert :type text: str, int + :return: Converted value :rtype: str, int """ return int(text) if text.isdigit() else text @@ -91,6 +93,7 @@ def alphanum_key(key): """Split the key into a list of [text, int, text, int, ...]. :param str key: String to split. + :return: List of tokens """ return [convert(c) for c in re.split('([0-9]+)', key)] @@ -312,6 +315,7 @@ def non_negative_int(string): Alias for :func:`validate_int` setting :attr:`minimum` to 0. :param str string: String to validate. + :return: Validated integer value """ return validate_int(string, minimum=0) @@ -322,6 +326,7 @@ def positive_int(string): Alias for :func:`validate_int` setting :attr:`minimum` to 1. :param str string: String to validate. + :return: Validated integer value """ return validate_int(string, minimum=1) diff --git a/COT/deploy.py b/COT/deploy.py index a9374d4..bbcd4ce 100644 --- a/COT/deploy.py +++ b/COT/deploy.py @@ -51,6 +51,8 @@ def from_cli_string(cls, cli_string): "" >>> str(SerialConnection.from_cli_string('telnet://1.1.1.1:1111')) '' + + :return: SerialConnection instance or None. """ if cli_string is None: return None diff --git a/COT/deploy_esxi.py b/COT/deploy_esxi.py index ec6fde6..b042601 100644 --- a/COT/deploy_esxi.py +++ b/COT/deploy_esxi.py @@ -159,6 +159,7 @@ def get_object_from_connection(conn, vimtype, name): :type conn: :class:`SmarterConnection` :param object vimtype: currently only `vim.VirtualMachine`` :param str name: Name of the object to look up. + :return: Located object """ obj = None content = conn.RetrieveContent() diff --git a/COT/file_reference.py b/COT/file_reference.py index 8a44708..526d088 100644 --- a/COT/file_reference.py +++ b/COT/file_reference.py @@ -50,7 +50,7 @@ def __init__(self, file_path, filename=None): else: self.file_path = os.path.join(file_path, filename) self.filename = filename - if not self.exists(): + if not self.exists: raise IOError("File {0} does not exist!".format(self.file_path)) self.obj = None @@ -60,6 +60,7 @@ def __eq__(self, other): No attempt is made to check file equivalence, symlinks, etc. :param object other: Other object to compare against + :return: True if the paths are the same, else False """ return type(other) is type(self) and self.file_path == other.file_path @@ -67,21 +68,25 @@ def __ne__(self, other): """FileOnDisk instances are not equal if they have different paths. :param object other: Other object to compare against + :return: False if the paths are the same, else True """ return not self.__eq__(other) + @property def exists(self): - """Check whether the file exists on disk.""" + """True if the file exists on disk, else False.""" return os.path.exists(self.file_path) + @property def size(self): - """Get the size of this file, in bytes.""" + """The size of this file, in bytes.""" return os.path.getsize(self.file_path) def open(self, mode): """Open the file and return a reference to the file object. :param str mode: Mode such as 'r', 'w', 'a', 'w+', etc. + :return: File object """ self.obj = open(self.file_path, mode) return self.obj @@ -124,7 +129,7 @@ def __init__(self, tarfile_path, filename): raise IOError("{0} is not a valid TAR file.".format(tarfile_path)) self.tarfile_path = tarfile_path self.filename = filename - if not self.exists(): + if not self.exists: raise IOError("{0} does not exist in {1}" .format(filename, tarfile_path)) self.file_path = None @@ -137,6 +142,7 @@ def __eq__(self, other): No attempt is made to check file equivalence, symlinks, etc. :param object other: Other object to compare against + :return: True if the filename and tarfile_path are the same, else False """ if type(other) is type(self): return (self.tarfile_path == other.tarfile_path and @@ -147,11 +153,13 @@ def __ne__(self, other): """FileInTar are not equal if they have different paths or names. :param object other: Other object to compare against + :return: False if the filename and tarfile_path are the same, else True """ return not self.__eq__(other) + @property def exists(self): - """Check whether the file exists in the TAR archive.""" + """True if the file exists in the TAR archive, else False.""" with closing(tarfile.open(self.tarfile_path, 'r')) as tarf: try: tarf.getmember(self.filename) @@ -159,8 +167,9 @@ def exists(self): except KeyError: return False + @property def size(self): - """Get the size of this file in bytes.""" + """The size of this file in bytes.""" with closing(tarfile.open(self.tarfile_path, 'r')) as tarf: return tarf.getmember(self.filename).size @@ -168,6 +177,7 @@ def open(self, mode): """Open the TAR and return a reference to the relevant file object. :param str mode: Only 'r' and 'rb' modes are supported. + :return: File object """ # We can only extract a file object from a TAR file in read mode. if mode != 'r' and mode != 'rb': diff --git a/COT/helpers/api.py b/COT/helpers/api.py index 71fa2b1..c87abe4 100644 --- a/COT/helpers/api.py +++ b/COT/helpers/api.py @@ -105,6 +105,7 @@ def download_and_expand(url): # d is automatically cleaned up. :param str url: URL of a .tgz or .tar.gz file to download. + :return: Path to temporary directory where file contents have been expanded """ with TemporaryDirectory(prefix="cot_helper") as d: logger.debug("Temporary directory is %s", d) @@ -132,7 +133,8 @@ def get_checksum(path_or_obj, checksum_type): :param str path_or_obj: File path to checksum OR an opened file object :param str checksum_type: Supported values are 'md5' and 'sha1'. - :return: String containing hexadecimal file checksum + :return: Hexadecimal file checksum + :rtype: str """ # pylint: disable=redefined-variable-type if checksum_type == 'md5': @@ -203,6 +205,7 @@ def get_disk_capacity(file_path): :param str file_path: Path to disk image file to inspect :return: Disk capacity, in bytes + :rtype: str """ return QEMUIMG.get_disk_capacity(file_path) diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 0bb9c3b..a630929 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -40,6 +40,8 @@ def guess_file_format_from_path(file_path): """Guess the preferred file format based on file path/extension. :param str file_path: Filename or file path. + :return: Guessed file format + :rtype: str """ file_format = os.path.splitext(file_path)[1][1:] if not file_format: @@ -125,6 +127,7 @@ def find_executable(name): """Wrapper for :func:`distutils.spawn.find_executable`. :param str name: Executable name. + :return: Path to executable, or None if not found. """ return distutils.spawn.find_executable(name) @@ -136,6 +139,8 @@ def apt_install(cls, package): """Try to use ``apt-get`` to install a package. :param str package: Package name. + :return: Whether package was installed/updated successfully. + :rtype: bool """ if not cls.PACKAGE_MANAGERS['apt-get']: return False @@ -158,6 +163,8 @@ def port_install(cls, package): """Try to use ``port`` to install a package. :param str package: Package name. + :return: Whether package was installed/updated successfully. + :rtype: bool """ if not cls.PACKAGE_MANAGERS['port']: return False @@ -172,6 +179,8 @@ def yum_install(cls, package): """Try to use ``yum`` to install a package. :param str package: Package name. + :return: Whether package was installed/updated successfully. + :rtype: bool """ if not cls.PACKAGE_MANAGERS['yum']: return False @@ -185,6 +194,7 @@ def make_install_dir(cls, directory, permissions=493): # 493 == 0o755 :param str directory: Directory to check/create. :param int permissions: Permissions to set on the created directory. + :return: True """ if os.path.isdir(directory): # TODO: permissions check, update permissions if needed diff --git a/COT/helpers/qemu_img.py b/COT/helpers/qemu_img.py index c7a6094..48f6038 100644 --- a/COT/helpers/qemu_img.py +++ b/COT/helpers/qemu_img.py @@ -91,6 +91,7 @@ def get_disk_capacity(self, file_path): :param str file_path: Path to disk image file to inspect :return: Disk capacity, in bytes + :rtype: str """ output = self.call_helper(['info', file_path]) match = re.search(r"(\d+) bytes", output) diff --git a/COT/helpers/tests/test_ovftool.py b/COT/helpers/tests/test_ovftool.py index bce41fa..4fdc1ad 100644 --- a/COT/helpers/tests/test_ovftool.py +++ b/COT/helpers/tests/test_ovftool.py @@ -53,13 +53,6 @@ def test_install_helper_already_present(self, mock_check_call, mock_check_output.assert_not_called() self.assertLogged(**self.ALREADY_INSTALLED) - def test_install_helper_unsupported(self): - """No support for automated installation of ovftool.""" - with mock.patch('COT.helpers.ovftool.OVFTool.path', - new_callable=mock.PropertyMock, return_value=None): - with self.assertRaises(NotImplementedError): - self.helper.install_helper() - @mock.patch('distutils.spawn.find_executable', return_value="/fake/ovftool") @mock.patch('COT.helpers.helper.Helper._check_output', return_value="") diff --git a/COT/ovf/hardware.py b/COT/ovf/hardware.py index 37598fc..c84dd09 100644 --- a/COT/ovf/hardware.py +++ b/COT/ovf/hardware.py @@ -142,6 +142,7 @@ def update_xml(self): def find_unused_instance_id(self): """Find the first available ``InstanceID`` number. + :return: Instance ID not yet in use. :rtype: string """ i = 1 @@ -211,6 +212,7 @@ def item_match(self, item, resource_type, properties, profile_list): :param properties: Property values to match :type properties: dict[property, value] :param list profile_list: List of profiles to filter on + :return: True if the item matches all filters, False if not. """ if resource_type and (self.ovf.RES_MAP[resource_type] != item.get_value(self.ovf.RESOURCE_TYPE)): @@ -252,7 +254,7 @@ def find_item(self, resource_type=None, properties=None, profile=None): :param properties: Property values to match :type properties: dict[property, value] :param str profile: Single profile ID to search within - :rtype: :class:`OVFItem` or ``None`` + :return: Matching :class:`OVFItem` instance, or None :raise LookupError: if more than one such Item exists. """ matches = self.find_all_items(resource_type, properties, [profile]) @@ -283,7 +285,8 @@ def get_item_count_per_profile(self, resource_type, profile_list): :param str resource_type: :param list profile_list: List of profiles to filter on (default: apply across all profiles) - :rtype: dict[profile, count] + :return: Dict mapping profile strings to the number of items under + each profile. """ count_dict = {} if not profile_list: @@ -358,6 +361,7 @@ def _update_cloned_item(self, new_item, new_item_profiles, item_count): :param int item_count: How many Items of this type (including this item) now exist. Used with :meth:`COT.platform.GenericPlatform.guess_nic_name` + :return: Updated :param:`new_item` """ resource_type = self.ovf.get_type_from_device(new_item) address = new_item.get(self.ovf.ADDRESS) diff --git a/COT/ovf/item.py b/COT/ovf/item.py index 02a5225..0f58e03 100644 --- a/COT/ovf/item.py +++ b/COT/ovf/item.py @@ -50,7 +50,17 @@ def list_union(*lists): """Get union of lists. + :: + + >>> list_union([1, 2, 3], [0, 4], [1, 5]) + [1, 2, 3, 0, 4, 5] + >>> list_union(['foo'], ['bar'], ['bar', 'foo']) + ['foo', 'bar'] + >>> list_union(['bar', 'foo'], ['foo'], ['bar']) + ['bar', 'foo'] + :param list lists: List of lists to unify. + :return: List of all distinct values across the given lists. """ result = [] for l in lists: @@ -112,6 +122,7 @@ def __getattr__(self, name): """Transparently pass attribute lookups off to OVF/OVFNameHelper. :param str name: Attribute name. + :return: Value looked up from OVFNameHelper. """ # Don't pass 'special' attributes through to the helper if re.match(r"^__", name): @@ -129,6 +140,7 @@ def property_values(self, name): """Get list of values known for a given property name. :param str name: Property name. + :return: List of values """ return list(self.properties[name].keys()) @@ -138,6 +150,7 @@ def property_profiles(self, name, value): :param str name: Property name. :param value: Property value of interest. :type value: str, tuple + :return: Set of profile strings """ return self.properties[name][value] @@ -146,6 +159,7 @@ def all_profiles(self, name, default=None): :param str name: Property name. :param object default: Default value to return if there are no matches + :return: Set of profile strings, or the given `default` if no matches. """ value_dict = self.properties.get(name, None) if not value_dict: @@ -232,6 +246,10 @@ def value_add_wildcards(self, name, value, profiles): :param str name: Property name :param str value: Value to add wildcards to. :param list profiles: Profiles to which this (name, value) applies. + :return: The updated value string with wildcards added. + + .. seealso:: + :meth:`value_replace_wildcards` """ if name == self.ELEMENT_NAME or name == self.ITEM_DESCRIPTION: vq_val = self.get_value(self.VIRTUAL_QUANTITY, profiles) @@ -258,6 +276,10 @@ def value_replace_wildcards(self, name, value, profiles): :param str name: Property name :param str value: Value to replace wildcards from. :param list profiles: Profiles to which this (name, value) applies. + :return: The updated value string, with wildcards replaced. + + .. seealso:: + :meth:`value_add_wildcards` """ if not value: return value @@ -478,7 +500,6 @@ def get(self, tag): """Get the dict associated with the given XML tag, if any. :param str tag: XML tag to look up - :rtype: dict :return: Dictionary of values associated with this tag (TODO?) """ return self.properties.get(tag, None) @@ -546,7 +567,7 @@ def get_all_values(self, tag): """Get the list of all value strings for the given tag. :param str tag: Tag to retrieve value for - :rtype: list + :return: List of value strings. """ if tag == self.RESOURCE_SUB_TYPE: # ResourceSubType values may themselves be tuples @@ -590,7 +611,7 @@ def has_profile(self, profile): """Check if this Item exists under the given profile. :param str profile: Profile name - :rtype: boolean + :return: True if the item exists in this profile, False if not. """ profiles = self.all_profiles(self.INSTANCE_ID) if profiles is None: @@ -647,6 +668,7 @@ def get_nonintersecting_set_list(self): def generate_items(self): """Get a list of Item XML elements derived from this object's data. + :return: Generated list of XML Item elements :rtype: list[xml.etree.ElementTree.Element] """ set_string_list = self.get_nonintersecting_set_list() diff --git a/COT/ovf/name_helper.py b/COT/ovf/name_helper.py index 16cceff..7cba9c8 100644 --- a/COT/ovf/name_helper.py +++ b/COT/ovf/name_helper.py @@ -258,6 +258,7 @@ def __getattr__(self, name): """Transparently pass attribute lookups to _raw and _cache. :param str name: Attribute name to look up. + :return: Value looked up from :attr:`_raw` and/or :attr:`_cache`. """ if name in self._item_children: return self._item_children[name] @@ -316,6 +317,7 @@ def namespace_for_item_tag(self, tag): """Get the XML namespace for the given item tag. :param str tag: Un-namespaced XML tag. + :return: XML namespace string, or None. """ if tag == self.ITEM: return self.RASD @@ -329,6 +331,7 @@ def namespace_for_resource_type(self, resource_type): """Get the XML namespace for the given ResourceType. :param str resource_type: ResourceType value string. + :return: XML namespace string, or None. """ if resource_type == self.RES_MAP['ethernet']: return self.EPASD @@ -342,6 +345,7 @@ def item_tag_for_namespace(self, ns): """Get the Item tag for the given XML namespace. :param str ns: XML namespace + :return: 'Item', 'StorageItem', or 'EthernetPortItem' as appropriate. """ if ns == self.RASD: return self.ITEM diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index db26f0f..cb56c9a 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -243,6 +243,7 @@ def _ovf_descriptor_from_name(self, input_file): return the path to the extracted OVF descriptor. :param str input_file: Path to an OVF descriptor or OVA file. + :return: OVF descriptor path """ extension = self.detect_type_from_name(input_file) if extension == '.ova': @@ -454,8 +455,8 @@ def product_class(self, product_class): def platform(self): """The platform type, as determined from the OVF descriptor. - :type: Class object - :class:`~COT.platforms.GenericPlatform` or - a more-specific subclass if recognized as such. + This will be the class :class:`~COT.platforms.GenericPlatform` or + a more-specific subclass if recognized as such. """ if self._platform is None: self._platform = platform_from_product_class(self.product_class) @@ -483,6 +484,7 @@ def _validate_helper(label, fn, *args): :param str label: Label to prepend to any warning messages :param function fn: Validation function to call. :param args: Arguments to validation function. + :return: True if valid, False if invalid """ try: fn(*args) @@ -554,9 +556,8 @@ def config_profiles(self): def environment_properties(self): """The array of environment properties. - :return: Array of dicts (one per property) with the keys - ``"key"``, ``"value"``, ``"qualifiers"``, ``"type"``, - ``"label"``, and ``"description"``. + Array of dicts (one per property) with the keys ``"key"``, ``"value"``, + ``"qualifiers"``, ``"type"``, ``"label"``, and ``"description"``. """ result = [] if self.ovf_version < 1.0 or self.product_section is None: @@ -578,10 +579,7 @@ def environment_properties(self): @property def environment_transports(self): - """The list of environment transport methods. - - :rtype: list[str] - """ + """The list of environment transport method strings.""" if self.ovf_version < 1.0: return None if self.virtual_hw_section is not None: @@ -603,10 +601,7 @@ def environment_transports(self, transports): @property def networks(self): - """The list of network names currently defined in this VM. - - :rtype: list[str] - """ + """The list of network names currently defined in this VM.""" if self.network_section is None: return [] return [network.get(self.NETWORK_NAME) for @@ -730,6 +725,7 @@ def __getattr__(self, name): """Transparently pass attribute lookups off to name_helper. :param str name: Attribute being looked up. + :return: Attribute value """ # Don't pass 'special' attributes through to the helper if re.match(r"^__", name): @@ -792,7 +788,7 @@ def validate_and_update_file_references(self): href = file_elem.get(self.FILE_HREF) file_ref = self._file_references[href] - if file_ref is not None and not file_ref.exists(): + if file_ref is not None and not file_ref.exists: # file used to exist but no longer does?? logger.error("Referenced file '%s' does not exist!", href) self._file_references[href] = None @@ -805,7 +801,7 @@ def validate_and_update_file_references(self): # TODO remove references to this file from Disk, Item? continue - real_size = str(file_ref.size()) + real_size = str(file_ref.size) real_capacity = None # We can't check disk capacity inside a tar file. # It seems wasteful to extract the disk file (could be @@ -869,6 +865,7 @@ def _info_string_header(self, width): """Generate OVF/OVA file header for :meth:`info_string`. :param int width: Line length to wrap to where possible. + :return: File header string """ str_list = [] str_list.append('-' * width) @@ -886,6 +883,7 @@ def _info_string_product(self, verbosity_option, wrapper): or ``'verbose'`` :param wrapper: Helper object for wrapping text lines if needed. :type wrapper: :class:`textwrap.TextWrapper` + :return: Product information string """ if ((not any([self.product, self.vendor, self.version_short])) and (verbosity_option == 'brief' or not any([ @@ -918,6 +916,7 @@ def _info_string_annotation(self, wrapper): :param wrapper: Helper object for wrapping text lines if needed. :type wrapper: :class:`textwrap.TextWrapper` + :return: Annotation information string, or None """ if self.annotation_section is None: return None @@ -945,6 +944,7 @@ def _info_string_eula(self, verbosity_option, wrapper): or ``'verbose'`` :param wrapper: Helper object for wrapping text lines if needed. :type wrapper: :class:`textwrap.TextWrapper` + :return: EULA string """ # An OVF may have zero, one, or more eula_header = False @@ -1028,6 +1028,7 @@ def _info_string_files_disks(self, width, verbosity_option): :param int width: Line length to wrap to where possible. :param str verbosity_option: ``'brief'``, ``None`` (default), or ``'verbose'`` + :return: File/disk information string, or None """ file_list = self.references.findall(self.FILE) disk_list = (self.disk_section.findall(self.DISK) @@ -1089,6 +1090,7 @@ def _info_string_hardware(self, wrapper): :param wrapper: Helper object for wrapping text lines if needed. :type wrapper: :class:`textwrap.TextWrapper` + :return: Hardware information string, or None """ virtual_system_types = self.system_types scsi_subtypes = list_union( @@ -1127,6 +1129,7 @@ def _info_string_networks(self, verbosity_option, wrapper): or ``'verbose'`` :param wrapper: Helper object for wrapping text lines if needed. :type wrapper: :class:`textwrap.TextWrapper` + :return: Network information string, or None """ if self.network_section is None: return None @@ -1164,6 +1167,7 @@ def _info_string_nics(self, verbosity_option, wrapper): or ``'verbose'`` :param wrapper: Helper object for wrapping text lines if needed. :type wrapper: :class:`textwrap.TextWrapper` + :return: NIC information string, or None """ if verbosity_option == 'brief': return None @@ -1199,6 +1203,7 @@ def _info_string_environment(self, wrapper): :param wrapper: Helper object for wrapping text lines if needed. :type wrapper: :class:`textwrap.TextWrapper` + :return: Environment information string, or None """ if not self.environment_transports: return None @@ -1217,6 +1222,7 @@ def _info_string_properties(self, verbosity_option, wrapper): or ``'verbose'`` :param wrapper: Helper object for wrapping text lines if needed. :type wrapper: :class:`textwrap.TextWrapper` + :return: Property information string, or None """ properties = self.environment_properties if not properties: @@ -1671,7 +1677,7 @@ def get_property_value(self, key): """Get the value of the given property. :param str key: Property identifier - :return: Value of this property, or ``None`` + :return: Value of this property as a string, or ``None`` """ if self.ovf_version < 1.0 or self.product_section is None: return None @@ -1692,6 +1698,7 @@ def _validate_value_for_property(self, prop, value): :param str value: Proposed value to set for this property. :raise ValueUnsupportedError: if the value does not meet criteria. :return: the value, potentially canonicalized. + :rtype: str """ key = prop.get(self.PROP_KEY) @@ -1735,6 +1742,7 @@ def set_property_value(self, key, value): :param str key: Property identifier :param str value: Value to set for this property :return: the (converted) value that was set. + :rtype: str """ if self.ovf_version < 1.0: raise NotImplementedError("No support for setting environment " @@ -2011,6 +2019,7 @@ def get_id_from_file(self, file_obj): :param file_obj: 'File' element :type file_obj: xml.etree.ElementTree.Element :return: 'id' attribute value of this element + :rtype: str """ return file_obj.get(self.FILE_ID) @@ -2020,6 +2029,7 @@ def get_path_from_file(self, file_obj): :param file_obj: 'File' element :type file_obj: xml.etree.ElementTree.Element :return: 'href' attribute value of this element + :rtype: str """ return file_obj.get(self.FILE_HREF) @@ -2029,6 +2039,7 @@ def get_file_ref_from_disk(self, disk): :param disk: 'Disk' element :type disk: xml.etree.ElementTree.Element :return: 'fileRef' attribute value of this element + :rtype: str """ return disk.get(self.DISK_FILE_REF) @@ -2130,6 +2141,7 @@ def add_file(self, file_path, file_id, file_obj=None, disk=None): :type disk: xml.etree.ElementTree.Element :return: New or updated file object + :rtype: xml.etree.ElementTree.Element """ logger.debug("Adding File to OVF") @@ -2215,6 +2227,7 @@ def add_disk(self, file_path, file_id, disk_type, disk=None): :type disk: xml.etree.ElementTree.Element :return: New or updated disk object + :rtype: xml.etree.ElementTree.Element """ if disk_type != 'harddisk': if disk is not None: @@ -2269,6 +2282,7 @@ def add_controller_device(self, device_type, subtype, address, (optional) :return: New or updated controller device object + :rtype: OVFItem """ if ctrl_item is None: logger.info("Controller not found, adding new Item") @@ -2304,6 +2318,7 @@ def _create_new_disk_device(self, disk_type, address, name, ctrl_item): :param str address: Address on controller, such as "1:0" (optional) :param str name: Device name string (optional) :param OVFItem ctrl_item: Controller object to serve as parent + :return: (disk_item, disk_name) """ ctrl_instance = ctrl_item.get_value(self.INSTANCE_ID) if address is None: @@ -2361,6 +2376,7 @@ def add_disk_device(self, disk_type, address, name, description, making a new device. :return: New or updated disk device object. + :rtype: xml.etree.ElementTree.Element """ if disk_item is None: logger.info("Disk Item not found, adding new Item") @@ -2537,6 +2553,7 @@ def _create_envelope_section_if_absent(self, section_tag, info_string, :param dict attrib: Attributes to filter by when looking for any existing section (optional). :return: Section element that was found or created + :rtype: xml.etree.ElementTree.Element """ section = self.find_child(self.envelope, section_tag, attrib=attrib) if section is not None: @@ -2568,6 +2585,7 @@ def set_product_section_child(self, child_tag, child_text): :param str child_tag: XML tag of the product section child element. :param str child_text: Text to set for the child element. :return: The product section element that was updated or created + :rtype: xml.etree.ElementTree.Element """ if self.product_section is None: self.product_section = self.set_or_make_child( @@ -2584,7 +2602,8 @@ def find_parent_from_item(self, item): """Find the parent Item of the given Item. :param OVFItem item: Item whose parent is desired - :return: :class:`OVFItem` representing the parent device, or None + :return: :class:`OVFItem` instance representing the parent device, + or None """ if item is None: return None @@ -2647,6 +2666,7 @@ def find_disk_from_file_id(self, file_id): :param str file_id: File identifier string :return: Disk element matching the file, or None + :rtype: xml.etree.ElementTree.Element, None """ if file_id is None or self.disk_section is None: return None @@ -2658,7 +2678,7 @@ def find_empty_drive(self, disk_type): """Find a disk device that exists but contains no data. :param str disk_type: Either 'cdrom' or 'harddisk' - :return: Hardware device object, or None. + :return: :class:`OVFItem` representing this disk device, or None. """ if disk_type == 'cdrom': # Find a drive that has no HostResource property @@ -2704,7 +2724,7 @@ def get_id_from_disk(self, disk): :param disk: Disk object to inspect :type disk: xml.etree.ElementTree.Element - :rtype: string + :return: Disk identifier string """ return disk.get(self.DISK_ID) @@ -2713,6 +2733,7 @@ def get_capacity_from_disk(self, disk): :param disk: Disk element to inspect :type disk: xml.etree.ElementTree.Element + :return: Disk capacity, in bytes :rtype: int """ cap = int(disk.get(self.DISK_CAPACITY)) diff --git a/COT/ovf/tests/test_doctests.py b/COT/ovf/tests/test_doctests.py new file mode 100644 index 0000000..b795f9b --- /dev/null +++ b/COT/ovf/tests/test_doctests.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# +# test_doctests.py - test runner for COT doctests +# +# July 2016, Glenn F. Matthews +# Copyright (c) 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. + +"""Test runner for COT.ovf doctest tests.""" + +from doctest import DocTestSuite +from unittest import TestSuite + + +def load_tests(*_): + """Load doctests as unittest test suite. + + 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')) + return suite diff --git a/COT/platforms.py b/COT/platforms.py index 757d853..6fe4ec9 100644 --- a/COT/platforms.py +++ b/COT/platforms.py @@ -22,6 +22,7 @@ .. autosummary:: :nosignatures: + is_known_product_class platform_from_product_class **Classes** @@ -46,9 +47,11 @@ import logging -from .data_validation import ValueUnsupportedError -from .data_validation import ValueTooLowError, ValueTooHighError -from .data_validation import NIC_TYPES +from COT.data_validation import ( + validate_int, + ValueUnsupportedError, ValueTooLowError, ValueTooHighError, + NIC_TYPES, +) logger = logging.getLogger(__name__) @@ -57,6 +60,7 @@ def is_known_product_class(product_class): """Determine if the given product class string is a known one. :param str product_class: String like 'com.cisco.csr1000v' + :return: True if the class is in :data:`PRODUCT_PLATFORM_MAP`, else False """ return product_class in PRODUCT_PLATFORM_MAP @@ -65,7 +69,10 @@ def platform_from_product_class(product_class): """Get the class of Platform corresponding to a product class string. :param str product_class: String like 'com.cisco.csr1000v' - :rtype: Instance of :class:`GenericPlatform` or subclass thereof. + :return: Best guess of the appropriate platform based on + :data:`PRODUCT_PLATFORM_MAP`, defaulting to + :class:`GenericPlatform` if no better guess exists. + :rtype: class """ if product_class is None: return GenericPlatform @@ -77,21 +84,6 @@ def platform_from_product_class(product_class): return GenericPlatform -def valid_range(label, value, min_val, max_val): - """Raise an exception if the value is not in the valid range. - - :param str label: Label to include in any exception raised - :param int value: Value to validate - :param int min_val: Minimum valid value (or None if no minimum) - :param int max_val: Maximum valid value (or None if no maximum) - """ - if min_val is not None and value < min_val: - raise ValueTooLowError(label, value, min_val) - elif max_val is not None and value > max_val: - raise ValueTooHighError(label, value, max_val) - return True - - class GenericPlatform(object): """Generic class for operations that depend on guest platform. @@ -123,11 +115,14 @@ class GenericPlatform(object): SER_MIN = 0 SER_MAX = None + # Some of these methods are semi-abstract, so: + # pylint: disable=unused-argument @classmethod - def controller_type_for_device(cls, _device_type): + def controller_type_for_device(cls, device_type): """Get the default controller type for the given device type. - :param str _device_type: 'harddisk', 'cdrom', etc. + :param str device_type: 'harddisk', 'cdrom', etc. + :return: 'ide' unless overridden by subclass. """ # For most platforms IDE is the correct default. return 'ide' @@ -139,6 +134,7 @@ def guess_nic_name(cls, nic_number): .. note:: This method counts from 1, not from 0! :param int nic_number: Nth NIC to name. + :return: "Ethernet1", "Ethernet2", etc. unless overridden by subclass. """ return "Ethernet" + str(nic_number) @@ -148,7 +144,7 @@ def validate_cpu_count(cls, cpus): :param int cpus: Number of CPUs """ - valid_range("CPUs", cpus, cls.CPU_MIN, cls.CPU_MAX) + validate_int(cpus, cls.CPU_MIN, cls.CPU_MAX, "CPUs") @classmethod def validate_memory_amount(cls, mebibytes): @@ -177,7 +173,7 @@ def validate_nic_count(cls, count): :param int count: Number of NICs. """ - valid_range("NIC count", count, cls.NIC_MIN, cls.NIC_MAX) + validate_int(count, cls.NIC_MIN, cls.NIC_MAX, "NIC count") @classmethod def validate_nic_type(cls, type_string): @@ -208,7 +204,7 @@ def validate_serial_count(cls, count): :param int count: Number of serial ports. """ - valid_range("serial port count", count, cls.SER_MIN, cls.SER_MAX) + validate_int(count, cls.SER_MIN, cls.SER_MAX, "serial port count") class IOSXRv(GenericPlatform): @@ -237,6 +233,11 @@ def guess_nic_name(cls, nic_number): """MgmtEth0/0/CPU0/0, GigabitEthernet0/0/0/0, Gig0/0/0/1, etc. :param int nic_number: Nth NIC to name. + :return: + * "MgmtEth0/0/CPU0/0" + * "GigabitEthernet0/0/0/0" + * "GigabitEthernet0/0/0/1" + * etc. """ if nic_number == 1: return "MgmtEth0/0/CPU0/0" @@ -257,10 +258,8 @@ class IOSXRvRP(IOSXRv): def guess_nic_name(cls, nic_number): """Fabric and management only. - * fabric - * MgmtEth0/{SLOT}/CPU0/0 - :param int nic_number: Nth NIC to name. + :return: "fabric" or "MgmtEth0/{SLOT}/CPU0/0" only """ if nic_number == 1: return "fabric" @@ -284,12 +283,12 @@ class IOSXRvLC(IOSXRv): def guess_nic_name(cls, nic_number): """Fabric interface plus slot-appropriate GigabitEthernet interfaces. - * fabric - * GigabitEthernet0/{SLOT}/0/0 - * GigabitEthernet0/{SLOT}/0/1 - * etc. - :param int nic_number: Nth NIC to name. + :return: + * "fabric" + * "GigabitEthernet0/{SLOT}/0/0" + * "GigabitEthernet0/{SLOT}/0/1" + * etc. """ if nic_number == 1: return "fabric" @@ -316,6 +315,13 @@ def guess_nic_name(cls, nic_number): """MgmtEth0/0/CPU0/0, CtrlEth, DevEth, GigabitEthernet0/0/0/0, etc. :param int nic_number: Nth NIC to name. + :return: + * "MgmtEth0/0/CPU0/0" + * "CtrlEth" + * "DevEth" + * "GigabitEthernet0/0/0/0" + * "GigabitEthernet0/0/0/1" + * etc. """ if nic_number == 1: return "MgmtEth0/0/CPU0/0" @@ -353,6 +359,7 @@ def controller_type_for_device(cls, device_type): """CSR1000V uses SCSI for hard disks and IDE for CD-ROMs. :param str device_type: 'harddisk' or 'cdrom' + :return: 'ide' for CD-ROM, 'scsi' for hard disk """ if device_type == 'harddisk': return 'scsi' @@ -371,6 +378,10 @@ def guess_nic_name(cls, nic_number): support that. :param int nic_number: Nth NIC to name. + :return: + * "GigabitEthernet1" + * "GigabitEthernet2" + * etc. """ return "GigabitEthernet" + str(nic_number) @@ -380,7 +391,7 @@ def validate_cpu_count(cls, cpus): :param int cpus: Number of CPUs. """ - valid_range("CPUs", cpus, 1, 8) + validate_int(cpus, 1, 8, "CPUs") if cpus not in [1, 2, 4, 8]: raise ValueUnsupportedError("CPUs", cpus, [1, 2, 4, 8]) @@ -409,6 +420,10 @@ def guess_nic_name(cls, nic_number): """GigabitEthernet0/0, GigabitEthernet0/1, etc. :param int nic_number: Nth NIC to name. + :return: + * "GigabitEthernet0/0" + * "GigabitEthernet0/1" + * etc. """ return "GigabitEthernet0/" + str(nic_number - 1) @@ -450,16 +465,16 @@ class NXOSv(GenericPlatform): def guess_nic_name(cls, nic_number): """NX-OSv names its NICs a bit interestingly... - * mgmt0 - * Ethernet2/1 - * Ethernet2/2 - * ... - * Ethernet2/48 - * Ethernet3/1 - * Ethernet3/2 - * ... - :param int nic_number: Nth NIC to name. + :return: + * "mgmt0" + * "Ethernet2/1" + * "Ethernet2/2" + * ... + * "Ethernet2/48" + * "Ethernet3/1" + * "Ethernet3/2" + * ... """ if nic_number == 1: return "mgmt0" diff --git a/COT/tests/test_doctests.py b/COT/tests/test_doctests.py index bbe85bd..81fe9e2 100644 --- a/COT/tests/test_doctests.py +++ b/COT/tests/test_doctests.py @@ -30,5 +30,4 @@ def load_tests(*_): suite.addTests(DocTestSuite('COT.deploy')) suite.addTests(DocTestSuite('COT.edit_hardware')) suite.addTests(DocTestSuite('COT.file_reference')) - suite.addTests(DocTestSuite('COT.ovf.ovf')) return suite diff --git a/COT/tests/test_file_reference.py b/COT/tests/test_file_reference.py index fb4660c..0a8f743 100644 --- a/COT/tests/test_file_reference.py +++ b/COT/tests/test_file_reference.py @@ -35,13 +35,13 @@ def test_nonexistent_file(self): self.assertRaises(IOError, FileOnDisk, "/foo", "bar.txt") def test_exists(self): - """Test the exists() API.""" - self.assertTrue(FileOnDisk(self.input_ovf).exists()) + """Test the exists property.""" + self.assertTrue(FileOnDisk(self.input_ovf).exists) # false case is covered by test_nonexistent_file def test_size(self): - """Test the size() API.""" - self.assertEqual(FileOnDisk(self.input_ovf).size(), + """Test the size property.""" + self.assertEqual(FileOnDisk(self.input_ovf).size, os.path.getsize(self.input_ovf)) def test_open_close(self): @@ -103,13 +103,13 @@ def test_not_tarfile(self): self.assertRaises(IOError, FileInTAR, self.input_ovf, self.input_ovf) def test_exists(self): - """Test the exists() API.""" - self.assertTrue(self.valid_ref.exists()) + """Test the exists property.""" + self.assertTrue(self.valid_ref.exists) # false case is covered in test_nonexistent_entry def test_size(self): - """Test the size() API.""" - self.assertEqual(self.valid_ref.size(), + """Test the size property.""" + self.assertEqual(self.valid_ref.size, os.path.getsize(resource_filename(__name__, 'sample_cfg.txt'))) diff --git a/COT/tests/test_install_helpers.py b/COT/tests/test_install_helpers.py index 10ec5db..bc7b309 100644 --- a/COT/tests/test_install_helpers.py +++ b/COT/tests/test_install_helpers.py @@ -36,6 +36,7 @@ def stub_check_output(arg_list, *_args, **_kwargs): :param list arg_list: arg_list[0] is script being called, others are ignored. + :return: Canned output line, or "" """ versions = { "fatdisk": "fatdisk, version 1.0.0-beta", diff --git a/COT/tests/ut.py b/COT/tests/ut.py index d47d78d..14c96ed 100644 --- a/COT/tests/ut.py +++ b/COT/tests/ut.py @@ -89,6 +89,7 @@ def shouldFlush(self, record): # noqa: N802 """Return False - we only flush manually. :param LogRecord record: Record to ignore. + :return: False """ return False @@ -96,6 +97,7 @@ def logs(self, **kwargs): """Look for log entries matching the given dict. :param kwargs: logging arguments to match against. + :return: List of record(s) that matched. """ matches = [] for record in self.buffer: @@ -244,6 +246,7 @@ def localfile(name): """Get the absolute path to a local resource file. :param str name: File name. + :return: Absolute file path. """ return os.path.abspath(resource_filename(__name__, name)) @@ -254,6 +257,7 @@ def invalid_hardware_warning(profile, value, kind): :param str profile: Config profile, or "". :param object value: Invalid value :param str kind: Label for this hardware kind. + :return: dict of kwargs suitable for passing into :meth:`assertLogged` """ msg = "" if profile: @@ -287,7 +291,6 @@ def check_cot_output(self, expected): :param str expected: Expected output """ - # pylint: disable=redefined-variable-type with mock.patch('sys.stdout', new_callable=StringIO.StringIO) as so: try: self.instance.run() diff --git a/COT/vm_factory.py b/COT/vm_factory.py index b3e71d9..ecdf74e 100644 --- a/COT/vm_factory.py +++ b/COT/vm_factory.py @@ -37,6 +37,7 @@ def create(cls, input_file, output_file): ValueUnsupportedError while loading the file. :param str input_file: File to read VM description from :param str output_file: File to write to when finished (optional) + :return: Created object :rtype: instance of :class:`VMDescription` or appropriate subclass """ vm_class = None diff --git a/COT/xml_file.py b/COT/xml_file.py index 26e8120..1be6221 100644 --- a/COT/xml_file.py +++ b/COT/xml_file.py @@ -153,6 +153,7 @@ def find_child(cls, parent, tag, attrib=None, required=False): :param str tag: Child tag to match on :param dict attrib: Child attributes to match on :param boolean required: Whether to raise an error if no child exists + :return: Child element found, or None :rtype: :class:`xml.etree.ElementTree.Element` """ matches = cls.find_all_children(parent, tag, attrib) @@ -182,6 +183,7 @@ def find_all_children(cls, parent, tag, attrib=None): :param tag: Child tag (or list of tags) to match on :type tag: string or iterable :param dict attrib: Child attributes to match on + :return: (Possibly empty) list of matching child elements :rtype: list of :class:`xml.etree.ElementTree.Element` instances """ assert parent is not None diff --git a/docs/COT.platforms.rst b/docs/COT.platforms.rst index 2d7b44d..0473695 100644 --- a/docs/COT.platforms.rst +++ b/docs/COT.platforms.rst @@ -2,13 +2,4 @@ ======================== .. automodule:: COT.platforms - :no-members: - -.. autoclass:: COT.platforms.GenericPlatform -.. autoclass:: COT.platforms.CSR1000V -.. autoclass:: COT.platforms.IOSv -.. autoclass:: COT.platforms.IOSXRv -.. autoclass:: COT.platforms.IOSXRvRP -.. autoclass:: COT.platforms.IOSXRvLC -.. autoclass:: COT.platforms.NXOSv From b468adcd4f2ee4bb0888c7b97a2490c42ab24993 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 14 Jul 2016 14:38:21 -0400 Subject: [PATCH 05/59] Add documentation of raised exceptions --- COT/deploy.py | 2 ++ COT/deploy_esxi.py | 4 ++++ COT/file_reference.py | 5 +++++ COT/helpers/api.py | 8 ++++++-- COT/helpers/helper.py | 6 ++++++ COT/helpers/qemu_img.py | 2 ++ COT/inject_config.py | 7 +++++++ COT/ovf/hardware.py | 4 ++++ COT/ovf/item.py | 10 ++++++++++ COT/ovf/name_helper.py | 2 ++ COT/ovf/ovf.py | 27 +++++++++++++++++++++++++++ COT/platforms.py | 22 ++++++++++++++++++++++ COT/vm_description.py | 9 ++++----- 13 files changed, 101 insertions(+), 7 deletions(-) diff --git a/COT/deploy.py b/COT/deploy.py index bbcd4ce..355a2cb 100644 --- a/COT/deploy.py +++ b/COT/deploy.py @@ -53,6 +53,7 @@ def from_cli_string(cls, cli_string): '' :return: SerialConnection instance or None. + :raise InvalidInputError: if ``cli_string`` cannot be parsed """ if cli_string is None: return None @@ -110,6 +111,7 @@ def validate_value(cls, kind, value): :param str value: Connection value such as '/dev/ttyS0' or '1.1.1.1:80' :return: Munged value string. :raise InvalidInputError: if value string is not recognized as valid + :raise NotImplementedError: if ``kind`` is not valid """ if kind == 'device' or kind == 'file' or kind == 'pipe': # TODO: Validate that device path exists on target? diff --git a/COT/deploy_esxi.py b/COT/deploy_esxi.py index b042601..9377c59 100644 --- a/COT/deploy_esxi.py +++ b/COT/deploy_esxi.py @@ -409,6 +409,10 @@ def fixup_serial_ports(self, serial_count): """Use PyVmomi to create and configure serial ports for the new VM. :param int serial_count: Number of serial ports desired. + :raise NotImplementedError: If any + :class:`~COT.deploy.SerialConnection` in :attr:`serial_connection` + has a :attr:`~COT.deploy.SerialConnection.kind` other than + 'tcp', 'telnet', or 'device' """ if serial_count > len(self.serial_connection): logger.warning("No serial connectivity information is " diff --git a/COT/file_reference.py b/COT/file_reference.py index 526d088..c203eda 100644 --- a/COT/file_reference.py +++ b/COT/file_reference.py @@ -43,6 +43,8 @@ def __init__(self, file_path, filename=None): >>> b = FileOnDisk('/etc', 'resolv.conf') >>> a == b True + + :raise IOError: if no such file exists """ if filename is None: self.file_path = file_path @@ -124,6 +126,8 @@ def __init__(self, tarfile_path, filename): :param str tarfile_path: Path to TAR archive to read :param str filename: File name in the TAR archive. + :raise IOError: if ``tarfile_path`` doesn't reference a TAR file, + or the TAR file does not contain ``filename``. """ if not tarfile.is_tarfile(tarfile_path): raise IOError("{0} is not a valid TAR file.".format(tarfile_path)) @@ -178,6 +182,7 @@ def open(self, mode): :param str mode: Only 'r' and 'rb' modes are supported. :return: File object + :raise ValueError: if ``mode`` is not valid. """ # We can only extract a file object from a TAR file in read mode. if mode != 'r' and mode != 'rb': diff --git a/COT/helpers/api.py b/COT/helpers/api.py index c87abe4..6f2de81 100644 --- a/COT/helpers/api.py +++ b/COT/helpers/api.py @@ -135,6 +135,7 @@ def get_checksum(path_or_obj, checksum_type): :param str checksum_type: Supported values are 'md5' and 'sha1'. :return: Hexadecimal file checksum :rtype: str + :raise NotImplementedError: if ``checksum_type`` is not a supported value. """ # pylint: disable=redefined-variable-type if checksum_type == 'md5': @@ -178,6 +179,7 @@ def get_disk_format(file_path): * ``format`` may be ``'vmdk'``, ``'raw'``, or ``'qcow2'`` * ``subformat`` may be ``None``, or various strings for ``'vmdk'`` files. + :raise RuntimeError: if unable to identify the sub-format of a VMDK file. """ file_format = QEMUIMG.get_disk_format(file_path) @@ -230,7 +232,7 @@ def convert_disk_image(file_path, output_dir, new_format, new_subformat=None): * :attr:`file_path`, if no conversion was required * or a file path in :attr:`output_dir` containing the converted image - :raise ValueUnsupportedError: if the :attr:`new_format` and/or + :raise NotImplementedError: if the :attr:`new_format` and/or :attr:`new_subformat` are not supported conversion targets. """ curr_format, curr_subformat = get_disk_format(file_path) @@ -294,10 +296,12 @@ def create_disk_image(file_path, file_format=None, :param str file_path: Desired location of new disk image :param str file_format: Desired image format (if not specified, this will be derived from the file extension of :attr:`file_path`) - :param str capacity: Disk capacity. A string like '16M' or '1G'. :param list contents: List of file paths to package into the created image. If not specified, the image will be left blank and unformatted. + :raise RuntimeError: if neither :attr:`capacity` nor :attr:`contents` is + specified + :raise NotImplementedError: if the :attr:`file_format` is not supported. """ if not capacity and not contents: raise RuntimeError("Either capacity or contents must be specified!") diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index a630929..a4b5c44 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -42,6 +42,7 @@ def guess_file_format_from_path(file_path): :param str file_path: Filename or file path. :return: Guessed file format :rtype: str + :raise RuntimeError: if unable to guess """ file_format = os.path.splitext(file_path)[1][1:] if not file_format: @@ -195,6 +196,9 @@ def make_install_dir(cls, directory, permissions=493): # 493 == 0o755 :param str directory: Directory to check/create. :param int permissions: Permissions to set on the created directory. :return: True + :raise RuntimeError: if something other than a directory already + exists at the path referenced by ``directory``. + :raise OSError: if directory creation failed """ if os.path.isdir(directory): # TODO: permissions check, update permissions if needed @@ -297,6 +301,8 @@ def call_helper(self, args, capture_output=True, require_success=True): raised if the helper exits with a non-zero status code. :return: Captured stdout/stderr (if :attr:`capture_output`), else ``None``. + :raise HelperNotFoundError: if the helper was not previously installed, + and the user declines to install it at this time. """ if not self.path: if not self.confirm("{0} does not appear to be installed.\n" diff --git a/COT/helpers/qemu_img.py b/COT/helpers/qemu_img.py index 48f6038..3cce29a 100644 --- a/COT/helpers/qemu_img.py +++ b/COT/helpers/qemu_img.py @@ -73,6 +73,7 @@ def get_disk_format(self, file_path): :param str file_path: Path to disk image file to inspect. :return: Disk image format (``'vmdk'``, ``'raw'``, ``'qcow2'``, etc.) + :raise RuntimeError: if unable to parse the qemu-img output. """ output = self.call_helper(['info', file_path]) # Read the format from the output @@ -92,6 +93,7 @@ def get_disk_capacity(self, file_path): :param str file_path: Path to disk image file to inspect :return: Disk capacity, in bytes :rtype: str + :raise RuntimeError: if unable to parse the qemu-img output. """ output = self.call_helper(['info', file_path]) match = re.search(r"(\d+) bytes", output) diff --git a/COT/inject_config.py b/COT/inject_config.py index 7ca0eba..ac59d0d 100644 --- a/COT/inject_config.py +++ b/COT/inject_config.py @@ -115,6 +115,13 @@ def run(self): """Do the actual work of this submodule. :raises InvalidInputError: if :func:`ready_to_run` reports ``False`` + :raise ValueUnsupportedError: if the + :const:`~COT.platforms.GenericPlatform.BOOTSTRAP_DISK_TYPE` of + the associated VM's + :attr:`~COT.vm_description.VMDescription.platform` is not + 'cdrom' or 'harddisk' + :raise LookupError: if unable to find a disk drive device to inject + the configuration into. """ super(COTInjectConfig, self).run() diff --git a/COT/ovf/hardware.py b/COT/ovf/hardware.py index c84dd09..83c6632 100644 --- a/COT/ovf/hardware.py +++ b/COT/ovf/hardware.py @@ -362,6 +362,10 @@ def _update_cloned_item(self, new_item, new_item_profiles, item_count): item) now exist. Used with :meth:`COT.platform.GenericPlatform.guess_nic_name` :return: Updated :param:`new_item` + :raise NotImplementedError: No support yet for updating ``Address`` + :raise NotImplementedError: If updating ``AddressOnParent`` but the + prior value varies across config profiles. + :raise NotImplementedError: if ``AddressOnParent`` is not an integer. """ resource_type = self.ovf.get_type_from_device(new_item) address = new_item.get(self.ovf.ADDRESS) diff --git a/COT/ovf/item.py b/COT/ovf/item.py index 0f58e03..7167c29 100644 --- a/COT/ovf/item.py +++ b/COT/ovf/item.py @@ -123,6 +123,8 @@ def __getattr__(self, name): :param str name: Attribute name. :return: Value looked up from OVFNameHelper. + :raise AttributeError: Magic methods (``__foo``) will not be passed + through but will raise an AttributeError as usual. """ # Don't pass 'special' attributes through to the helper if re.match(r"^__", name): @@ -171,6 +173,8 @@ def add_item(self, item): :param item: XML ``Item`` element :type item: :class:`xml.etree.ElementTree.Element` + :raise ValueUnsupportedError: if the ``item`` is not a recognized + Item variant. :raise OVFItemDataError: if the new Item conflicts with existing data already in the OVFItem. """ @@ -325,6 +329,8 @@ def _set_existing_property(self, name, value, profiles, overwrite): :param str value: Value to store for this property. :param list profiles: Profiles to which this (name, value) applies. :param bool overwrite: Whether to permit overwriting existing values. + :raise OVFItemDataError: If ``overwrite`` is False and the value is + already set for one or more of the requested ``profiles``. """ for (known_value, profile_set) in list(self.properties[name].items()): if not overwrite and profile_set.intersection(profiles): @@ -424,6 +430,8 @@ def add_profile(self, new_profile, from_item=None): :param str new_profile: Profile name to add :param OVFItem from_item: Item to inherit properties from. If unset, this defaults to ``self``. + :raise RuntimeError: If unable to determine what value to inherit for + a particular property. """ if self.has_profile(new_profile): logger.error("Profile %s already exists under %s!", @@ -552,6 +560,8 @@ def get_value(self, tag, profiles=None): :param profiles: set of profile names, or None :type profiles: set of strings :return: Value string or list, or ``None`` + :raise OVFItemDataError: if :meth:`value_replace_wildcards` failed to + remove any wildcards from the internally stored value. """ val = self._get_value(tag, profiles) val = self.value_replace_wildcards(tag, val, profiles) diff --git a/COT/ovf/name_helper.py b/COT/ovf/name_helper.py index 7cba9c8..b3fd1ec 100644 --- a/COT/ovf/name_helper.py +++ b/COT/ovf/name_helper.py @@ -259,6 +259,7 @@ def __getattr__(self, name): :param str name: Attribute name to look up. :return: Value looked up from :attr:`_raw` and/or :attr:`_cache`. + :raise AttributeError: if the given ``name`` is not found. """ if name in self._item_children: return self._item_children[name] @@ -346,6 +347,7 @@ def item_tag_for_namespace(self, ns): :param str ns: XML namespace :return: 'Item', 'StorageItem', or 'EthernetPortItem' as appropriate. + :raise ValueUnsupportedError: if the namespace is unrecognized """ if ns == self.RASD: return self.ITEM diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index cb56c9a..bf4b184 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -262,6 +262,12 @@ def __init__(self, input_file, output_file): (there will never be an output file) this value should be ``None``; if the output filename is not yet known, use ``""`` and subsequently set :attr:`output_file` when it is determined. + :raise VMInitError: if the OVF descriptor cannot be located + :raise VMInitError: if an XML parsing error occurs + :raise VMInitError: if the XML is not actually an OVF descriptor + :raise VMInitError: if the OVF hardware validation fails + :raise Exception: will call :meth:`destroy` to clean up before + reraising any exception encountered. """ try: self.output_extension = None @@ -726,6 +732,8 @@ def __getattr__(self, name): :param str name: Attribute being looked up. :return: Attribute value + :raise AttributeError: Magic methods (``__foo``) will not be passed + through but will raise an AttributeError as usual. """ # Don't pass 'special' attributes through to the helper if re.match(r"^__", name): @@ -1456,6 +1464,7 @@ def delete_configuration_profile(self, profile): """Delete the profile with the given ID. :param str profile: Profile ID to delete. + :raise LookupError: if the profile does not exist. """ cfg = self.find_child(self.deploy_opt_section, self.CONFIG, attrib={self.CONFIG_ID: profile}) @@ -1743,6 +1752,8 @@ def set_property_value(self, key, value): :param str value: Value to set for this property :return: the (converted) value that was set. :rtype: str + :raise NotImplementedError: if :attr:`ovf_version` is less than 1.0; + OVF version 0.9 is not currently supported. """ if self.ovf_version < 1.0: raise NotImplementedError("No support for setting environment " @@ -1823,6 +1834,8 @@ def search_from_filename(self, filename): :param str filename: Filename to search from :return: ``(file, disk, ctrl_item, disk_item)``, any or all of which may be ``None`` + :raise LookupError: If the ``disk_item`` is found but no ``ctrl_item`` + is found to be its parent. """ file_obj = None disk = None @@ -1864,6 +1877,10 @@ def search_from_file_id(self, file_id): :param str file_id: File ID to search from :return: ``(file, disk, ctrl_item, disk_item)``, any or all of which may be ``None`` + :raise LookupError: If the ``disk`` entry is found but no corresponding + ``file`` is found. + :raise LookupError: If the ``disk_item`` is found but no ``ctrl_item`` + is found to be its parent. """ if file_id is None: return (None, None, None, None) @@ -2196,6 +2213,8 @@ def remove_file(self, file_obj, disk=None, disk_drive=None): :param disk: Disk object referencing :attr:`file` :type disk: xml.etree.ElementTree.Element :param OVFItem disk_drive: Disk drive mapping :attr:`file` to a device + :raise ValueUnsupportedError: If the ``disk_drive`` is a device type + other than 'cdrom' or 'harddisk' """ self.references.remove(file_obj) del self._file_references[file_obj.get(self.FILE_HREF)] @@ -2283,6 +2302,8 @@ def add_controller_device(self, device_type, subtype, address, :return: New or updated controller device object :rtype: OVFItem + + :raise ValueTooHighError: if no more controllers can be created """ if ctrl_item is None: logger.info("Controller not found, adding new Item") @@ -2319,6 +2340,10 @@ def _create_new_disk_device(self, disk_type, address, name, ctrl_item): :param str name: Device name string (optional) :param OVFItem ctrl_item: Controller object to serve as parent :return: (disk_item, disk_name) + :raise ValueTooHighError: if the requested address is out of range + for the given controller, or if the controller is already full. + :raise ValueUnsupportedError: if ``name`` is not specified and + ``disk_type`` is not 'harddisk' or 'cdrom'. """ ctrl_instance = ctrl_item.get_value(self.INSTANCE_ID) if address is None: @@ -2679,6 +2704,7 @@ def find_empty_drive(self, disk_type): :param str disk_type: Either 'cdrom' or 'harddisk' :return: :class:`OVFItem` representing this disk device, or None. + :raise ValueUnsupportedError: if ``disk_type`` is unrecognized. """ if disk_type == 'cdrom': # Find a drive that has no HostResource property @@ -2711,6 +2737,7 @@ def find_device_location(self, device): :param OVFItem device: Hardware device object. :returns: ``(type, address)``, such as ``("ide", "1:0")``. + :raise LookupError: if the controller is not found. """ controller = self.find_parent_from_item(device) if controller is None: diff --git a/COT/platforms.py b/COT/platforms.py index 6fe4ec9..4dd8749 100644 --- a/COT/platforms.py +++ b/COT/platforms.py @@ -143,6 +143,8 @@ def validate_cpu_count(cls, cpus): """Throw an error if the number of CPUs is not a supported value. :param int cpus: Number of CPUs + :raise ValueTooLowError: if ``cpus`` is less than :const:`CPU_MIN` + :raise ValueTooHighError: if ``cpus`` is more than :const:`CPU_MAX` """ validate_int(cpus, cls.CPU_MIN, cls.CPU_MAX, "CPUs") @@ -151,6 +153,10 @@ def validate_memory_amount(cls, mebibytes): """Throw an error if the amount of RAM is not supported. :param int mebibytes: RAM, in MiB. + :raise ValueTooLowError: if :attr:`mebibytes` is less than + :const:`RAM_MIN` + :raise ValueTooHighError: if :attr:`mebibytes` is more than + :const:`RAM_MAX` """ if cls.RAM_MIN is not None and mebibytes < cls.RAM_MIN: if cls.RAM_MIN > 1024 and cls.RAM_MIN % 1024 == 0: @@ -172,6 +178,8 @@ def validate_nic_count(cls, count): """Throw an error if the number of NICs is not supported. :param int count: Number of NICs. + :raise ValueTooLowError: if ``count`` is less than :const:`NIC_MIN` + :raise ValueTooHighError: if ``count`` is more than :const:`NIC_MAX` """ validate_int(count, cls.NIC_MIN, cls.NIC_MAX, "NIC count") @@ -184,6 +192,8 @@ def validate_nic_type(cls, type_string): - :data:`COT.data_validation.NIC_TYPES` :param str type_string: See :data:`COT.data_validation.NIC_TYPES` + :raise 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, @@ -194,6 +204,8 @@ def validate_nic_types(cls, type_list): """Throw an error if any NIC type string in the list is unsupported. :param list type_list: See :data:`COT.data_validation.NIC_TYPES` + :raise 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) @@ -203,6 +215,8 @@ def validate_serial_count(cls, count): """Throw an error if the number of serial ports is not supported. :param int count: Number of serial ports. + :raise ValueTooLowError: if ``count`` is less than :const:`SER_MIN` + :raise ValueTooHighError: if ``count`` is more than :const:`SER_MAX` """ validate_int(count, cls.SER_MIN, cls.SER_MAX, "serial port count") @@ -390,6 +404,10 @@ def validate_cpu_count(cls, cpus): """CSR1000V supports 1, 2, 4, or 8 CPUs. :param int cpus: Number of CPUs. + :raise ValueTooLowError: if ``cpus`` is less than :const:`CPU_MIN` + :raise ValueTooHighError: if ``cpus`` is more than :const:`CPU_MAX` + :raise ValueUnsupportedError: if ``cpus`` is an unsupported value + between :const:`CPU_MIN` and :const:`CPU_MAX` """ validate_int(cpus, 1, 8, "CPUs") if cpus not in [1, 2, 4, 8]: @@ -432,6 +450,10 @@ def validate_memory_amount(cls, mebibytes): """IOSv has minimum 192 MiB (with minimal feature set), max 3 GiB. :param int mebibytes: RAM amount, in MiB. + :raise ValueTooLowError: if :attr:`mebibytes` is less than + :const:`RAM_MIN` + :raise ValueTooHighError: if :attr:`mebibytes` is more than + :const:`RAM_MAX` """ if mebibytes < 192: raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "192 MiB") diff --git a/COT/vm_description.py b/COT/vm_description.py index f02f55c..75d5384 100644 --- a/COT/vm_description.py +++ b/COT/vm_description.py @@ -64,9 +64,10 @@ class VMDescription(object): version_long """ - # Pylint wants to complain about returns documentation for functions that - # raise a NotImplementedError - shush it. + # Many of these methods are abstract interfaces, so quiet, Pylint! + # pylint: disable=missing-raises-doc # pylint: disable=redundant-returns-doc + # pylint: disable=no-self-use, unused-argument @classmethod def detect_type_from_name(cls, filename): @@ -238,9 +239,7 @@ def version_long(self, value): raise NotImplementedError("version_long setter not implemented") # API methods needed for add-disk - def convert_disk_if_needed(self, # pylint: disable=no-self-use - file_path, - kind): # pylint: disable=unused-argument + def convert_disk_if_needed(self, file_path, kind): """Convert the disk to a more appropriate format if needed. :param str file_path: Image to inspect and possibly convert From 2fd6cf4d62b91fcd38d7059279aabbbdc799726e Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 14 Jul 2016 15:16:09 -0400 Subject: [PATCH 06/59] Misc cleanup --- .pylintrc | 5 +++++ COT/add_disk.py | 19 ++++++++----------- COT/data_validation.py | 12 ++++++------ COT/deploy.py | 3 +++ COT/deploy_esxi.py | 5 ++++- COT/submodule.py | 10 ++++------ tox.ini | 12 ++++++------ 7 files changed, 36 insertions(+), 30 deletions(-) diff --git a/.pylintrc b/.pylintrc index 8b324bc..f21dccc 100644 --- a/.pylintrc +++ b/.pylintrc @@ -24,6 +24,10 @@ reports=no # locally-disabled # too-few-public-methods # +# Disabled temporarily +# missing-returns-doc (https://github.com/PyCQA/pylint/pull/1008) +# missing-raises-doc (https://github.com/PyCQA/pylint/pull/1011) +# disable=bad-continuation, duplicate-code, fixme, @@ -66,6 +70,7 @@ max-public-methods=75 [DOCS] accept-no-param-doc=no +accept-no-return-type-doc=yes [FORMAT] diff --git a/COT/add_disk.py b/COT/add_disk.py index 0ada8cb..f385aa3 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -54,8 +54,7 @@ def validate_controller_address(controller, address): :param str controller: ``'ide'`` or ``'scsi'`` :param str address: A string like '0:0' or '2:10' - :raises: :exc:`.InvalidInputError` if the address/controller combo - is invalid. + :raise InvalidInputError: if the address/controller combo is invalid. """ logger.info("validate_controller_address: %s, %s", controller, address) if controller is not None and address is not None: @@ -116,7 +115,7 @@ def __init__(self, ui): def disk_image(self): """Path to disk image file to add to the VM. - :raises: :exc:`.InvalidInputError` if the file does not exist. + :raise InvalidInputError: if the file does not exist. """ return self._disk_image @@ -131,8 +130,7 @@ def disk_image(self, value): def address(self): """Disk device address on controller (``1:0``, etc.). - :raises: :exc:`.InvalidInputError`, - see :meth:`validate_controller_address` + :raise InvalidInputError: see :meth:`validate_controller_address` """ return self._address @@ -146,8 +144,7 @@ def address(self, value): def controller(self): """Disk controller type (``ide``, ``scsi``). - :raises: :exc:`.InvalidInputError`, - see :meth:`validate_controller_address` + :raise InvalidInputError: see :meth:`validate_controller_address` """ return self._controller @@ -287,10 +284,10 @@ def search_for_elements(vm, disk_file, file_id, controller, address): A disk is defined by up to four different sections in the OVF: - File (references the actual disk image file) - Disk (references the File, only used for HD not CD-ROM) - Item (defines the SCSI/IDE controller) - Item (defines the disk drive, links to controller and File or Disk) + * File (references the actual disk image file) + * Disk (references the File, only used for HD not CD-ROM) + * Item (defines the SCSI/IDE controller) + * Item (defines the disk drive, links to controller and File or Disk) For each of these four sections, we need to know whether to add a new one or overwrite an existing one. Depending on the user diff --git a/COT/data_validation.py b/COT/data_validation.py index a1d1765..4375bb5 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -370,9 +370,9 @@ def __str__(self): class ValueTooLowError(ValueUnsupportedError): """A numerical input was less than the lowest supported value. - :ivar value_type: descriptive string - :ivar actual_value: invalid value that was provided - :ivar expected_value: minimum supported value + :param str value_type: descriptive string + :param int actual_value: invalid value that was provided + :param int expected_value: minimum supported value """ def __str__(self): @@ -385,9 +385,9 @@ def __str__(self): class ValueTooHighError(ValueUnsupportedError): """A numerical input was higher than the highest supported value. - :ivar value_type: descriptive string - :ivar actual_value: invalid value that was provided - :ivar expected_value: maximum supported value + :param str value_type: descriptive string + :param int actual_value: invalid value that was provided + :param int expected_value: maximum supported value """ def __str__(self): diff --git a/COT/deploy.py b/COT/deploy.py index 355a2cb..1829d2d 100644 --- a/COT/deploy.py +++ b/COT/deploy.py @@ -160,8 +160,11 @@ def __init__(self, kind, value, options): "kind: %s, value: %s, options: %s", kind, value, options) self.kind = self.validate_kind(kind) + """Connection type string""" self.value = self.validate_value(self.kind, value) + """Connection value such as '/dev/ttyS0' or '1.1.1.1:80'""" self.options = self.validate_options(self.kind, self.value, options) + """Dictionary of connection options.""" def __str__(self): """Represent SerialConnection object as a string.""" diff --git a/COT/deploy_esxi.py b/COT/deploy_esxi.py index 9377c59..3e12b8e 100644 --- a/COT/deploy_esxi.py +++ b/COT/deploy_esxi.py @@ -76,6 +76,9 @@ def __enter__(self): Unlike SmartConnection, this lets the user override SSL certificate validation failures and connect anyway. It also produces slightly more meaningful error messages on failure. + + :raise vim.fault.HostConnectFault: + :raise requests.exceptions.ConnectionError: """ logger.verbose("Establishing connection to %s:%s...", self.server, self.port) @@ -157,7 +160,7 @@ def get_object_from_connection(conn, vimtype, name): :param conn: Connection to ESXi. :type conn: :class:`SmarterConnection` - :param object vimtype: currently only `vim.VirtualMachine`` + :param object vimtype: currently only ``vim.VirtualMachine`` :param str name: Name of the object to look up. :return: Located object """ diff --git a/COT/submodule.py b/COT/submodule.py index 012dc89..3b05c45 100644 --- a/COT/submodule.py +++ b/COT/submodule.py @@ -68,8 +68,7 @@ def ready_to_run(self): # pylint: disable=no-self-use def run(self): """Do the actual work of this submodule. - :raises: :exc:`.InvalidInputError` if :meth:`ready_to_run` - reports ``False`` + :raise InvalidInputError: if :meth:`ready_to_run` reports ``False`` """ (ready, reason) = self.ready_to_run() if not ready: @@ -122,7 +121,7 @@ def package(self): Calls :meth:`COT.vm_factory.VMFactory.create` to instantiate :attr:`self.vm` from the provided file. - :raises: :exc:`.InvalidInputError` if the file does not exist. + :raise InvalidInputError: if the file does not exist. """ return self._package @@ -178,7 +177,7 @@ def package(self): Calls :meth:`COT.vm_factory.VMFactory.create` to instantiate :attr:`self.vm` from the provided file. - :raises: :exc:`.InvalidInputError` if the file does not exist. + :raise InvalidInputError: if the file does not exist. """ return self._package @@ -228,8 +227,7 @@ def run(self): If :attr:`output` was not previously set, automatically sets it to the value of :attr:`PACKAGE`. - :raises: :exc:`.InvalidInputError` if :meth:`ready_to_run` - reports ``False`` + :raise InvalidInputError: if :meth:`ready_to_run` reports ``False`` """ super(COTSubmodule, self).run() diff --git a/tox.ini b/tox.ini index 93cb509..16d6a75 100644 --- a/tox.ini +++ b/tox.ini @@ -19,12 +19,12 @@ envlist = stats [tox:travis] -2.6 = setup, py26, stats -2.7 = setup, py27, flake8, pylint, docs, stats -3.3 = setup, py33, pylint, stats -3.4 = setup, py34, flake8, pylint, docs, stats -3.5 = setup, py35, pylint, stats -PyPy = setup, pypy, stats +2.6 = setup, py26, stats +2.7 = setup, flake8, pylint, py27, docs, stats +3.3 = setup, pylint, py33, stats +3.4 = setup, flake8, pylint, py34, docs, stats +3.5 = setup, pylint, py35, stats +PyPy = setup, pypy, stats [testenv] passenv = PREFIX From 92e97fab1fc06880d30a9b626074b7065d57fccf Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 14 Jul 2016 21:14:42 -0400 Subject: [PATCH 07/59] Actually configure intersphinx to link to Python stdlib --- docs/conf.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 267be88..d048683 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -262,6 +262,13 @@ def autodoc_skip_member(app, what, name, obj, skip, options): return False return skip +# -- Intersphinx configuration ---------------- + +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'requests': ('http://docs.python-requests.org/en/latest', None), +} + # -- General configuration, continued --------- # Add any paths that contain templates here, relative to this directory. From 442ecd9975685b52010fb73e39df2e1da4c7077d Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Sun, 17 Jul 2016 22:12:09 -0400 Subject: [PATCH 08/59] s/:raise /:raises /g --- COT/add_disk.py | 13 ++++++----- COT/add_file.py | 2 +- COT/cli.py | 4 ++-- COT/data_validation.py | 22 +++++++++--------- COT/deploy.py | 14 ++++++------ COT/deploy_esxi.py | 6 ++--- COT/edit_properties.py | 2 +- COT/file_reference.py | 6 ++--- COT/helpers/api.py | 10 ++++----- COT/helpers/helper.py | 24 ++++++++++---------- COT/helpers/ovftool.py | 6 ++--- COT/helpers/qemu_img.py | 6 ++--- COT/helpers/vmdktool.py | 2 +- COT/inject_config.py | 12 +++++----- COT/install_helpers.py | 4 ++-- COT/ovf/hardware.py | 10 ++++----- COT/ovf/item.py | 17 +++++++------- COT/ovf/name_helper.py | 4 ++-- COT/ovf/ovf.py | 50 ++++++++++++++++++++--------------------- COT/platforms.py | 30 ++++++++++++------------- COT/submodule.py | 8 +++---- COT/ui_shared.py | 4 ++-- COT/vm_description.py | 5 +++-- COT/vm_factory.py | 4 ++-- COT/xml_file.py | 4 ++-- 25 files changed, 137 insertions(+), 132 deletions(-) diff --git a/COT/add_disk.py b/COT/add_disk.py index f385aa3..0a9e192 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -54,7 +54,7 @@ def validate_controller_address(controller, address): :param str controller: ``'ide'`` or ``'scsi'`` :param str address: A string like '0:0' or '2:10' - :raise InvalidInputError: if the address/controller combo is invalid. + :raises InvalidInputError: if the address/controller combo is invalid. """ logger.info("validate_controller_address: %s, %s", controller, address) if controller is not None and address is not None: @@ -115,7 +115,7 @@ def __init__(self, ui): def disk_image(self): """Path to disk image file to add to the VM. - :raise InvalidInputError: if the file does not exist. + :raises InvalidInputError: if the file does not exist. """ return self._disk_image @@ -130,7 +130,7 @@ def disk_image(self, value): def address(self): """Disk device address on controller (``1:0``, etc.). - :raise InvalidInputError: see :meth:`validate_controller_address` + :raises InvalidInputError: see :meth:`validate_controller_address` """ return self._address @@ -144,7 +144,7 @@ def address(self, value): def controller(self): """Disk controller type (``ide``, ``scsi``). - :raise InvalidInputError: see :meth:`validate_controller_address` + :raises InvalidInputError: see :meth:`validate_controller_address` """ return self._controller @@ -258,7 +258,8 @@ def guess_disk_type_from_extension(disk_file): """Guess the disk type (harddisk/cdrom) from the disk file name. :param str disk_file: File name or file path. - :return: "cdrom" or "harrdisk" + :return: "cdrom" or "harddisk" + :raises InvalidInputError: if the disk type cannot be guessed. """ disk_extension = os.path.splitext(disk_file)[1] ext_type_map = { @@ -350,6 +351,8 @@ def guess_controller_type(vm, ctrl_item, disk_type): :param object ctrl_item: Any known controller object :param str disk_type: "cdrom" or "harddisk" :return: 'ide' or 'scsi' + :raises ValueUnsupportedError: if ``ctrl_item`` is not an IDE or SCSI + controller device. """ if ctrl_item is None: # If the user didn't tell us which controller type they wanted, diff --git a/COT/add_file.py b/COT/add_file.py index a782f8e..3b86f32 100644 --- a/COT/add_file.py +++ b/COT/add_file.py @@ -57,7 +57,7 @@ def __init__(self, ui): def file(self): """File to be added to the package. - :raises: :exc:`.InvalidInputError` if the file does not exist. + :raises InvalidInputError: if the file does not exist. """ return self._file diff --git a/COT/cli.py b/COT/cli.py index 4b564fa..52c46af 100644 --- a/COT/cli.py +++ b/COT/cli.py @@ -389,7 +389,7 @@ def get_password(self, username, host): :param str username: Username the password is associated with :param str host: Host the password is associated with - :raise InvalidInputError: if :attr:`force` is ``True`` + :raises InvalidInputError: if :attr:`force` is ``True`` (as there is no "default" password value) :return: Password string """ @@ -570,7 +570,7 @@ def set_instance_attributes(arg_dict): """Set attributes of the :attr:`instance` based on the given arg_dict. :param dict arg_dict: Dictionary of (attribute, value). - :raise InvalidInputError: if attributes are not validly set. + :raises InvalidInputError: if attributes are not validly set. """ # Set mandatory (CAPITALIZED) args first, then optional args for (arg, value) in arg_dict.items(): diff --git a/COT/data_validation.py b/COT/data_validation.py index 4375bb5..12eb2b3 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -108,7 +108,7 @@ def match_or_die(first_label, first, second_label, second): :param object first: First object to compare :param str second_label: Descriptive label for :attr:`second` :param object second: Second object to compare - :raise ValueMismatchError: if ``first != second`` + :raises ValueMismatchError: if ``first != second`` """ if first != second: raise ValueMismatchError("{0} {1} does not match {2} {3}" @@ -126,7 +126,7 @@ def canonicalize_helper(label, user_input, mappings, re_flags=0): :param list mappings: List of ``(expr, canonical)`` pairs for mapping. :param int re_flags: ``re.IGNORECASE``, etc. if desired :returns: The canonical string - :raise ValueUnsupportedError: If no ``expr`` in ``mappings`` matches + :raises ValueUnsupportedError: If no ``expr`` in ``mappings`` matches ``input``. """ if user_input is None or user_input == "": @@ -146,7 +146,7 @@ def canonicalize_ide_subtype(subtype): - ``PIIX4`` - ``virtio`` - :raise ValueUnsupportedError: If the canonical string cannot be determined + :raises ValueUnsupportedError: If the canonical string cannot be determined """ return canonicalize_helper("IDE controller subtype", subtype, [ @@ -173,7 +173,7 @@ def canonicalize_nic_subtype(subtype): :param str subtype: User-provided string :returns: The canonical string, one of :data:`NIC_TYPES` - :raise ValueUnsupportedError: If the canonical string cannot be determined + :raises ValueUnsupportedError: If the canonical string cannot be determined .. seealso:: :meth:`COT.platforms.GenericPlatform.validate_nic_type` @@ -194,7 +194,7 @@ def canonicalize_scsi_subtype(subtype): - ``virtio`` - ``VirtualSCSI`` - :raise ValueUnsupportedError: If the canonical string cannot be determined + :raises ValueUnsupportedError: If the canonical string cannot be determined """ return canonicalize_helper("SCSI controller subtype", subtype, [ @@ -241,7 +241,7 @@ def mac_address(string): * xxxx.xxxx.xxxx :param str string: String to validate - :raise InvalidInputError: if string is not a valid MAC address + :raises InvalidInputError: if string is not a valid MAC address :return: Validated string(with leading/trailing whitespace stripped) """ string = string.strip() @@ -260,7 +260,7 @@ def device_address(string): Validate string is an appropriately formed device address such as '1:0'. :param str string: String to validate - :raise InvalidInputError: if string is not a well-formatted device address + :raises InvalidInputError: if string is not a well-formatted device address :return: Validated string (with leading/trailing whitespace stripped) """ string = string.strip() @@ -274,7 +274,7 @@ def no_whitespace(string): """Parser helper function for arguments not allowed to contain whitespace. :param str string: String to validate - :raise InvalidInputError: if string contains internal whitespace + :raises InvalidInputError: if string contains internal whitespace :return: Validated string (with leading/trailing whitespace stripped) """ string = string.strip() @@ -294,9 +294,9 @@ def validate_int(string, :param int maximum: Maximum valid value (optional) :param str label: Label to include in any errors raised :return: Validated integer value - :raise ValueUnsupportedError: if :attr:`string` can't be converted to int - :raise ValueTooLowError: if value is less than :attr:`minimum` - :raise ValueTooHighError: if value is more than :attr:`maximum` + :raises ValueUnsupportedError: if :attr:`string` can't be converted to int + :raises ValueTooLowError: if value is less than :attr:`minimum` + :raises ValueTooHighError: if value is more than :attr:`maximum` """ try: i = int(string) diff --git a/COT/deploy.py b/COT/deploy.py index 1829d2d..fee1cf5 100644 --- a/COT/deploy.py +++ b/COT/deploy.py @@ -53,7 +53,7 @@ def from_cli_string(cls, cli_string): '' :return: SerialConnection instance or None. - :raise InvalidInputError: if ``cli_string`` cannot be parsed + :raises InvalidInputError: if ``cli_string`` cannot be parsed """ if cli_string is None: return None @@ -86,7 +86,7 @@ def validate_kind(cls, kind): :param str kind: Connection type string, possibly in need of munging. :return: A valid type string - :raise ValueUnsupportedError: if type string is not recognized as valid + :raises ValueUnsupportedError: if ``kind`` is not recognized as valid """ kind = kind.lower() if kind == '': @@ -110,8 +110,8 @@ def validate_value(cls, kind, value): :param str kind: Connection type, valid per :func:`validate_kind`. :param str value: Connection value such as '/dev/ttyS0' or '1.1.1.1:80' :return: Munged value string. - :raise InvalidInputError: if value string is not recognized as valid - :raise NotImplementedError: if ``kind`` is not valid + :raises InvalidInputError: if value string is not recognized as valid + :raises NotImplementedError: if ``kind`` is not valid """ if kind == 'device' or kind == 'file' or kind == 'pipe': # TODO: Validate that device path exists on target? @@ -141,7 +141,7 @@ def validate_options(cls, kind, _value, options): :param str _value: Validated 'value' string. Currently unused. :param dict options: Input options dictionary. :return: validated options dict - :raise InvalidInputError: if options are not valid. + :raises InvalidInputError: if options are not valid. """ if kind == 'file': if 'datastore' not in options: @@ -234,7 +234,7 @@ def __init__(self, ui): def hypervisor(self): """Hypervisor to deploy to. - :raise: :exc:`InvalidInputError` if not a recognized value. + :raises InvalidInputError: if not a recognized value. """ return self._hypervisor @@ -249,7 +249,7 @@ def hypervisor(self, value): def configuration(self): """VM configuration profile to use for deployment. - :raise: :exc:`InvalidInputError` if not a profile defined in the VM. + :raises InvalidInputError: if not a profile defined in the VM. """ return self._configuration diff --git a/COT/deploy_esxi.py b/COT/deploy_esxi.py index 3e12b8e..cacebcc 100644 --- a/COT/deploy_esxi.py +++ b/COT/deploy_esxi.py @@ -77,8 +77,8 @@ def __enter__(self): validation failures and connect anyway. It also produces slightly more meaningful error messages on failure. - :raise vim.fault.HostConnectFault: - :raise requests.exceptions.ConnectionError: + :raises vim.fault.HostConnectFault: + :raises requests.exceptions.ConnectionError: """ logger.verbose("Establishing connection to %s:%s...", self.server, self.port) @@ -412,7 +412,7 @@ def fixup_serial_ports(self, serial_count): """Use PyVmomi to create and configure serial ports for the new VM. :param int serial_count: Number of serial ports desired. - :raise NotImplementedError: If any + :raises NotImplementedError: If any :class:`~COT.deploy.SerialConnection` in :attr:`serial_connection` has a :attr:`~COT.deploy.SerialConnection.kind` other than 'tcp', 'telnet', or 'device' diff --git a/COT/edit_properties.py b/COT/edit_properties.py index 56765ed..1e5f826 100644 --- a/COT/edit_properties.py +++ b/COT/edit_properties.py @@ -63,7 +63,7 @@ def __init__(self, ui): def config_file(self): """Path to plaintext file to read configuration lines from. - :raise: :exc:`InvalidInputError` if the file does not exist. + :raises InvalidInputError: if the file does not exist. """ return self._config_file diff --git a/COT/file_reference.py b/COT/file_reference.py index c203eda..ea20dd3 100644 --- a/COT/file_reference.py +++ b/COT/file_reference.py @@ -44,7 +44,7 @@ def __init__(self, file_path, filename=None): >>> a == b True - :raise IOError: if no such file exists + :raises IOError: if no such file exists """ if filename is None: self.file_path = file_path @@ -126,7 +126,7 @@ def __init__(self, tarfile_path, filename): :param str tarfile_path: Path to TAR archive to read :param str filename: File name in the TAR archive. - :raise IOError: if ``tarfile_path`` doesn't reference a TAR file, + :raises IOError: if ``tarfile_path`` doesn't reference a TAR file, or the TAR file does not contain ``filename``. """ if not tarfile.is_tarfile(tarfile_path): @@ -182,7 +182,7 @@ def open(self, mode): :param str mode: Only 'r' and 'rb' modes are supported. :return: File object - :raise ValueError: if ``mode`` is not valid. + :raises ValueError: if ``mode`` is not valid. """ # We can only extract a file object from a TAR file in read mode. if mode != 'r' and mode != 'rb': diff --git a/COT/helpers/api.py b/COT/helpers/api.py index 6f2de81..5da8720 100644 --- a/COT/helpers/api.py +++ b/COT/helpers/api.py @@ -135,7 +135,7 @@ def get_checksum(path_or_obj, checksum_type): :param str checksum_type: Supported values are 'md5' and 'sha1'. :return: Hexadecimal file checksum :rtype: str - :raise NotImplementedError: if ``checksum_type`` is not a supported value. + :raises NotImplementedError: if ``checksum_type`` is not a supported value. """ # pylint: disable=redefined-variable-type if checksum_type == 'md5': @@ -179,7 +179,7 @@ def get_disk_format(file_path): * ``format`` may be ``'vmdk'``, ``'raw'``, or ``'qcow2'`` * ``subformat`` may be ``None``, or various strings for ``'vmdk'`` files. - :raise RuntimeError: if unable to identify the sub-format of a VMDK file. + :raises RuntimeError: if unable to identify the sub-format of a VMDK file. """ file_format = QEMUIMG.get_disk_format(file_path) @@ -232,7 +232,7 @@ def convert_disk_image(file_path, output_dir, new_format, new_subformat=None): * :attr:`file_path`, if no conversion was required * or a file path in :attr:`output_dir` containing the converted image - :raise NotImplementedError: if the :attr:`new_format` and/or + :raises NotImplementedError: if the :attr:`new_format` and/or :attr:`new_subformat` are not supported conversion targets. """ curr_format, curr_subformat = get_disk_format(file_path) @@ -299,9 +299,9 @@ def create_disk_image(file_path, file_format=None, :param str capacity: Disk capacity. A string like '16M' or '1G'. :param list contents: List of file paths to package into the created image. If not specified, the image will be left blank and unformatted. - :raise RuntimeError: if neither :attr:`capacity` nor :attr:`contents` is + :raises RuntimeError: if neither :attr:`capacity` nor :attr:`contents` is specified - :raise NotImplementedError: if the :attr:`file_format` is not supported. + :raises NotImplementedError: if the :attr:`file_format` is not supported. """ if not capacity and not contents: raise RuntimeError("Either capacity or contents must be specified!") diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index a4b5c44..bbf8c80 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -42,7 +42,7 @@ def guess_file_format_from_path(file_path): :param str file_path: Filename or file path. :return: Guessed file format :rtype: str - :raise RuntimeError: if unable to guess + :raises RuntimeError: if unable to guess """ file_format = os.path.splitext(file_path)[1][1:] if not file_format: @@ -196,9 +196,9 @@ def make_install_dir(cls, directory, permissions=493): # 493 == 0o755 :param str directory: Directory to check/create. :param int permissions: Permissions to set on the created directory. :return: True - :raise RuntimeError: if something other than a directory already + :raises RuntimeError: if something other than a directory already exists at the path referenced by ``directory``. - :raise OSError: if directory creation failed + :raises OSError: if directory creation failed """ if os.path.isdir(directory): # TODO: permissions check, update permissions if needed @@ -301,8 +301,8 @@ def call_helper(self, args, capture_output=True, require_success=True): raised if the helper exits with a non-zero status code. :return: Captured stdout/stderr (if :attr:`capture_output`), else ``None``. - :raise HelperNotFoundError: if the helper was not previously installed, - and the user declines to install it at this time. + :raises HelperNotFoundError: if the helper was not previously + installed, and the user declines to install it at this time. """ if not self.path: if not self.confirm("{0} does not appear to be installed.\n" @@ -338,7 +338,7 @@ def should_not_be_installed_but_is(self): def install_helper(self): """Install the helper program (abstract method). - :raise: :exc:`NotImplementedError` as this method must be implemented + :raises NotImplementedError: as this method must be implemented by a concrete subclass. """ if self.should_not_be_installed_but_is(): @@ -363,12 +363,12 @@ def _check_call(cls, args, require_success=True, retry_with_sudo=False, an exception, prepend ``sudo`` to the command and try again. :param kwargs: Arguments passed to :func:`subprocess.check_call`. - :raise HelperNotFoundError: if the command doesn't exist + :raises HelperNotFoundError: if the command doesn't exist (instead of a :class:`OSError`) - :raise HelperError: if :attr:`require_success` is not ``False`` and + :raises HelperError: if :attr:`require_success` is not ``False`` and the command returns a value other than 0 (instead of a :class:`CalledProcessError`). - :raise OSError: as :func:`subprocess.check_call`. + :raises OSError: as :func:`subprocess.check_call`. """ cmd = args[0] logger.info("Calling '%s'...", " ".join(args)) @@ -415,12 +415,12 @@ def _check_output(cls, args, require_success=True, **kwargs): :return: Captured stdout/stderr from the command - :raise HelperNotFoundError: if the command doesn't exist + :raises HelperNotFoundError: if the command doesn't exist (instead of a :class:`OSError`) - :raise HelperError: if :attr:`require_success` is not ``False`` and + :raises HelperError: if :attr:`require_success` is not ``False`` and the command returns a value other than 0 (instead of a :class:`CalledProcessError`). - :raise OSError: as :func:`subprocess.check_call`. + :raises OSError: as :func:`subprocess.check_call`. """ cmd = args[0] logger.info("Calling '%s' and capturing its output...", " ".join(args)) diff --git a/COT/helpers/ovftool.py b/COT/helpers/ovftool.py index 737c359..c730cb1 100644 --- a/COT/helpers/ovftool.py +++ b/COT/helpers/ovftool.py @@ -48,7 +48,7 @@ def __init__(self): def install_helper(self): """Install ``ovftool``. - :raise: :exc:`NotImplementedError` as VMware does not currently provide + :raises NotImplementedError: as VMware does not currently provide any mechanism for automatic download of ovftool. """ if self.should_not_be_installed_but_is(): @@ -66,7 +66,7 @@ def validate_ovf(self, ovf_file): :param str ovf_file: File to validate :return: Output from ``ovftool`` - :raise HelperNotFoundError: if ``ovftool`` is not found. - :raise HelperError: if ``ovftool`` regards the file as invalid + :raises HelperNotFoundError: if ``ovftool`` is not found. + :raises HelperError: if ``ovftool`` regards the file as invalid """ return self.call_helper(['--schemaValidate', ovf_file]) diff --git a/COT/helpers/qemu_img.py b/COT/helpers/qemu_img.py index 3cce29a..e4cf7a8 100644 --- a/COT/helpers/qemu_img.py +++ b/COT/helpers/qemu_img.py @@ -73,7 +73,7 @@ def get_disk_format(self, file_path): :param str file_path: Path to disk image file to inspect. :return: Disk image format (``'vmdk'``, ``'raw'``, ``'qcow2'``, etc.) - :raise RuntimeError: if unable to parse the qemu-img output. + :raises RuntimeError: if unable to parse the qemu-img output. """ output = self.call_helper(['info', file_path]) # Read the format from the output @@ -93,7 +93,7 @@ def get_disk_capacity(self, file_path): :param str file_path: Path to disk image file to inspect :return: Disk capacity, in bytes :rtype: str - :raise RuntimeError: if unable to parse the qemu-img output. + :raises RuntimeError: if unable to parse the qemu-img output. """ output = self.call_helper(['info', file_path]) match = re.search(r"(\d+) bytes", output) @@ -127,7 +127,7 @@ def convert_disk_image(self, file_path, output_dir, * :attr:`file_path`, if no conversion was required * or a file path in :attr:`output_dir` containing the converted image - :raise NotImplementedError: if the :attr:`new_format` and/or + :raises NotImplementedError: if the :attr:`new_format` and/or :attr:`new_subformat` are not supported conversion targets. """ file_name = os.path.basename(file_path) diff --git a/COT/helpers/vmdktool.py b/COT/helpers/vmdktool.py index c660c43..2f7fefc 100644 --- a/COT/helpers/vmdktool.py +++ b/COT/helpers/vmdktool.py @@ -127,7 +127,7 @@ def convert_disk_image(self, file_path, output_dir, * :attr:`file_path`, if no conversion was required * or a file path in :attr:`output_dir` containing the converted image - :raise NotImplementedError: if the :attr:`new_format` and/or + :raises NotImplementedError: if the :attr:`new_format` and/or :attr:`new_subformat` are not supported conversion targets. """ file_name = os.path.basename(file_path) diff --git a/COT/inject_config.py b/COT/inject_config.py index ac59d0d..063bacf 100644 --- a/COT/inject_config.py +++ b/COT/inject_config.py @@ -55,8 +55,8 @@ def __init__(self, ui): def config_file(self): """Primary configuration file. - :raise InvalidInputError: if the file does not exist - :raise InvalidInputError: if the `platform described by + :raises InvalidInputError: if the file does not exist + :raises InvalidInputError: if the `platform described by :attr:`package` doesn't support configuration files. """ return self._config_file @@ -77,8 +77,8 @@ def config_file(self, value): def secondary_config_file(self): """Secondary configuration file. - :raise InvalidInputError: if the file does not exist - :raise InvalidInputError: if the platform described by + :raises InvalidInputError: if the file does not exist + :raises InvalidInputError: if the platform described by :attr:`package` doesn't support secondary configuration files. """ return self._secondary_config_file @@ -115,12 +115,12 @@ def run(self): """Do the actual work of this submodule. :raises InvalidInputError: if :func:`ready_to_run` reports ``False`` - :raise ValueUnsupportedError: if the + :raises ValueUnsupportedError: if the :const:`~COT.platforms.GenericPlatform.BOOTSTRAP_DISK_TYPE` of the associated VM's :attr:`~COT.vm_description.VMDescription.platform` is not 'cdrom' or 'harddisk' - :raise LookupError: if unable to find a disk drive device to inject + :raises LookupError: if unable to find a disk drive device to inject the configuration into. """ super(COTInjectConfig, self).run() diff --git a/COT/install_helpers.py b/COT/install_helpers.py index eb5fc86..659e378 100644 --- a/COT/install_helpers.py +++ b/COT/install_helpers.py @@ -83,8 +83,8 @@ def _install_manpage(src_path, man_dir): :param str src_path: Path to manual page file. :param str man_dir: Base directory where page should be installed. :return: (page_previously_installed, page_updated) - :raise IOError: if installation fails under some circumstances - :raise OSError: if installation fails under other circumstances + :raises IOError: if installation fails under some circumstances + :raises OSError: if installation fails under other circumstances """ # Which man section does this belong in? f = os.path.basename(src_path) diff --git a/COT/ovf/hardware.py b/COT/ovf/hardware.py index 83c6632..54c02fd 100644 --- a/COT/ovf/hardware.py +++ b/COT/ovf/hardware.py @@ -54,7 +54,7 @@ def __init__(self, ovf): :param ovf: OVF instance to extract hardware information from. :type ovf: :class:`~COT.ovf.ovf.OVF` - :raise OVFHardwareDataError: if any data errors are seen + :raises OVFHardwareDataError: if any data errors are seen """ self.ovf = ovf self.item_dict = {} @@ -255,7 +255,7 @@ def find_item(self, resource_type=None, properties=None, profile=None): :type properties: dict[property, value] :param str profile: Single profile ID to search within :return: Matching :class:`OVFItem` instance, or None - :raise LookupError: if more than one such Item exists. + :raises LookupError: if more than one such Item exists. """ matches = self.find_all_items(resource_type, properties, [profile]) if len(matches) > 1: @@ -362,10 +362,10 @@ def _update_cloned_item(self, new_item, new_item_profiles, item_count): item) now exist. Used with :meth:`COT.platform.GenericPlatform.guess_nic_name` :return: Updated :param:`new_item` - :raise NotImplementedError: No support yet for updating ``Address`` - :raise NotImplementedError: If updating ``AddressOnParent`` but the + :raises NotImplementedError: No support yet for updating ``Address`` + :raises NotImplementedError: If updating ``AddressOnParent`` but the prior value varies across config profiles. - :raise NotImplementedError: if ``AddressOnParent`` is not an integer. + :raises NotImplementedError: if ``AddressOnParent`` is not an integer. """ resource_type = self.ovf.get_type_from_device(new_item) address = new_item.get(self.ovf.ADDRESS) diff --git a/COT/ovf/item.py b/COT/ovf/item.py index 7167c29..d5102f5 100644 --- a/COT/ovf/item.py +++ b/COT/ovf/item.py @@ -123,7 +123,7 @@ def __getattr__(self, name): :param str name: Attribute name. :return: Value looked up from OVFNameHelper. - :raise AttributeError: Magic methods (``__foo``) will not be passed + :raises AttributeError: Magic methods (``__foo``) will not be passed through but will raise an AttributeError as usual. """ # Don't pass 'special' attributes through to the helper @@ -173,9 +173,9 @@ def add_item(self, item): :param item: XML ``Item`` element :type item: :class:`xml.etree.ElementTree.Element` - :raise ValueUnsupportedError: if the ``item`` is not a recognized + :raises ValueUnsupportedError: if the ``item`` is not a recognized Item variant. - :raise OVFItemDataError: if the new Item conflicts with existing data + :raises OVFItemDataError: if the new Item conflicts with existing data already in the OVFItem. """ logger.debug("Adding new %s", item.tag) @@ -329,7 +329,7 @@ def _set_existing_property(self, name, value, profiles, overwrite): :param str value: Value to store for this property. :param list profiles: Profiles to which this (name, value) applies. :param bool overwrite: Whether to permit overwriting existing values. - :raise OVFItemDataError: If ``overwrite`` is False and the value is + :raises OVFItemDataError: If ``overwrite`` is False and the value is already set for one or more of the requested ``profiles``. """ for (known_value, profile_set) in list(self.properties[name].items()): @@ -381,7 +381,7 @@ def set_property(self, name, value, profiles=None, overwrite=True): :param boolean overwrite: Whether to permit overwriting of existing value set in this item. - :raise OVFItemDataError: if a value is already defined and would be + :raises OVFItemDataError: if a value is already defined and would be overwritten, unless :attr:`overwrite` is ``True`` """ # A ResourceSubType in the XML can be a single value or a @@ -430,7 +430,7 @@ def add_profile(self, new_profile, from_item=None): :param str new_profile: Profile name to add :param OVFItem from_item: Item to inherit properties from. If unset, this defaults to ``self``. - :raise RuntimeError: If unable to determine what value to inherit for + :raises RuntimeError: If unable to determine what value to inherit for a particular property. """ if self.has_profile(new_profile): @@ -560,7 +560,7 @@ def get_value(self, tag, profiles=None): :param profiles: set of profile names, or None :type profiles: set of strings :return: Value string or list, or ``None`` - :raise OVFItemDataError: if :meth:`value_replace_wildcards` failed to + :raises OVFItemDataError: if :meth:`value_replace_wildcards` failed to remove any wildcards from the internally stored value. """ val = self._get_value(tag, profiles) @@ -590,7 +590,8 @@ def validate(self): Also clean up any oddities (like a property value assigned to 'all profiles' and also redundantly to a specific profile). - :raise RuntimeError: if validation fails and self-repair is impossible. + :raises RuntimeError: if validation fails and COT doesn't know + how to automatically repair the error(s) identified. """ # An OVFItem must describe only one InstanceID # All Items with a given InstanceID must have the same ResourceType diff --git a/COT/ovf/name_helper.py b/COT/ovf/name_helper.py index b3fd1ec..f9a196f 100644 --- a/COT/ovf/name_helper.py +++ b/COT/ovf/name_helper.py @@ -259,7 +259,7 @@ def __getattr__(self, name): :param str name: Attribute name to look up. :return: Value looked up from :attr:`_raw` and/or :attr:`_cache`. - :raise AttributeError: if the given ``name`` is not found. + :raises AttributeError: if the given ``name`` is not found. """ if name in self._item_children: return self._item_children[name] @@ -347,7 +347,7 @@ def item_tag_for_namespace(self, ns): :param str ns: XML namespace :return: 'Item', 'StorageItem', or 'EthernetPortItem' as appropriate. - :raise ValueUnsupportedError: if the namespace is unrecognized + :raises ValueUnsupportedError: if the namespace is unrecognized """ if ns == self.RASD: return self.ITEM diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index bf4b184..539064d 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -211,7 +211,7 @@ def detect_type_from_name(filename): :param str filename: File name/path :return: '.ovf' or '.ova' - :raise ValueUnsupportedError: if filename doesn't match ovf/ova + :raises ValueUnsupportedError: if filename doesn't match ovf/ova """ # We don't care about any directory path filename = os.path.basename(filename) @@ -262,11 +262,11 @@ def __init__(self, input_file, output_file): (there will never be an output file) this value should be ``None``; if the output filename is not yet known, use ``""`` and subsequently set :attr:`output_file` when it is determined. - :raise VMInitError: if the OVF descriptor cannot be located - :raise VMInitError: if an XML parsing error occurs - :raise VMInitError: if the XML is not actually an OVF descriptor - :raise VMInitError: if the OVF hardware validation fails - :raise Exception: will call :meth:`destroy` to clean up before + :raises VMInitError: if the OVF descriptor cannot be located + :raises VMInitError: if an XML parsing error occurs + :raises VMInitError: if the XML is not actually an OVF descriptor + :raises VMInitError: if the OVF hardware validation fails + :raises Exception: will call :meth:`destroy` to clean up before reraising any exception encountered. """ try: @@ -392,7 +392,7 @@ def _init_check_file_entries(self): def output_file(self): """OVF or OVA file that will be created or updated by :meth:`write`. - :raise ValueUnsupportedError: if :func:`detect_type_from_name` fails + :raises ValueUnsupportedError: if :func:`detect_type_from_name` fails """ return super(OVF, self).output_file @@ -732,7 +732,7 @@ def __getattr__(self, name): :param str name: Attribute being looked up. :return: Attribute value - :raise AttributeError: Magic methods (``__foo``) will not be passed + :raises AttributeError: Magic methods (``__foo``) will not be passed through but will raise an AttributeError as usual. """ # Don't pass 'special' attributes through to the helper @@ -1464,7 +1464,7 @@ def delete_configuration_profile(self, profile): """Delete the profile with the given ID. :param str profile: Profile ID to delete. - :raise LookupError: if the profile does not exist. + :raises LookupError: if the profile does not exist. """ cfg = self.find_child(self.deploy_opt_section, self.CONFIG, attrib={self.CONFIG_ID: profile}) @@ -1705,7 +1705,7 @@ def _validate_value_for_property(self, prop, value): :param prop: Existing Property element. :type prop: :class:`xml.etree.ElementTree.Element` :param str value: Proposed value to set for this property. - :raise ValueUnsupportedError: if the value does not meet criteria. + :raises ValueUnsupportedError: if the value does not meet criteria. :return: the value, potentially canonicalized. :rtype: str """ @@ -1752,7 +1752,7 @@ def set_property_value(self, key, value): :param str value: Value to set for this property :return: the (converted) value that was set. :rtype: str - :raise NotImplementedError: if :attr:`ovf_version` is less than 1.0; + :raises NotImplementedError: if :attr:`ovf_version` is less than 1.0; OVF version 0.9 is not currently supported. """ if self.ovf_version < 1.0: @@ -1783,7 +1783,7 @@ def set_property_value(self, key, value): def config_file_to_properties(self, file_path): """Import each line of a text file into a configuration property. - :raise NotImplementedError: if the :attr:`platform` for this OVF + :raises NotImplementedError: if the :attr:`platform` for this OVF does not define :const:`~COT.platforms.GenericPlatform.LITERAL_CLI_STRING` :param str file_path: File name to import. @@ -1834,7 +1834,7 @@ def search_from_filename(self, filename): :param str filename: Filename to search from :return: ``(file, disk, ctrl_item, disk_item)``, any or all of which may be ``None`` - :raise LookupError: If the ``disk_item`` is found but no ``ctrl_item`` + :raises LookupError: If the ``disk_item`` is found but no ``ctrl_item`` is found to be its parent. """ file_obj = None @@ -1877,9 +1877,9 @@ def search_from_file_id(self, file_id): :param str file_id: File ID to search from :return: ``(file, disk, ctrl_item, disk_item)``, any or all of which may be ``None`` - :raise LookupError: If the ``disk`` entry is found but no corresponding - ``file`` is found. - :raise LookupError: If the ``disk_item`` is found but no ``ctrl_item`` + :raises LookupError: If the ``disk`` entry is found but no + corresponding ``file`` is found. + :raises LookupError: If the ``disk_item`` is found but no ``ctrl_item`` is found to be its parent. """ if file_id is None: @@ -2115,8 +2115,8 @@ def check_sanity_of_disk_device(self, disk, file_obj, this disk (optional) :param OVFItem ctrl_item: Controller device object which should link to the :attr:`disk_item` - :raise ValueMismatchError: if the given items are not linked properly. - :raise ValueUnsupportedError: if the :attr:`disk_item` has a + :raises ValueMismatchError: if the given items are not linked properly. + :raises ValueUnsupportedError: if the :attr:`disk_item` has a ``HostResource`` value in an unrecognized or invalid format. """ if disk_item is None: @@ -2213,7 +2213,7 @@ def remove_file(self, file_obj, disk=None, disk_drive=None): :param disk: Disk object referencing :attr:`file` :type disk: xml.etree.ElementTree.Element :param OVFItem disk_drive: Disk drive mapping :attr:`file` to a device - :raise ValueUnsupportedError: If the ``disk_drive`` is a device type + :raises ValueUnsupportedError: If the ``disk_drive`` is a device type other than 'cdrom' or 'harddisk' """ self.references.remove(file_obj) @@ -2303,7 +2303,7 @@ def add_controller_device(self, device_type, subtype, address, :return: New or updated controller device object :rtype: OVFItem - :raise ValueTooHighError: if no more controllers can be created + :raises ValueTooHighError: if no more controllers can be created """ if ctrl_item is None: logger.info("Controller not found, adding new Item") @@ -2340,9 +2340,9 @@ def _create_new_disk_device(self, disk_type, address, name, ctrl_item): :param str name: Device name string (optional) :param OVFItem ctrl_item: Controller object to serve as parent :return: (disk_item, disk_name) - :raise ValueTooHighError: if the requested address is out of range + :raises ValueTooHighError: if the requested address is out of range for the given controller, or if the controller is already full. - :raise ValueUnsupportedError: if ``name`` is not specified and + :raises ValueUnsupportedError: if ``name`` is not specified and ``disk_type`` is not 'harddisk' or 'cdrom'. """ ctrl_instance = ctrl_item.get_value(self.INSTANCE_ID) @@ -2436,7 +2436,7 @@ def untar(self, file_path): """Untar the OVF descriptor from an .ova to the working directory. :param str file_path: OVA file path - :raise VMInitError: if the given file does not represent a valid + :raises VMInitError: if the given file does not represent a valid OVA archive. :return: Path to extracted OVF descriptor """ @@ -2704,7 +2704,7 @@ def find_empty_drive(self, disk_type): :param str disk_type: Either 'cdrom' or 'harddisk' :return: :class:`OVFItem` representing this disk device, or None. - :raise ValueUnsupportedError: if ``disk_type`` is unrecognized. + :raises ValueUnsupportedError: if ``disk_type`` is unrecognized. """ if disk_type == 'cdrom': # Find a drive that has no HostResource property @@ -2737,7 +2737,7 @@ def find_device_location(self, device): :param OVFItem device: Hardware device object. :returns: ``(type, address)``, such as ``("ide", "1:0")``. - :raise LookupError: if the controller is not found. + :raises LookupError: if the controller is not found. """ controller = self.find_parent_from_item(device) if controller is None: diff --git a/COT/platforms.py b/COT/platforms.py index 4dd8749..8e54c75 100644 --- a/COT/platforms.py +++ b/COT/platforms.py @@ -143,8 +143,8 @@ def validate_cpu_count(cls, cpus): """Throw an error if the number of CPUs is not a supported value. :param int cpus: Number of CPUs - :raise ValueTooLowError: if ``cpus`` is less than :const:`CPU_MIN` - :raise ValueTooHighError: if ``cpus`` is more than :const:`CPU_MAX` + :raises ValueTooLowError: if ``cpus`` is less than :const:`CPU_MIN` + :raises ValueTooHighError: if ``cpus`` is more than :const:`CPU_MAX` """ validate_int(cpus, cls.CPU_MIN, cls.CPU_MAX, "CPUs") @@ -153,9 +153,9 @@ def validate_memory_amount(cls, mebibytes): """Throw an error if the amount of RAM is not supported. :param int mebibytes: RAM, in MiB. - :raise ValueTooLowError: if :attr:`mebibytes` is less than + :raises ValueTooLowError: if :attr:`mebibytes` is less than :const:`RAM_MIN` - :raise ValueTooHighError: if :attr:`mebibytes` is more than + :raises ValueTooHighError: if :attr:`mebibytes` is more than :const:`RAM_MAX` """ if cls.RAM_MIN is not None and mebibytes < cls.RAM_MIN: @@ -178,8 +178,8 @@ def validate_nic_count(cls, count): """Throw an error if the number of NICs is not supported. :param int count: Number of NICs. - :raise ValueTooLowError: if ``count`` is less than :const:`NIC_MIN` - :raise ValueTooHighError: if ``count`` is more than :const:`NIC_MAX` + :raises ValueTooLowError: if ``count`` is less than :const:`NIC_MIN` + :raises ValueTooHighError: if ``count`` is more than :const:`NIC_MAX` """ validate_int(count, cls.NIC_MIN, cls.NIC_MAX, "NIC count") @@ -192,7 +192,7 @@ def validate_nic_type(cls, type_string): - :data:`COT.data_validation.NIC_TYPES` :param str type_string: See :data:`COT.data_validation.NIC_TYPES` - :raise ValueUnsupportedError: if ``type_string`` is not in + :raises ValueUnsupportedError: if ``type_string`` is not in :const:`SUPPORTED_NIC_TYPES` """ if type_string not in cls.SUPPORTED_NIC_TYPES: @@ -204,7 +204,7 @@ def validate_nic_types(cls, type_list): """Throw an error if any NIC type string in the list is unsupported. :param list type_list: See :data:`COT.data_validation.NIC_TYPES` - :raise ValueUnsupportedError: if any value in ``type_list`` is not in + :raises ValueUnsupportedError: if any value in ``type_list`` is not in :const:`SUPPORTED_NIC_TYPES` """ for type_string in type_list: @@ -215,8 +215,8 @@ def validate_serial_count(cls, count): """Throw an error if the number of serial ports is not supported. :param int count: Number of serial ports. - :raise ValueTooLowError: if ``count`` is less than :const:`SER_MIN` - :raise ValueTooHighError: if ``count`` is more than :const:`SER_MAX` + :raises ValueTooLowError: if ``count`` is less than :const:`SER_MIN` + :raises ValueTooHighError: if ``count`` is more than :const:`SER_MAX` """ validate_int(count, cls.SER_MIN, cls.SER_MAX, "serial port count") @@ -404,9 +404,9 @@ def validate_cpu_count(cls, cpus): """CSR1000V supports 1, 2, 4, or 8 CPUs. :param int cpus: Number of CPUs. - :raise ValueTooLowError: if ``cpus`` is less than :const:`CPU_MIN` - :raise ValueTooHighError: if ``cpus`` is more than :const:`CPU_MAX` - :raise ValueUnsupportedError: if ``cpus`` is an unsupported value + :raises ValueTooLowError: if ``cpus`` is less than :const:`CPU_MIN` + :raises ValueTooHighError: if ``cpus`` is more than :const:`CPU_MAX` + :raises ValueUnsupportedError: if ``cpus`` is an unsupported value between :const:`CPU_MIN` and :const:`CPU_MAX` """ validate_int(cpus, 1, 8, "CPUs") @@ -450,9 +450,9 @@ def validate_memory_amount(cls, mebibytes): """IOSv has minimum 192 MiB (with minimal feature set), max 3 GiB. :param int mebibytes: RAM amount, in MiB. - :raise ValueTooLowError: if :attr:`mebibytes` is less than + :raises ValueTooLowError: if :attr:`mebibytes` is less than :const:`RAM_MIN` - :raise ValueTooHighError: if :attr:`mebibytes` is more than + :raises ValueTooHighError: if :attr:`mebibytes` is more than :const:`RAM_MAX` """ if mebibytes < 192: diff --git a/COT/submodule.py b/COT/submodule.py index 3b05c45..d30fb69 100644 --- a/COT/submodule.py +++ b/COT/submodule.py @@ -68,7 +68,7 @@ def ready_to_run(self): # pylint: disable=no-self-use def run(self): """Do the actual work of this submodule. - :raise InvalidInputError: if :meth:`ready_to_run` reports ``False`` + :raises InvalidInputError: if :meth:`ready_to_run` reports ``False`` """ (ready, reason) = self.ready_to_run() if not ready: @@ -121,7 +121,7 @@ def package(self): Calls :meth:`COT.vm_factory.VMFactory.create` to instantiate :attr:`self.vm` from the provided file. - :raise InvalidInputError: if the file does not exist. + :raises InvalidInputError: if the file does not exist. """ return self._package @@ -177,7 +177,7 @@ def package(self): Calls :meth:`COT.vm_factory.VMFactory.create` to instantiate :attr:`self.vm` from the provided file. - :raise InvalidInputError: if the file does not exist. + :raises InvalidInputError: if the file does not exist. """ return self._package @@ -227,7 +227,7 @@ def run(self): If :attr:`output` was not previously set, automatically sets it to the value of :attr:`PACKAGE`. - :raise InvalidInputError: if :meth:`ready_to_run` reports ``False`` + :raises InvalidInputError: if :meth:`ready_to_run` reports ``False`` """ super(COTSubmodule, self).run() diff --git a/COT/ui_shared.py b/COT/ui_shared.py index 4191159..8a43cc4 100644 --- a/COT/ui_shared.py +++ b/COT/ui_shared.py @@ -70,7 +70,7 @@ def fill_examples(self, example_list): """Pretty-print a set of usage examples. :param list example_list: List of (example, description) tuples. - :raise NotImplementedError: Must be implemented by a subclass. + :raises NotImplementedError: Must be implemented by a subclass. """ raise NotImplementedError("No implementation for fill_examples()") @@ -169,6 +169,6 @@ def get_password(self, username, host): :param str username: Username the password is associated with :param str host: Host the password is associated with - :raise NotImplementedError: Must be implemented by a subclass. + :raises NotImplementedError: Must be implemented by a subclass. """ raise NotImplementedError("No implementation of get_password()") diff --git a/COT/vm_description.py b/COT/vm_description.py index 75d5384..e9e2299 100644 --- a/COT/vm_description.py +++ b/COT/vm_description.py @@ -77,7 +77,8 @@ def detect_type_from_name(cls, filename): :param str filename: File name or path :return: A string representing a recognized and supported type of file - :raise ValueUnsupportedError: if we don't know how to handle this file. + :raises ValueUnsupportedError: if COT can't recognize the file type or + doesn't know how to handle this file type. """ raise ValueUnsupportedError("filename", filename, ("none implemented")) @@ -357,7 +358,7 @@ def check_sanity_of_disk_device(self, disk, file_obj, this disk (optional) :param object ctrl_item: Controller device object which should link to the :attr:`disk_item` - :raise ValueMismatchError: if the given items are not linked properly. + :raises ValueMismatchError: if the given items are not linked properly. """ raise NotImplementedError( "check_sanity_of_disk_device not implemented") diff --git a/COT/vm_factory.py b/COT/vm_factory.py index ecdf74e..21bd6a2 100644 --- a/COT/vm_factory.py +++ b/COT/vm_factory.py @@ -32,8 +32,8 @@ class VMFactory(object): def create(cls, input_file, output_file): """Create an appropriate VMDescription subclass instance from a file. - :raise VMInitError: if no appropriate class is identified - :raise VMInitError: if the selected subclass raises a + :raises VMInitError: if no appropriate class is identified + :raises VMInitError: if the selected subclass raises a ValueUnsupportedError while loading the file. :param str input_file: File to read VM description from :param str output_file: File to write to when finished (optional) diff --git a/COT/xml_file.py b/COT/xml_file.py index 1be6221..b8c8597 100644 --- a/COT/xml_file.py +++ b/COT/xml_file.py @@ -79,9 +79,9 @@ def __init__(self, xml_file): :param str xml_file: File path to read. - :raise xml.etree.ElementTree.ParseError: if parsing fails under Python + :raises xml.etree.ElementTree.ParseError: if parsing fails under Python 2.7 or later - :raise xml.parsers.expat.ExpatError: if parsing fails under Python 2.6 + :raises xml.parsers.expat.ExpatError: if parsing fails under Python 2.6 """ # Parse the XML into memory self.tree = ET.parse(xml_file) From a42f5346e91a4585ceb3c2347e8d273935f037a8 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 25 Jul 2016 16:51:43 -0400 Subject: [PATCH 09/59] Cleanup --- COT/helpers/tests/test_fatdisk.py | 3 --- COT/helpers/tests/test_helper.py | 20 +++++--------------- COT/helpers/tests/test_mkisofs.py | 2 -- COT/helpers/tests/test_vmdktool.py | 2 -- tox.ini | 2 +- 5 files changed, 6 insertions(+), 23 deletions(-) diff --git a/COT/helpers/tests/test_fatdisk.py b/COT/helpers/tests/test_fatdisk.py index 360f3cb..8609044 100644 --- a/COT/helpers/tests/test_fatdisk.py +++ b/COT/helpers/tests/test_fatdisk.py @@ -72,7 +72,6 @@ def test_install_helper_apt_get(self, mock_copy, mock_find_executable, *_): - # pylint: disable=missing-param-doc,missing-type-doc """Test installation via 'apt-get'.""" self.enable_apt_install() mock_find_executable.side_effect = [ @@ -151,7 +150,6 @@ def test_install_helper_yum(self, mock_copy, mock_find_executable, *_): - # pylint: disable=missing-param-doc,missing-type-doc """Test installation via 'yum'.""" self.enable_yum_install() mock_find_executable.side_effect = [ @@ -186,7 +184,6 @@ def test_install_helper_linux_need_make_no_package_manager(self, *_): @staticmethod def _find_make_only(name): - # pylint: disable=missing-param-doc,missing-type-doc """Stub for distutils.spawn.find_executable - only finds 'make'.""" logger.info("stub_find_executable(%s)", name) if name == 'make': diff --git a/COT/helpers/tests/test_helper.py b/COT/helpers/tests/test_helper.py index 8768081..e117117 100644 --- a/COT/helpers/tests/test_helper.py +++ b/COT/helpers/tests/test_helper.py @@ -32,7 +32,7 @@ logger = logging.getLogger(__name__) -# pylint: disable=missing-type-doc,missing-param-doc +# pylint: disable=missing-type-doc,missing-param-doc,protected-access class HelperUT(COT_UT): @@ -65,13 +65,11 @@ def assertSubprocessCalls(self, mock_function, args_list): # noqa: N802 [a[0][0] for a in mock_function.call_args_list]) def set_helper_version(self, ver): - # pylint: disable=missing-param-doc,missing-type-doc """Override the version number of the helper class.""" - self.helper._version = ver # pylint: disable=protected-access + self.helper._version = ver @staticmethod def select_package_manager(name): - # pylint: disable=missing-param-doc,missing-type-doc """Select the specified installer program for Helper to use.""" for pm in Helper.PACKAGE_MANAGERS: Helper.PACKAGE_MANAGERS[pm] = (pm == name) @@ -79,7 +77,7 @@ def select_package_manager(name): def enable_apt_install(self): """Set flags and values to force an apt-get update and apt install.""" self.select_package_manager('apt-get') - Helper._apt_updated = False # pylint: disable=protected-access + Helper._apt_updated = False os.environ['PREFIX'] = '/usr/local' if 'DESTDIR' in os.environ: del os.environ['DESTDIR'] @@ -93,7 +91,6 @@ def enable_yum_install(self): def assertAptUpdated(self): # noqa: N802 """Assert that the hidden _apt_updated flag is set.""" - # pylint: disable=protected-access self.assertTrue(Helper._apt_updated) @mock.patch('distutils.spawn.find_executable', return_value=None) @@ -139,7 +136,6 @@ def port_install_test(self, portname, *_): :param str portname: MacPorts package name to test. """ - # pylint: disable=protected-access self.select_package_manager('port') Helper._port_updated = False # Python 2.6 doesn't let us use multiple contexts in one 'with' @@ -180,7 +176,7 @@ def setUp(self): # subclass needs to set self.helper super(HelperUT, self).setUp() if self.helper: - self.helper._path = None # pylint: disable=protected-access + self.helper._path = None # save some environment properties for sanity self._port = Helper.PACKAGE_MANAGERS['port'] self._apt_get = Helper.PACKAGE_MANAGERS['apt-get'] @@ -214,7 +210,6 @@ def setUp(self): def test_check_call_helpernotfounderror(self): """HelperNotFoundError if executable doesn't exist.""" - # pylint: disable=protected-access self.assertRaises(HelperNotFoundError, Helper._check_call, ["not_a_command"]) self.assertRaises(HelperNotFoundError, @@ -223,7 +218,6 @@ def test_check_call_helpernotfounderror(self): def test_check_call_helpererror(self): """HelperError if executable fails and require_success is set.""" - # pylint: disable=protected-access with self.assertRaises(HelperError) as cm: Helper._check_call(["false"]) self.assertEqual(cm.exception.errno, 1) @@ -240,7 +234,6 @@ def raise_oserror(args, **_): return mock_check_call.side_effect = raise_oserror - # pylint: disable=protected-access # Without retry_on_sudo, we reraise the permissions error with self.assertRaises(OSError) as cm: @@ -282,7 +275,6 @@ def raise_subprocess_error(args, **_): def test_check_output_helpernotfounderror(self): """HelperNotFoundError if executable doesn't exist.""" - # pylint: disable=protected-access self.assertRaises(HelperNotFoundError, Helper._check_output, ["not_a_command"]) self.assertRaises(HelperNotFoundError, @@ -291,13 +283,11 @@ def test_check_output_helpernotfounderror(self): def test_check_output_oserror(self): """OSError if requested command isn't an executable.""" - # pylint: disable=protected-access self.assertRaises(OSError, Helper._check_output, self.input_ovf) def test_check_output_helpererror(self): """HelperError if executable fails and require_success is set.""" - # pylint: disable=protected-access with self.assertRaises(HelperError) as cm: Helper._check_output(["false"]) @@ -312,7 +302,7 @@ def test_helper_not_found(self, *_): def test_install_helper_already_present(self): """Make sure a warning is logged when attempting to re-install.""" - self.helper._path = True # pylint: disable=protected-access + self.helper._path = True self.helper.install_helper() self.assertLogged(**self.ALREADY_INSTALLED) diff --git a/COT/helpers/tests/test_mkisofs.py b/COT/helpers/tests/test_mkisofs.py index f30b7e6..bc3e83d 100644 --- a/COT/helpers/tests/test_mkisofs.py +++ b/COT/helpers/tests/test_mkisofs.py @@ -75,7 +75,6 @@ def test_get_version_xorriso(self, _): def test_find_mkisofs(self, mock_call_helper, mock_find_executable): """If mkisofs is found, use it.""" def find_one(name): - # pylint: disable=missing-param-doc,missing-type-doc """Find mkisofs but no other.""" if name == "mkisofs": return "/mkisofs" @@ -94,7 +93,6 @@ def find_one(name): def test_find_genisoimage(self, mock_call_helper, mock_find_executable): """If mkisofs is not found, but genisoimage is, use that.""" def find_one(name): - # pylint: disable=missing-param-doc,missing-type-doc """Find genisoimage but no other.""" if name == "genisoimage": return "/genisoimage" diff --git a/COT/helpers/tests/test_vmdktool.py b/COT/helpers/tests/test_vmdktool.py index c8770c5..d8ce6af 100644 --- a/COT/helpers/tests/test_vmdktool.py +++ b/COT/helpers/tests/test_vmdktool.py @@ -65,7 +65,6 @@ def test_install_helper_apt_get(self, mock_check_output, mock_find_executable, *_): - # pylint: disable=missing-param-doc,missing-type-doc """Test installation via 'apt-get'.""" self.enable_apt_install() mock_find_executable.side_effect = [ @@ -135,7 +134,6 @@ def test_install_helper_yum(self, mock_check_call, mock_find_executable, *_): - # pylint: disable=missing-param-doc,missing-type-doc """Test installation via 'yum'.""" self.enable_yum_install() mock_find_executable.side_effect = [ diff --git a/tox.ini b/tox.ini index 16d6a75..ecf043b 100644 --- a/tox.ini +++ b/tox.ini @@ -62,7 +62,7 @@ commands = flake8 --verbose [testenv:pylint] deps = {[testenv]deps} - pylint==1.6.1 + pylint==1.6.4 commands = pylint COT [testenv:docs] From ac61bf68a8213e1021e3e82d3b49d4019b7c8abe Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 22 Sep 2016 11:02:19 -0400 Subject: [PATCH 10/59] Move platforms to subpackage --- COT/{platforms.py => platforms/__init__.py} | 69 ++++++++++----------- 1 file changed, 32 insertions(+), 37 deletions(-) rename COT/{platforms.py => platforms/__init__.py} (88%) diff --git a/COT/platforms.py b/COT/platforms/__init__.py similarity index 88% rename from COT/platforms.py rename to COT/platforms/__init__.py index b135f00..ad2e410 100644 --- a/COT/platforms.py +++ b/COT/platforms/__init__.py @@ -1,8 +1,3 @@ -#!/usr/bin/env python -# -# platforms.py - Module for methods related to variations between -# guest platforms (Cisco CSR1000V, Cisco IOS XRv, etc.) -# # October 2013, Glenn F. Matthews # Copyright (c) 2013-2016 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution @@ -15,13 +10,21 @@ # of COT, including this file, may be copied, modified, propagated, or # distributed except according to the terms contained in the LICENSE.txt file. -"""Handles behavior that varies between guest platforms. +"""Package for identifying guest platforms and handling platform differences. + +The :class:`COT.platforms.GenericPlatform` class describes the API and provides +a generic API 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:`platform_from_product_class` API. **Functions** .. autosummary:: :nosignatures: + is_known_product_class platform_from_product_class **Classes** @@ -46,9 +49,10 @@ import logging -from .data_validation import ValueUnsupportedError -from .data_validation import ValueTooLowError, ValueTooHighError -from .data_validation import NIC_TYPES +from COT.data_validation import ( + ValueUnsupportedError, ValueTooLowError, ValueTooHighError, + validate_int, NIC_TYPES, +) logger = logging.getLogger(__name__) @@ -70,15 +74,6 @@ def platform_from_product_class(product_class): return GenericPlatform -def valid_range(label, value, min_val, max_val): - """Raise an exception if the value is not in the valid range.""" - if min_val is not None and value < min_val: - raise ValueTooLowError(label, value, min_val) - elif max_val is not None and value > max_val: - raise ValueTooHighError(label, value, max_val) - return True - - class GenericPlatform(object): """Generic class for operations that depend on guest platform. @@ -117,17 +112,17 @@ def guess_nic_name(cls, nic_number): @classmethod def validate_cpu_count(cls, cpus): """Throw an error if the number of CPUs is not a supported value.""" - valid_range("CPUs", cpus, 1, None) + validate_int(cpus, 1, None, "CPUs") @classmethod def validate_memory_amount(cls, mebibytes): """Throw an error if the amount of RAM is not supported.""" - valid_range("RAM", mebibytes, 1, None) + validate_int(mebibytes, 1, None, "RAM") @classmethod def validate_nic_count(cls, count): """Throw an error if the number of NICs is not supported.""" - valid_range("NIC count", count, 0, None) + validate_int(count, 0, None, "NIC count") @classmethod def validate_nic_type(cls, type_string): @@ -150,7 +145,7 @@ def validate_nic_types(cls, type_list): @classmethod def validate_serial_count(cls, count): """Throw an error if the number of serial ports is not supported.""" - valid_range("serial port count", count, 0, None) + validate_int(count, 0, None, "serial port count") class IOSXRv(GenericPlatform): @@ -174,7 +169,7 @@ def guess_nic_name(cls, nic_number): @classmethod def validate_cpu_count(cls, cpus): """IOS XRv supports 1-8 CPUs.""" - valid_range("CPUs", cpus, 1, 8) + validate_int(cpus, 1, 8, "CPUs") @classmethod def validate_memory_amount(cls, mebibytes): @@ -187,12 +182,12 @@ def validate_memory_amount(cls, mebibytes): @classmethod def validate_nic_count(cls, count): """IOS XRv requires at least one NIC.""" - valid_range("NIC count", count, 1, None) + validate_int(count, 1, None, "NIC count") @classmethod def validate_serial_count(cls, count): """IOS XRv supports 1-4 serial ports.""" - valid_range("serial ports", count, 1, 4) + validate_int(count, 1, 4, "serial ports") class IOSXRvRP(IOSXRv): @@ -215,7 +210,7 @@ def guess_nic_name(cls, nic_number): @classmethod def validate_nic_count(cls, count): """Fabric plus an optional management NIC.""" - valid_range("NIC count", count, 1, 2) + validate_int(count, 1, 2, "NIC count") class IOSXRvLC(IOSXRv): @@ -244,7 +239,7 @@ def guess_nic_name(cls, nic_number): @classmethod def validate_serial_count(cls, count): """No serial ports are needed but up to 4 can be used for debugging.""" - valid_range("serial ports", count, 0, 4) + validate_int(count, 0, 4, "serial ports") class IOSXRv9000(IOSXRv): @@ -268,7 +263,7 @@ def guess_nic_name(cls, nic_number): @classmethod def validate_cpu_count(cls, cpus): """Minimum 1, maximum 32 CPUs.""" - valid_range("CPUs", cpus, 1, 32) + validate_int(cpus, 1, 32, "CPUs") @classmethod def validate_memory_amount(cls, mebibytes): @@ -281,7 +276,7 @@ def validate_memory_amount(cls, mebibytes): @classmethod def validate_nic_count(cls, count): """IOS XRv 9000 requires at least 4 NICs.""" - valid_range("NIC count", count, 4, None) + validate_int(count, 4, None, "NIC count") class CSR1000V(GenericPlatform): @@ -318,7 +313,7 @@ def guess_nic_name(cls, nic_number): @classmethod def validate_cpu_count(cls, cpus): """CSR1000V supports 1, 2, or 4 CPUs.""" - valid_range("CPUs", cpus, 1, 4) + validate_int(cpus, 1, 4, "CPUs") if cpus != 1 and cpus != 2 and cpus != 4: raise ValueUnsupportedError("CPUs", cpus, [1, 2, 4]) @@ -333,12 +328,12 @@ def validate_memory_amount(cls, mebibytes): @classmethod def validate_nic_count(cls, count): """CSR1000V requires 3 NICs and supports up to 26.""" - valid_range("NICs", count, 3, 26) + validate_int(count, 3, 26, "NICs") @classmethod def validate_serial_count(cls, count): """CSR1000V supports 0-2 serial ports.""" - valid_range("serial ports", count, 0, 2) + validate_int(count, 0, 2, "serial ports") class IOSv(GenericPlatform): @@ -360,7 +355,7 @@ def guess_nic_name(cls, nic_number): @classmethod def validate_cpu_count(cls, cpus): """IOSv only supports a single CPU.""" - valid_range("CPUs", cpus, 1, 1) + validate_int(cpus, 1, 1, "CPUs") @classmethod def validate_memory_amount(cls, mebibytes): @@ -377,12 +372,12 @@ def validate_memory_amount(cls, mebibytes): @classmethod def validate_nic_count(cls, count): """IOSv supports up to 16 NICs.""" - valid_range("NICs", count, 0, 16) + validate_int(count, 0, 16, "NICs") @classmethod def validate_serial_count(cls, count): """IOSv requires 1-2 serial ports.""" - valid_range("serial ports", count, 1, 2) + validate_int(count, 1, 2, "serial ports") class NXOSv(GenericPlatform): @@ -416,7 +411,7 @@ def guess_nic_name(cls, nic_number): @classmethod def validate_cpu_count(cls, cpus): """NX-OSv requires 1-8 CPUs.""" - valid_range("CPUs", cpus, 1, 8) + validate_int(cpus, 1, 8, "CPUs") @classmethod def validate_memory_amount(cls, mebibytes): @@ -429,7 +424,7 @@ def validate_memory_amount(cls, mebibytes): @classmethod def validate_serial_count(cls, count): """NX-OSv requires 1-2 serial ports.""" - valid_range("serial ports", count, 1, 2) + validate_int(count, 1, 2, "serial ports") PRODUCT_PLATFORM_MAP = { 'com.cisco.csr1000v': CSR1000V, From 42b33d449d2bf66b36dc30c8d3c5e1f58fc33cea Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 22 Sep 2016 11:04:56 -0400 Subject: [PATCH 11/59] Move test script to subdirectory too --- COT/platforms/tests/__init__.py | 13 +++++++++++++ COT/{ => platforms}/tests/test_platforms.py | 0 2 files changed, 13 insertions(+) create mode 100644 COT/platforms/tests/__init__.py rename COT/{ => platforms}/tests/test_platforms.py (100%) diff --git a/COT/platforms/tests/__init__.py b/COT/platforms/tests/__init__.py new file mode 100644 index 0000000..409ece1 --- /dev/null +++ b/COT/platforms/tests/__init__.py @@ -0,0 +1,13 @@ +# September 2016, Glenn F. Matthews +# Copyright (c) 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 COT.platforms package and its submodules.""" diff --git a/COT/tests/test_platforms.py b/COT/platforms/tests/test_platforms.py similarity index 100% rename from COT/tests/test_platforms.py rename to COT/platforms/tests/test_platforms.py From 7ce22180edffb458a933cb548ec47bbe402659cc Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 22 Sep 2016 15:23:21 -0400 Subject: [PATCH 12/59] Split platforms into individual files --- COT/platforms/__init__.py | 397 ++--------------------------- COT/platforms/cisco_csr1000v.py | 80 ++++++ COT/platforms/cisco_iosv.py | 66 +++++ COT/platforms/cisco_iosxrv.py | 116 +++++++++ COT/platforms/cisco_iosxrv_9000.py | 59 +++++ COT/platforms/cisco_nxosv.py | 69 +++++ COT/platforms/generic.py | 89 +++++++ setup.py | 2 +- 8 files changed, 507 insertions(+), 371 deletions(-) create mode 100644 COT/platforms/cisco_csr1000v.py create mode 100644 COT/platforms/cisco_iosv.py create mode 100644 COT/platforms/cisco_iosxrv.py create mode 100644 COT/platforms/cisco_iosxrv_9000.py create mode 100644 COT/platforms/cisco_nxosv.py create mode 100644 COT/platforms/generic.py diff --git a/COT/platforms/__init__.py b/COT/platforms/__init__.py index ad2e410..032cc11 100644 --- a/COT/platforms/__init__.py +++ b/COT/platforms/__init__.py @@ -17,7 +17,8 @@ platform-specific logic. In general, other modules should not instantiate subclasses directly but should -instead use the :func:`platform_from_product_class` API. +instead use the :func:`platform_from_product_class` API to derive the +appropriate subclass instance. **Functions** @@ -49,14 +50,31 @@ import logging -from COT.data_validation import ( - ValueUnsupportedError, ValueTooLowError, ValueTooHighError, - validate_int, NIC_TYPES, -) +from .generic import GenericPlatform +from .cisco_csr1000v import CSR1000V +from .cisco_iosv import IOSv +from .cisco_iosxrv import IOSXRv, IOSXRvRP, IOSXRvLC +from .cisco_iosxrv_9000 import IOSXRv9000 +from .cisco_nxosv import NXOSv logger = logging.getLogger(__name__) +PRODUCT_PLATFORM_MAP = { + 'com.cisco.csr1000v': CSR1000V, + 'com.cisco.iosv': IOSv, + '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.""" return product_class in PRODUCT_PLATFORM_MAP @@ -74,368 +92,7 @@ def platform_from_product_class(product_class): return GenericPlatform -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 - - @classmethod - def controller_type_for_device(cls, _device_type): - """Get the default controller type for the given device type.""" - # 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! - """ - 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.""" - validate_int(cpus, 1, None, "CPUs") - - @classmethod - def validate_memory_amount(cls, mebibytes): - """Throw an error if the amount of RAM is not supported.""" - validate_int(mebibytes, 1, None, "RAM") - - @classmethod - def validate_nic_count(cls, count): - """Throw an error if the number of NICs is not supported.""" - 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` - """ - 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.""" - 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.""" - validate_int(count, 0, None, "serial port count") - - -class IOSXRv(GenericPlatform): - """Platform-specific logic for Cisco IOS XRv platform.""" - - PLATFORM_NAME = "Cisco IOS XRv" - - CONFIG_TEXT_FILE = 'iosxr_config.txt' - SECONDARY_CONFIG_TEXT_FILE = 'iosxr_config_admin.txt' - LITERAL_CLI_STRING = None - SUPPORTED_NIC_TYPES = ["E1000", "virtio"] - - @classmethod - def guess_nic_name(cls, nic_number): - """MgmtEth0/0/CPU0/0, GigabitEthernet0/0/0/0, Gig0/0/0/1, etc.""" - if nic_number == 1: - return "MgmtEth0/0/CPU0/0" - else: - return "GigabitEthernet0/0/0/" + str(nic_number - 2) - - @classmethod - def validate_cpu_count(cls, cpus): - """IOS XRv supports 1-8 CPUs.""" - validate_int(cpus, 1, 8, "CPUs") - - @classmethod - def validate_memory_amount(cls, mebibytes): - """Minimum 3 GiB, max 8 GiB of RAM.""" - 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.""" - validate_int(count, 1, None, "NIC count") - - @classmethod - def validate_serial_count(cls, count): - """IOS XRv supports 1-4 serial ports.""" - 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): - """Fabric and management only. - - * fabric - * MgmtEth0/{SLOT}/CPU0/0 - """ - if nic_number == 1: - return "fabric" - else: - return "MgmtEth0/{SLOT}/CPU0/" + str(nic_number - 2) - - @classmethod - def validate_nic_count(cls, count): - """Fabric plus an optional management NIC.""" - validate_int(count, 1, 2, "NIC count") - - -class IOSXRvLC(IOSXRv): - """Platform-specific logic for Cisco IOS XRv line card.""" - - PLATFORM_NAME = "Cisco IOS XRv line card" - - # No bootstrap config for LCs - they inherit from the RP - CONFIG_TEXT_FILE = None - SECONDARY_CONFIG_TEXT_FILE = None - - @classmethod - def guess_nic_name(cls, nic_number): - """Fabric interface plus slot-appropriate GigabitEthernet interfaces. - - * fabric - * GigabitEthernet0/{SLOT}/0/0 - * GigabitEthernet0/{SLOT}/0/1 - * etc. - """ - if nic_number == 1: - return "fabric" - 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.""" - validate_int(count, 0, 4, "serial ports") - - -class IOSXRv9000(IOSXRv): - """Platform-specific logic for Cisco IOS XRv 9000 platform.""" - - PLATFORM_NAME = "Cisco IOS XRv 9000" - SUPPORTED_NIC_TYPES = ["E1000", "virtio", "VMXNET3"] - - @classmethod - def guess_nic_name(cls, nic_number): - """MgmtEth0/0/CPU0/0, CtrlEth, DevEth, GigabitEthernet0/0/0/0, etc.""" - if nic_number == 1: - return "MgmtEth0/0/CPU0/0" - elif nic_number == 2: - return "CtrlEth" - elif nic_number == 3: - return "DevEth" - else: - return "GigabitEthernet0/0/0/" + str(nic_number - 4) - - @classmethod - def validate_cpu_count(cls, cpus): - """Minimum 1, maximum 32 CPUs.""" - validate_int(cpus, 1, 32, "CPUs") - - @classmethod - def validate_memory_amount(cls, mebibytes): - """Minimum 8 GiB, maximum 32 GiB.""" - if mebibytes < 8192: - raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "8 GiB") - elif mebibytes > 32768: - raise ValueTooHighError("RAM", str(mebibytes) + " MiB", "32 GiB") - - @classmethod - def validate_nic_count(cls, count): - """IOS XRv 9000 requires at least 4 NICs.""" - validate_int(count, 4, None, "NIC count") - - -class CSR1000V(GenericPlatform): - """Platform-specific logic for Cisco CSR1000V platform.""" - - PLATFORM_NAME = "Cisco CSR1000V" - - CONFIG_TEXT_FILE = 'iosxe_config.txt' - LITERAL_CLI_STRING = 'ios-config' - # 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): - """CSR1000V uses SCSI for hard disks and IDE for CD-ROMs.""" - if device_type == 'harddisk': - return 'scsi' - elif device_type == 'cdrom': - return 'ide' - else: - return super(CSR1000V, cls).controller_type_for_device(device_type) - - @classmethod - def guess_nic_name(cls, nic_number): - """GigabitEthernet1, GigabitEthernet2, etc. - - .. warning:: - In all current CSR releases, NIC names start at "GigabitEthernet1". - Some early versions started at "GigabitEthernet0" but we don't - support that. - """ - return "GigabitEthernet" + str(nic_number) - - @classmethod - def validate_cpu_count(cls, cpus): - """CSR1000V supports 1, 2, or 4 CPUs.""" - validate_int(cpus, 1, 4, "CPUs") - if cpus != 1 and cpus != 2 and cpus != 4: - raise ValueUnsupportedError("CPUs", cpus, [1, 2, 4]) - - @classmethod - def validate_memory_amount(cls, mebibytes): - """Minimum 2.5 GiB, max 8 GiB.""" - 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.""" - validate_int(count, 3, 26, "NICs") - - @classmethod - def validate_serial_count(cls, count): - """CSR1000V supports 0-2 serial ports.""" - validate_int(count, 0, 2, "serial ports") - - -class IOSv(GenericPlatform): - """Platform-specific logic for Cisco IOSv.""" - - PLATFORM_NAME = "Cisco IOSv" - - CONFIG_TEXT_FILE = 'ios_config.txt' - LITERAL_CLI_STRING = None - # IOSv has no CD-ROM driver so bootstrap configs must be provided on disk. - BOOTSTRAP_DISK_TYPE = 'harddisk' - SUPPORTED_NIC_TYPES = ["E1000"] - - @classmethod - def guess_nic_name(cls, nic_number): - """GigabitEthernet0/0, GigabitEthernet0/1, etc.""" - return "GigabitEthernet0/" + str(nic_number - 1) - - @classmethod - def validate_cpu_count(cls, cpus): - """IOSv only supports a single CPU.""" - validate_int(cpus, 1, 1, "CPUs") - - @classmethod - def validate_memory_amount(cls, mebibytes): - """IOSv has minimum 192 MiB (with minimal feature set), max 3 GiB.""" - if mebibytes < 192: - raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "192 MiB") - elif mebibytes < 384: - # 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.""" - validate_int(count, 0, 16, "NICs") - - @classmethod - def validate_serial_count(cls, count): - """IOSv requires 1-2 serial ports.""" - validate_int(count, 1, 2, "serial ports") - - -class NXOSv(GenericPlatform): - """Platform-specific logic for Cisco NX-OSv (Titanium).""" - - PLATFORM_NAME = "Cisco NX-OSv" - - CONFIG_TEXT_FILE = 'nxos_config.txt' - LITERAL_CLI_STRING = None - SUPPORTED_NIC_TYPES = ["E1000", "virtio"] - - @classmethod - def guess_nic_name(cls, nic_number): - """NX-OSv names its NICs a bit interestingly... - - * mgmt0 - * Ethernet2/1 - * Ethernet2/2 - * ... - * Ethernet2/48 - * Ethernet3/1 - * Ethernet3/2 - * ... - """ - if nic_number == 1: - return "mgmt0" - else: - 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.""" - validate_int(cpus, 1, 8, "CPUs") - - @classmethod - def validate_memory_amount(cls, mebibytes): - """NX-OSv requires 2-8 GiB of RAM.""" - 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.""" - validate_int(count, 1, 2, "serial ports") - -PRODUCT_PLATFORM_MAP = { - 'com.cisco.csr1000v': CSR1000V, - 'com.cisco.iosv': IOSv, - '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.""" +__all__ = ( + 'is_known_product_class', + 'platform_from_product_class', +) diff --git a/COT/platforms/cisco_csr1000v.py b/COT/platforms/cisco_csr1000v.py new file mode 100644 index 0000000..76a21b2 --- /dev/null +++ b/COT/platforms/cisco_csr1000v.py @@ -0,0 +1,80 @@ +# 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. + +"""Package describing the Cisco CSR1000V virtual router platform.""" + +import logging + +from COT.platforms.generic import GenericPlatform +from COT.data_validation import ( + ValueUnsupportedError, ValueTooLowError, ValueTooHighError, + validate_int, +) + +logger = logging.getLogger(__name__) + + +class CSR1000V(GenericPlatform): + """Platform-specific logic for Cisco CSR1000V platform.""" + + PLATFORM_NAME = "Cisco CSR1000V" + + CONFIG_TEXT_FILE = 'iosxe_config.txt' + LITERAL_CLI_STRING = 'ios-config' + # 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): + """CSR1000V uses SCSI for hard disks and IDE for CD-ROMs.""" + if device_type == 'harddisk': + return 'scsi' + elif device_type == 'cdrom': + return 'ide' + else: + return super(CSR1000V, cls).controller_type_for_device(device_type) + + @classmethod + def guess_nic_name(cls, nic_number): + """GigabitEthernet1, GigabitEthernet2, etc. + + .. warning:: + In all current CSR releases, NIC names start at "GigabitEthernet1". + Some early versions started at "GigabitEthernet0" but we don't + support that. + """ + return "GigabitEthernet" + str(nic_number) + + @classmethod + def validate_cpu_count(cls, cpus): + """CSR1000V supports 1, 2, or 4 CPUs.""" + validate_int(cpus, 1, 4, "CPUs") + if cpus != 1 and cpus != 2 and cpus != 4: + raise ValueUnsupportedError("CPUs", cpus, [1, 2, 4]) + + @classmethod + def validate_memory_amount(cls, mebibytes): + """Minimum 2.5 GiB, max 8 GiB.""" + 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.""" + validate_int(count, 3, 26, "NICs") + + @classmethod + def validate_serial_count(cls, count): + """CSR1000V supports 0-2 serial ports.""" + validate_int(count, 0, 2, "serial ports") diff --git a/COT/platforms/cisco_iosv.py b/COT/platforms/cisco_iosv.py new file mode 100644 index 0000000..6fb4914 --- /dev/null +++ b/COT/platforms/cisco_iosv.py @@ -0,0 +1,66 @@ +# 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. + +"""Package describing the Cisco IOSv virtual router platform.""" + +import logging + +from COT.platforms.generic import GenericPlatform +from COT.data_validation import ( + ValueTooLowError, ValueTooHighError, validate_int, +) + +logger = logging.getLogger(__name__) + + +class IOSv(GenericPlatform): + """Platform-specific logic for Cisco IOSv.""" + + PLATFORM_NAME = "Cisco IOSv" + + CONFIG_TEXT_FILE = 'ios_config.txt' + LITERAL_CLI_STRING = None + # IOSv has no CD-ROM driver so bootstrap configs must be provided on disk. + BOOTSTRAP_DISK_TYPE = 'harddisk' + SUPPORTED_NIC_TYPES = ["E1000"] + + @classmethod + def guess_nic_name(cls, nic_number): + """GigabitEthernet0/0, GigabitEthernet0/1, etc.""" + return "GigabitEthernet0/" + str(nic_number - 1) + + @classmethod + def validate_cpu_count(cls, cpus): + """IOSv only supports a single CPU.""" + validate_int(cpus, 1, 1, "CPUs") + + @classmethod + def validate_memory_amount(cls, mebibytes): + """IOSv has minimum 192 MiB (with minimal feature set), max 3 GiB.""" + if mebibytes < 192: + raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "192 MiB") + elif mebibytes < 384: + # 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.""" + validate_int(count, 0, 16, "NICs") + + @classmethod + def validate_serial_count(cls, count): + """IOSv requires 1-2 serial ports.""" + validate_int(count, 1, 2, "serial ports") diff --git a/COT/platforms/cisco_iosxrv.py b/COT/platforms/cisco_iosxrv.py new file mode 100644 index 0000000..6e30cab --- /dev/null +++ b/COT/platforms/cisco_iosxrv.py @@ -0,0 +1,116 @@ +# 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. + +"""Package describing the Cisco IOS XRv virtual router platform.""" + +import logging + +from COT.platforms.generic import GenericPlatform +from COT.data_validation import ( + ValueTooLowError, ValueTooHighError, validate_int, +) + +logger = logging.getLogger(__name__) + + +class IOSXRv(GenericPlatform): + """Platform-specific logic for Cisco IOS XRv platform.""" + + PLATFORM_NAME = "Cisco IOS XRv" + + CONFIG_TEXT_FILE = 'iosxr_config.txt' + SECONDARY_CONFIG_TEXT_FILE = 'iosxr_config_admin.txt' + LITERAL_CLI_STRING = None + SUPPORTED_NIC_TYPES = ["E1000", "virtio"] + + @classmethod + def guess_nic_name(cls, nic_number): + """MgmtEth0/0/CPU0/0, GigabitEthernet0/0/0/0, Gig0/0/0/1, etc.""" + if nic_number == 1: + return "MgmtEth0/0/CPU0/0" + else: + return "GigabitEthernet0/0/0/" + str(nic_number - 2) + + @classmethod + def validate_cpu_count(cls, cpus): + """IOS XRv supports 1-8 CPUs.""" + validate_int(cpus, 1, 8, "CPUs") + + @classmethod + def validate_memory_amount(cls, mebibytes): + """Minimum 3 GiB, max 8 GiB of RAM.""" + 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.""" + validate_int(count, 1, None, "NIC count") + + @classmethod + def validate_serial_count(cls, count): + """IOS XRv supports 1-4 serial ports.""" + 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): + """Fabric and management only. + + * fabric + * MgmtEth0/{SLOT}/CPU0/0 + """ + if nic_number == 1: + return "fabric" + else: + return "MgmtEth0/{SLOT}/CPU0/" + str(nic_number - 2) + + @classmethod + def validate_nic_count(cls, count): + """Fabric plus an optional management NIC.""" + validate_int(count, 1, 2, "NIC count") + + +class IOSXRvLC(IOSXRv): + """Platform-specific logic for Cisco IOS XRv line card.""" + + PLATFORM_NAME = "Cisco IOS XRv line card" + + # No bootstrap config for LCs - they inherit from the RP + CONFIG_TEXT_FILE = None + SECONDARY_CONFIG_TEXT_FILE = None + + @classmethod + def guess_nic_name(cls, nic_number): + """Fabric interface plus slot-appropriate GigabitEthernet interfaces. + + * fabric + * GigabitEthernet0/{SLOT}/0/0 + * GigabitEthernet0/{SLOT}/0/1 + * etc. + """ + if nic_number == 1: + return "fabric" + 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.""" + validate_int(count, 0, 4, "serial ports") diff --git a/COT/platforms/cisco_iosxrv_9000.py b/COT/platforms/cisco_iosxrv_9000.py new file mode 100644 index 0000000..91dd5bd --- /dev/null +++ b/COT/platforms/cisco_iosxrv_9000.py @@ -0,0 +1,59 @@ +# 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. + +"""Package describing the Cisco IOS XRv 9000 virtual router platform.""" + +import logging + +from COT.platforms.cisco_iosxrv import IOSXRv +from COT.data_validation import ( + ValueTooLowError, ValueTooHighError, validate_int, +) + +logger = logging.getLogger(__name__) + + +class IOSXRv9000(IOSXRv): + """Platform-specific logic for Cisco IOS XRv 9000 platform.""" + + PLATFORM_NAME = "Cisco IOS XRv 9000" + SUPPORTED_NIC_TYPES = ["E1000", "virtio", "VMXNET3"] + + @classmethod + def guess_nic_name(cls, nic_number): + """MgmtEth0/0/CPU0/0, CtrlEth, DevEth, GigabitEthernet0/0/0/0, etc.""" + if nic_number == 1: + return "MgmtEth0/0/CPU0/0" + elif nic_number == 2: + return "CtrlEth" + elif nic_number == 3: + return "DevEth" + else: + return "GigabitEthernet0/0/0/" + str(nic_number - 4) + + @classmethod + def validate_cpu_count(cls, cpus): + """Minimum 1, maximum 32 CPUs.""" + validate_int(cpus, 1, 32, "CPUs") + + @classmethod + def validate_memory_amount(cls, mebibytes): + """Minimum 8 GiB, maximum 32 GiB.""" + if mebibytes < 8192: + raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "8 GiB") + elif mebibytes > 32768: + raise ValueTooHighError("RAM", str(mebibytes) + " MiB", "32 GiB") + + @classmethod + def validate_nic_count(cls, count): + """IOS XRv 9000 requires at least 4 NICs.""" + validate_int(count, 4, None, "NIC count") diff --git a/COT/platforms/cisco_nxosv.py b/COT/platforms/cisco_nxosv.py new file mode 100644 index 0000000..4e687ad --- /dev/null +++ b/COT/platforms/cisco_nxosv.py @@ -0,0 +1,69 @@ +# 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. + +"""Package describing the Cisco NX-OSv virtual switch platform.""" + +import logging + +from COT.platforms.generic import GenericPlatform +from COT.data_validation import ( + ValueTooLowError, ValueTooHighError, validate_int, +) + +logger = logging.getLogger(__name__) + + +class NXOSv(GenericPlatform): + """Platform-specific logic for Cisco NX-OSv (Titanium).""" + + PLATFORM_NAME = "Cisco NX-OSv" + + CONFIG_TEXT_FILE = 'nxos_config.txt' + LITERAL_CLI_STRING = None + SUPPORTED_NIC_TYPES = ["E1000", "virtio"] + + @classmethod + def guess_nic_name(cls, nic_number): + """NX-OSv names its NICs a bit interestingly... + + * mgmt0 + * Ethernet2/1 + * Ethernet2/2 + * ... + * Ethernet2/48 + * Ethernet3/1 + * Ethernet3/2 + * ... + """ + if nic_number == 1: + return "mgmt0" + else: + 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.""" + validate_int(cpus, 1, 8, "CPUs") + + @classmethod + def validate_memory_amount(cls, mebibytes): + """NX-OSv requires 2-8 GiB of RAM.""" + 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.""" + validate_int(count, 1, 2, "serial ports") diff --git a/COT/platforms/generic.py b/COT/platforms/generic.py new file mode 100644 index 0000000..1694771 --- /dev/null +++ b/COT/platforms/generic.py @@ -0,0 +1,89 @@ +# 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, 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 + + @classmethod + def controller_type_for_device(cls, _device_type): + """Get the default controller type for the given device type.""" + # 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! + """ + 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.""" + validate_int(cpus, 1, None, "CPUs") + + @classmethod + def validate_memory_amount(cls, mebibytes): + """Throw an error if the amount of RAM is not supported.""" + validate_int(mebibytes, 1, None, "RAM") + + @classmethod + def validate_nic_count(cls, count): + """Throw an error if the number of NICs is not supported.""" + 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` + """ + 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.""" + 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.""" + validate_int(count, 0, None, "serial port count") diff --git a/setup.py b/setup.py index f508172..3ce4b16 100644 --- a/setup.py +++ b/setup.py @@ -122,7 +122,7 @@ def with_project_on_sys_path(self, func): # Package contents cmdclass=cmdclass, - packages=['COT', 'COT.helpers', 'COT.ovf'], + packages=['COT', 'COT.helpers', 'COT.ovf', 'COT.platforms'], package_data={ 'COT': ['docs/man/*'], }, From 7e64ec23b37c048f4f9104dc08d237c5ea0d64b9 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 22 Sep 2016 18:48:30 -0400 Subject: [PATCH 13/59] Documentation fixes for COT.platforms --- COT/platforms/__init__.py | 51 +++++++++++++----------- COT/platforms/cisco_csr1000v.py | 2 +- COT/platforms/cisco_iosv.py | 2 +- COT/platforms/cisco_iosxrv.py | 12 +++++- COT/platforms/cisco_iosxrv_9000.py | 2 +- COT/platforms/cisco_nxosv.py | 2 +- docs/COT.platforms.cisco_csr1000v.rst | 4 ++ docs/COT.platforms.cisco_iosv.rst | 4 ++ docs/COT.platforms.cisco_iosxrv.rst | 4 ++ docs/COT.platforms.cisco_iosxrv_9000.rst | 4 ++ docs/COT.platforms.cisco_nxosv.rst | 4 ++ docs/COT.platforms.generic.rst | 4 ++ docs/COT.platforms.rst | 14 +------ docs/index.rst | 1 + 14 files changed, 69 insertions(+), 41 deletions(-) create mode 100644 docs/COT.platforms.cisco_csr1000v.rst create mode 100644 docs/COT.platforms.cisco_iosv.rst create mode 100644 docs/COT.platforms.cisco_iosxrv.rst create mode 100644 docs/COT.platforms.cisco_iosxrv_9000.rst create mode 100644 docs/COT.platforms.cisco_nxosv.rst create mode 100644 docs/COT.platforms.generic.rst diff --git a/COT/platforms/__init__.py b/COT/platforms/__init__.py index 032cc11..dba680e 100644 --- a/COT/platforms/__init__.py +++ b/COT/platforms/__init__.py @@ -12,15 +12,16 @@ """Package for identifying guest platforms and handling platform differences. -The :class:`COT.platforms.GenericPlatform` class describes the API and provides -a generic API implementation that can be overridden by subclasses to provide -platform-specific logic. +The :class:`~COT.platforms.generic.GenericPlatform` 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:`platform_from_product_class` API to derive the -appropriate subclass instance. +instead use the :func:`~COT.platforms.platform_from_product_class` API to +derive the appropriate subclass instance. -**Functions** +API +--- .. autosummary:: :nosignatures: @@ -28,24 +29,18 @@ is_known_product_class platform_from_product_class -**Classes** +Platform modules +---------------- .. autosummary:: - :nosignatures: - - GenericPlatform - CSR1000V - IOSv - IOSXRv - IOSXRvRP - IOSXRvLC - IOSXRv9000 - NXOSv - -**Constants** - -.. autosummary:: - PRODUCT_PLATFORM_MAP + :toctree: + + COT.platforms.generic + COT.platforms.cisco_csr1000v + COT.platforms.cisco_iosv + COT.platforms.cisco_iosxrv + COT.platforms.cisco_iosxrv_9000 + COT.platforms.cisco_nxosv """ import logging @@ -76,12 +71,20 @@ def is_known_product_class(product_class): - """Determine if the given product class string is a known one.""" + """Determine if the given product class string is a known one. + + :param str product_class: String such as 'com.cisco.iosv' + :rtype: boolean + """ 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.""" + """Get the class of Platform corresponding to a product class string. + + :param str product_class: String such as 'com.cisco.iosv' + :return: Class object - GenericPlatform or a subclass of it + """ if product_class is None: return GenericPlatform if is_known_product_class(product_class): diff --git a/COT/platforms/cisco_csr1000v.py b/COT/platforms/cisco_csr1000v.py index 76a21b2..4fa6800 100644 --- a/COT/platforms/cisco_csr1000v.py +++ b/COT/platforms/cisco_csr1000v.py @@ -10,7 +10,7 @@ # of COT, including this file, may be copied, modified, propagated, or # distributed except according to the terms contained in the LICENSE.txt file. -"""Package describing the Cisco CSR1000V virtual router platform.""" +"""Platform logic for the Cisco CSR1000V virtual router.""" import logging diff --git a/COT/platforms/cisco_iosv.py b/COT/platforms/cisco_iosv.py index 6fb4914..478c496 100644 --- a/COT/platforms/cisco_iosv.py +++ b/COT/platforms/cisco_iosv.py @@ -10,7 +10,7 @@ # of COT, including this file, may be copied, modified, propagated, or # distributed except according to the terms contained in the LICENSE.txt file. -"""Package describing the Cisco IOSv virtual router platform.""" +"""Platform logic for the Cisco IOSv virtual router.""" import logging diff --git a/COT/platforms/cisco_iosxrv.py b/COT/platforms/cisco_iosxrv.py index 6e30cab..6fe920a 100644 --- a/COT/platforms/cisco_iosxrv.py +++ b/COT/platforms/cisco_iosxrv.py @@ -10,7 +10,17 @@ # of COT, including this file, may be copied, modified, propagated, or # distributed except according to the terms contained in the LICENSE.txt file. -"""Package describing the Cisco IOS XRv virtual router platform.""" +"""Platform logic for the Cisco IOS XRv virtual router and its variants. + +**Classes** + +.. autosummary:: + :nosignatures: + + IOSXRv + IOSXRvLC + IOSXRvRP +""" import logging diff --git a/COT/platforms/cisco_iosxrv_9000.py b/COT/platforms/cisco_iosxrv_9000.py index 91dd5bd..290d708 100644 --- a/COT/platforms/cisco_iosxrv_9000.py +++ b/COT/platforms/cisco_iosxrv_9000.py @@ -10,7 +10,7 @@ # of COT, including this file, may be copied, modified, propagated, or # distributed except according to the terms contained in the LICENSE.txt file. -"""Package describing the Cisco IOS XRv 9000 virtual router platform.""" +"""Platform logic for the Cisco IOS XRv 9000 virtual router.""" import logging diff --git a/COT/platforms/cisco_nxosv.py b/COT/platforms/cisco_nxosv.py index 4e687ad..af3a8ae 100644 --- a/COT/platforms/cisco_nxosv.py +++ b/COT/platforms/cisco_nxosv.py @@ -10,7 +10,7 @@ # of COT, including this file, may be copied, modified, propagated, or # distributed except according to the terms contained in the LICENSE.txt file. -"""Package describing the Cisco NX-OSv virtual switch platform.""" +"""Platform logic for the Cisco NX-OSv virtual switch.""" import logging diff --git a/docs/COT.platforms.cisco_csr1000v.rst b/docs/COT.platforms.cisco_csr1000v.rst new file mode 100644 index 0000000..59d67dc --- /dev/null +++ b/docs/COT.platforms.cisco_csr1000v.rst @@ -0,0 +1,4 @@ +``COT.platforms.cisco_csr1000v`` module +======================================= + +.. automodule:: COT.platforms.cisco_csr1000v diff --git a/docs/COT.platforms.cisco_iosv.rst b/docs/COT.platforms.cisco_iosv.rst new file mode 100644 index 0000000..fa40aff --- /dev/null +++ b/docs/COT.platforms.cisco_iosv.rst @@ -0,0 +1,4 @@ +``COT.platforms.cisco_iosv`` module +=================================== + +.. automodule:: COT.platforms.cisco_iosv diff --git a/docs/COT.platforms.cisco_iosxrv.rst b/docs/COT.platforms.cisco_iosxrv.rst new file mode 100644 index 0000000..0e72f59 --- /dev/null +++ b/docs/COT.platforms.cisco_iosxrv.rst @@ -0,0 +1,4 @@ +``COT.platforms.cisco_iosxrv`` module +===================================== + +.. automodule:: COT.platforms.cisco_iosxrv diff --git a/docs/COT.platforms.cisco_iosxrv_9000.rst b/docs/COT.platforms.cisco_iosxrv_9000.rst new file mode 100644 index 0000000..3f3420d --- /dev/null +++ b/docs/COT.platforms.cisco_iosxrv_9000.rst @@ -0,0 +1,4 @@ +``COT.platforms.cisco_iosxrv_9000`` module +========================================== + +.. automodule:: COT.platforms.cisco_iosxrv_9000 diff --git a/docs/COT.platforms.cisco_nxosv.rst b/docs/COT.platforms.cisco_nxosv.rst new file mode 100644 index 0000000..30de55a --- /dev/null +++ b/docs/COT.platforms.cisco_nxosv.rst @@ -0,0 +1,4 @@ +``COT.platforms.cisco_nxosv`` module +==================================== + +.. automodule:: COT.platforms.cisco_nxosv diff --git a/docs/COT.platforms.generic.rst b/docs/COT.platforms.generic.rst new file mode 100644 index 0000000..a9f9506 --- /dev/null +++ b/docs/COT.platforms.generic.rst @@ -0,0 +1,4 @@ +``COT.platforms.generic`` module +================================ + +.. automodule:: COT.platforms.generic diff --git a/docs/COT.platforms.rst b/docs/COT.platforms.rst index 2d7b44d..3242fb6 100644 --- a/docs/COT.platforms.rst +++ b/docs/COT.platforms.rst @@ -1,14 +1,4 @@ -``COT.platforms`` module -======================== +``COT.platforms`` package reference +=================================== .. automodule:: COT.platforms - :no-members: - -.. autoclass:: COT.platforms.GenericPlatform -.. autoclass:: COT.platforms.CSR1000V -.. autoclass:: COT.platforms.IOSv -.. autoclass:: COT.platforms.IOSXRv -.. autoclass:: COT.platforms.IOSXRvRP -.. autoclass:: COT.platforms.IOSXRvLC -.. autoclass:: COT.platforms.NXOSv - diff --git a/docs/index.rst b/docs/index.rst index f672728..9e275cd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,7 @@ Common OVF Tool (COT) COT COT.helpers COT.ovf + COT.platforms Indices and tables From 0ef14ccfd0d7ff05e085c48bae14d68f76aa2d1b Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 22 Sep 2016 21:07:31 -0400 Subject: [PATCH 14/59] Exclude COT/platforms/tests from coverage --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index a9164d4..5626556 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,7 @@ omit = COT/tests/* COT/helpers/tests/* COT/ovf/tests/* + COT/platforms/tests/* COT/_version.py setup.py versioneer.py From 19857811ef9a57b62b23e546c47fac44d678f69f Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 28 Sep 2016 11:30:37 -0400 Subject: [PATCH 15/59] Move COT.helpers.get_checksum to COT.data_validation.file_checksum --- COT/data_validation.py | 41 +++++++++++++++++++++++++++++++ COT/helpers/__init__.py | 3 --- COT/helpers/api.py | 41 ------------------------------- COT/helpers/tests/test_api.py | 34 +------------------------ COT/ovf/ovf.py | 13 +++++----- COT/tests/test_data_validation.py | 38 +++++++++++++++++++++++++++- 6 files changed, 86 insertions(+), 84 deletions(-) diff --git a/COT/data_validation.py b/COT/data_validation.py index 1d1baaf..a7ed472 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -39,6 +39,7 @@ canonicalize_scsi_subtype check_for_conflict device_address + file_checksum mac_address match_or_die natural_sort @@ -56,6 +57,7 @@ """ import xml.etree.ElementTree as ET +import hashlib import re from distutils.util import strtobool @@ -220,6 +222,45 @@ def check_for_conflict(label, li): return obj +def file_checksum(path_or_obj, checksum_type): + """Get the checksum of the given file. + + :param str path_or_obj: File path to checksum OR an opened file object + :param str checksum_type: Supported values are 'md5' and 'sha1'. + :return: String containing hexadecimal file checksum + """ + # pylint: disable=redefined-variable-type + if checksum_type == 'md5': + h = hashlib.md5() + elif checksum_type == 'sha1': + h = hashlib.sha1() + else: + raise NotImplementedError( + "No support for generating checksum type {0}" + .format(checksum_type)) + + # Is it a file or do we need to open it? + try: + path_or_obj.read(0) + file_obj = path_or_obj + except AttributeError: + file_obj = open(path_or_obj, 'rb') + + BLOCKSIZE = 65536 + + try: + while True: + buf = file_obj.read(BLOCKSIZE) + if len(buf) == 0: + break + h.update(buf) + finally: + if file_obj != path_or_obj: + file_obj.close() + + return h.hexdigest() + + def mac_address(string): """Parser helper function for MAC address arguments. diff --git a/COT/helpers/__init__.py b/COT/helpers/__init__.py index 723c472..b5ef8f8 100644 --- a/COT/helpers/__init__.py +++ b/COT/helpers/__init__.py @@ -27,7 +27,6 @@ ~COT.helpers.api.convert_disk_image ~COT.helpers.api.create_disk_image - ~COT.helpers.api.get_checksum ~COT.helpers.api.get_disk_capacity ~COT.helpers.api.get_disk_format ~COT.helpers.api.install_file @@ -62,7 +61,6 @@ create_disk_image, create_install_dir, download_and_expand, - get_checksum, get_disk_capacity, get_disk_format, install_file, @@ -76,7 +74,6 @@ 'create_disk_image', 'create_install_dir', 'download_and_expand', - 'get_checksum', 'get_disk_capacity', 'get_disk_format', 'install_file', diff --git a/COT/helpers/api.py b/COT/helpers/api.py index ed93660..43cd021 100644 --- a/COT/helpers/api.py +++ b/COT/helpers/api.py @@ -31,14 +31,12 @@ create_disk_image create_install_dir download_and_expand - get_checksum get_disk_capacity get_disk_format install_file """ import contextlib -import hashlib import logging import os import re @@ -63,8 +61,6 @@ QEMUIMG = QEMUImg() VMDKTOOL = VmdkTool() -BLOCKSIZE = 65536 - try: # Python 3.x from tempfile import TemporaryDirectory @@ -126,43 +122,6 @@ def download_and_expand(url): logger.debug("Cleaning up temporary directory %s", d) -def get_checksum(path_or_obj, checksum_type): - """Get the checksum of the given file. - - :param str path_or_obj: File path to checksum OR an opened file object - :param str checksum_type: Supported values are 'md5' and 'sha1'. - :return: String containing hexadecimal file checksum - """ - # pylint: disable=redefined-variable-type - if checksum_type == 'md5': - h = hashlib.md5() - elif checksum_type == 'sha1': - h = hashlib.sha1() - else: - raise NotImplementedError( - "No support for generating checksum type {0}" - .format(checksum_type)) - - # Is it a file or do we need to open it? - try: - path_or_obj.read(0) - file_obj = path_or_obj - except AttributeError: - file_obj = open(path_or_obj, 'rb') - - try: - while True: - buf = file_obj.read(BLOCKSIZE) - if len(buf) == 0: - break - h.update(buf) - finally: - if file_obj != path_or_obj: - file_obj.close() - - return h.hexdigest() - - def get_disk_format(file_path): """Get the disk image format of the given file. diff --git a/COT/helpers/tests/test_api.py b/COT/helpers/tests/test_api.py index f907749..57fa854 100644 --- a/COT/helpers/tests/test_api.py +++ b/COT/helpers/tests/test_api.py @@ -24,7 +24,7 @@ from COT.tests.ut import COT_UT from COT.helpers import ( - get_checksum, create_disk_image, convert_disk_image, get_disk_format, + create_disk_image, convert_disk_image, get_disk_format, get_disk_capacity, create_install_dir, install_file, ) from COT.helpers import HelperError, HelperNotFoundError @@ -32,38 +32,6 @@ logger = logging.getLogger(__name__) -class TestGetChecksum(COT_UT): - """Test cases for get_checksum() function.""" - - def test_get_checksum_md5(self): - """Test case for get_checksum() with md5 sum.""" - checksum = get_checksum(self.input_ovf, 'md5') - self.assertEqual(checksum, "4e7a3ba0b70f6784a3a91b18336296c7") - - checksum = get_checksum(self.minimal_ovf, 'md5') - self.assertEqual(checksum, "288e1e3fcb05265cd9b8c7578e173fef") - - def test_get_checksum_sha1(self): - """Test case for get_checksum() with sha1 sum.""" - checksum = get_checksum(self.input_ovf, 'sha1') - self.assertEqual(checksum, "c3bd2579c2edc76ea35b5bde7d4f4e41eab08963") - - checksum = get_checksum(self.minimal_ovf, 'sha1') - self.assertEqual(checksum, - "5d0635163f6a580442f01466245e122f8412e8d6") - - def test_get_checksum_unsupported(self): - """Test invalid options to get_checksum().""" - self.assertRaises(NotImplementedError, - get_checksum, - self.input_ovf, - 'sha256') - self.assertRaises(NotImplementedError, - get_checksum, - self.input_ovf, - 'crc') - - class TestGetDiskFormat(COT_UT): """Test cases for get_disk_format() function.""" diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index 0771e00..75b59b6 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -51,11 +51,12 @@ from COT.xml_file import XML, register_namespace from COT.vm_description import VMDescription, VMInitError -from COT.data_validation import match_or_die, check_for_conflict -from COT.data_validation import ValueTooHighError, ValueUnsupportedError -from COT.data_validation import canonicalize_nic_subtype +from COT.data_validation import ( + match_or_die, check_for_conflict, file_checksum, + ValueTooHighError, ValueUnsupportedError, canonicalize_nic_subtype, +) from COT.file_reference import FileOnDisk, FileInTAR -from COT.helpers import get_checksum, get_disk_capacity, convert_disk_image +from COT.helpers import get_disk_capacity, convert_disk_image from COT.platforms import platform_from_product_class, GenericPlatform from COT.ovf.name_helper import name_helper @@ -2415,7 +2416,7 @@ def generate_manifest(self, ovf_file): logger.verbose("Generating manifest for %s", ovf_file) manifest = prefix + '.mf' # TODO: OVF 2.0 uses SHA256 instead of SHA1. - sha1sum = get_checksum(ovf_file, 'sha1') + sha1sum = file_checksum(ovf_file, 'sha1') with open(manifest, 'wb') as f: f.write("SHA1({file})= {sum}\n" .format(file=os.path.basename(ovf_file), sum=sha1sum) @@ -2426,7 +2427,7 @@ def generate_manifest(self, ovf_file): file_ref = self._file_references[file_name] try: file_obj = file_ref.open('rb') - sha1sum = get_checksum(file_obj, 'sha1') + sha1sum = file_checksum(file_obj, 'sha1') finally: file_ref.close() diff --git a/COT/tests/test_data_validation.py b/COT/tests/test_data_validation.py index 1eca7ec..7b0c20e 100644 --- a/COT/tests/test_data_validation.py +++ b/COT/tests/test_data_validation.py @@ -17,6 +17,7 @@ """Unit test cases for COT.data_validation module.""" import re +import logging try: import unittest2 as unittest @@ -24,13 +25,48 @@ import unittest from COT.data_validation import ( - match_or_die, + match_or_die, file_checksum, canonicalize_helper, canonicalize_nic_subtype, NIC_TYPES, mac_address, device_address, no_whitespace, truth_value, validate_int, non_negative_int, positive_int, InvalidInputError, ValueMismatchError, ValueUnsupportedError, ValueTooLowError, ValueTooHighError, ) +from COT.tests.ut import COT_UT + +logger = logging.getLogger(__name__) + + +class TestFileChecksum(COT_UT): + """Test cases for file_checksum() function.""" + + def test_file_checksum_md5(self): + """Test case for file_checksum() with md5 sum.""" + checksum = file_checksum(self.input_ovf, 'md5') + self.assertEqual(checksum, "4e7a3ba0b70f6784a3a91b18336296c7") + + checksum = file_checksum(self.minimal_ovf, 'md5') + self.assertEqual(checksum, "288e1e3fcb05265cd9b8c7578e173fef") + + def test_file_checksum_sha1(self): + """Test case for file_checksum() with sha1 sum.""" + checksum = file_checksum(self.input_ovf, 'sha1') + self.assertEqual(checksum, "c3bd2579c2edc76ea35b5bde7d4f4e41eab08963") + + checksum = file_checksum(self.minimal_ovf, 'sha1') + self.assertEqual(checksum, + "5d0635163f6a580442f01466245e122f8412e8d6") + + def test_file_checksum_unsupported(self): + """Test invalid options to file_checksum().""" + self.assertRaises(NotImplementedError, + file_checksum, + self.input_ovf, + 'sha256') + self.assertRaises(NotImplementedError, + file_checksum, + self.input_ovf, + 'crc') class TestValidationFunctions(unittest.TestCase): From 52fb5919459e86ae7aa4beeda817378fd7265a4a Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 28 Sep 2016 11:52:36 -0400 Subject: [PATCH 16/59] Remove download_and_expand from the public API of COT.helpers module --- COT/helpers/__init__.py | 3 -- COT/helpers/api.py | 65 ------------------------------ COT/helpers/fatdisk.py | 3 +- COT/helpers/helper.py | 64 +++++++++++++++++++++++++++++ COT/helpers/tests/test_fatdisk.py | 4 +- COT/helpers/tests/test_helper.py | 14 +++---- COT/helpers/tests/test_vmdktool.py | 4 +- COT/helpers/vmdktool.py | 3 +- docs/COT.helpers.api.rst | 1 - docs/COT.helpers.helper.rst | 2 +- 10 files changed, 77 insertions(+), 86 deletions(-) diff --git a/COT/helpers/__init__.py b/COT/helpers/__init__.py index b5ef8f8..5673fc2 100644 --- a/COT/helpers/__init__.py +++ b/COT/helpers/__init__.py @@ -31,7 +31,6 @@ ~COT.helpers.api.get_disk_format ~COT.helpers.api.install_file ~COT.helpers.api.create_install_dir - ~COT.helpers.api.download_and_expand Exceptions ---------- @@ -60,7 +59,6 @@ convert_disk_image, create_disk_image, create_install_dir, - download_and_expand, get_disk_capacity, get_disk_format, install_file, @@ -73,7 +71,6 @@ 'convert_disk_image', 'create_disk_image', 'create_install_dir', - 'download_and_expand', 'get_disk_capacity', 'get_disk_format', 'install_file', diff --git a/COT/helpers/api.py b/COT/helpers/api.py index 43cd021..6c75de2 100644 --- a/COT/helpers/api.py +++ b/COT/helpers/api.py @@ -30,21 +30,16 @@ convert_disk_image create_disk_image create_install_dir - download_and_expand get_disk_capacity get_disk_format install_file """ -import contextlib import logging import os import re -import shutil -import tarfile from distutils.version import StrictVersion -import requests from .helper import Helper, guess_file_format_from_path from .fatdisk import FatDisk @@ -61,66 +56,6 @@ QEMUIMG = QEMUImg() VMDKTOOL = VmdkTool() -try: - # Python 3.x - from tempfile import TemporaryDirectory -except ImportError: - # Python 2.x - import tempfile - - @contextlib.contextmanager - def TemporaryDirectory(suffix='', # noqa: N802 - prefix='tmp', - dirpath=None): - """Create a temporary directory and make sure it's deleted later. - - Reimplementation of Python 3's ``tempfile.TemporaryDirectory``. - """ - tempdir = tempfile.mkdtemp(suffix, prefix, dirpath) - try: - yield tempdir - finally: - shutil.rmtree(tempdir) - - -@contextlib.contextmanager -def download_and_expand(url): - """Context manager for downloading and expanding a .tar.gz file. - - Creates a temporary directory, downloads the specified URL into - the directory, unzips and untars the file into this directory, - then yields to the given block. When the block exits, the temporary - directory and its contents are deleted. - - :: - - with download_and_expand("http://example.com/foo.tgz") as d: - # archive contents have been extracted to 'd' - ... - # d is automatically cleaned up. - - :param str url: URL of a .tgz or .tar.gz file to download. - """ - with TemporaryDirectory(prefix="cot_helper") as d: - logger.debug("Temporary directory is %s", d) - logger.verbose("Downloading and extracting %s", url) - response = requests.get(url, stream=True) - tgz = os.path.join(d, 'helper.tgz') - with open(tgz, 'wb') as f: - shutil.copyfileobj(response.raw, f) - del response - logger.debug("Extracting %s", tgz) - # the "with tarfile.open()..." construct isn't supported in 2.6 - tarf = tarfile.open(tgz, "r:gz") - try: - tarf.extractall(path=d) - finally: - tarf.close() - try: - yield d - finally: - logger.debug("Cleaning up temporary directory %s", d) - def get_disk_format(file_path): """Get the disk image format of the given file. diff --git a/COT/helpers/fatdisk.py b/COT/helpers/fatdisk.py index 898b65a..86dfd4c 100644 --- a/COT/helpers/fatdisk.py +++ b/COT/helpers/fatdisk.py @@ -24,7 +24,6 @@ import os.path import platform -import COT.helpers from COT.helpers.helper import Helper logger = logging.getLogger(__name__) @@ -79,7 +78,7 @@ def install_helper(self): pass elif platform.system() == 'Linux': self._install_linux_prereqs() - with COT.helpers.download_and_expand( + with self.download_and_expand_tgz( 'https://github.com/goblinhack/' 'fatdisk/archive/v1.0.0-beta.tar.gz') as d: new_d = os.path.join(d, 'fatdisk-1.0.0-beta') diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index eb2a0dc..41d804e 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -23,18 +23,42 @@ import logging import os import os.path +import contextlib import errno import re import shutil import subprocess +import tarfile import distutils.spawn from distutils.version import StrictVersion +import requests from verboselogs import VerboseLogger logging.setLoggerClass(VerboseLogger) logger = logging.getLogger(__name__) +try: + # Python 3.x + from tempfile import TemporaryDirectory +except ImportError: + # Python 2.x + import tempfile + + @contextlib.contextmanager + def TemporaryDirectory(suffix='', # noqa: N802 + prefix='tmp', + dirpath=None): + """Create a temporary directory and make sure it's deleted later. + + Reimplementation of Python 3's ``tempfile.TemporaryDirectory``. + """ + tempdir = tempfile.mkdtemp(suffix, prefix, dirpath) + try: + yield tempdir + finally: + shutil.rmtree(tempdir) + def guess_file_format_from_path(file_path): """Guess the preferred file format based on file path/extension.""" @@ -73,6 +97,7 @@ class Helper(object): :nosignatures: confirm + download_and_expand_tgz apt_install port_install yum_install @@ -117,6 +142,45 @@ def confirm(cls, _prompt): } """Class-level lookup for package manager executables.""" + @staticmethod + @contextlib.contextmanager + def download_and_expand_tgz(url): + """Context manager for downloading and expanding a .tar.gz file. + + Creates a temporary directory, downloads the specified URL into + the directory, unzips and untars the file into this directory, + then yields to the given block. When the block exits, the temporary + directory and its contents are deleted. + + :: + + with download_and_expand_tgz("http://example.com/foo.tgz") as d: + # archive contents have been extracted to 'd' + ... + # d is automatically cleaned up. + + :param str url: URL of a .tgz or .tar.gz file to download. + """ + with TemporaryDirectory(prefix="cot_helper") as d: + logger.debug("Temporary directory is %s", d) + logger.verbose("Downloading and extracting %s", url) + response = requests.get(url, stream=True) + tgz = os.path.join(d, 'helper.tgz') + with open(tgz, 'wb') as f: + shutil.copyfileobj(response.raw, f) + del response + logger.debug("Extracting %s", tgz) + # the "with tarfile.open()..." construct isn't supported in 2.6 + tarf = tarfile.open(tgz, "r:gz") + try: + tarf.extractall(path=d) + finally: + tarf.close() + try: + yield d + finally: + logger.debug("Cleaning up temporary directory %s", d) + @staticmethod def find_executable(name): """Wrapper for :func:`distutils.spawn.find_executable`.""" diff --git a/COT/helpers/tests/test_fatdisk.py b/COT/helpers/tests/test_fatdisk.py index bd8c726..76817b3 100644 --- a/COT/helpers/tests/test_fatdisk.py +++ b/COT/helpers/tests/test_fatdisk.py @@ -29,8 +29,8 @@ logger = logging.getLogger(__name__) -@mock.patch('COT.helpers.download_and_expand', - side_effect=HelperUT.stub_download_and_expand) +@mock.patch('COT.helpers.fatdisk.FatDisk.download_and_expand_tgz', + side_effect=HelperUT.stub_download_and_expand_tgz) class TestFatDisk(HelperUT): """Test cases for FatDisk helper class.""" diff --git a/COT/helpers/tests/test_helper.py b/COT/helpers/tests/test_helper.py index 14a3c03..2624e10 100644 --- a/COT/helpers/tests/test_helper.py +++ b/COT/helpers/tests/test_helper.py @@ -25,9 +25,7 @@ import mock from COT.tests.ut import COT_UT -from COT.helpers.api import TemporaryDirectory -from COT.helpers.helper import Helper -import COT.helpers +from COT.helpers.helper import Helper, TemporaryDirectory from COT.helpers import HelperError, HelperNotFoundError logger = logging.getLogger(__name__) @@ -154,8 +152,8 @@ def yum_install_test(self, pkgname, *_): @staticmethod @contextlib.contextmanager - def stub_download_and_expand(_url): - """Stub for Helper.download_and_expand - create a fake directory.""" + def stub_download_and_expand_tgz(_url): + """Stub for Helper.download_and_expand_tgz - make a fake directory.""" with TemporaryDirectory(prefix="cot_ut_helper") as d: yield d @@ -311,10 +309,10 @@ def test_call_helper_no_install(self, *_): self.assertRaises(HelperNotFoundError, self.helper.call_helper, ["Hello!"]) - def test_download_and_expand(self): - """Validate the download_and_expand() context_manager.""" + def test_download_and_expand_tgz(self): + """Validate the download_and_expand_tgz() context_manager.""" try: - with COT.helpers.download_and_expand( + with self.helper.download_and_expand_tgz( "https://github.com/glennmatthews/cot/archive/master.tar.gz" ) as directory: self.assertTrue(os.path.exists(directory)) diff --git a/COT/helpers/tests/test_vmdktool.py b/COT/helpers/tests/test_vmdktool.py index 23d2a60..198bc00 100644 --- a/COT/helpers/tests/test_vmdktool.py +++ b/COT/helpers/tests/test_vmdktool.py @@ -25,8 +25,8 @@ from COT.helpers.vmdktool import VmdkTool -@mock.patch('COT.helpers.download_and_expand', - side_effect=HelperUT.stub_download_and_expand) +@mock.patch('COT.helpers.vmdktool.VmdkTool.download_and_expand_tgz', + side_effect=HelperUT.stub_download_and_expand_tgz) class TestVmdkTool(HelperUT): """Test cases for VmdkTool helper class.""" diff --git a/COT/helpers/vmdktool.py b/COT/helpers/vmdktool.py index c660c43..89d6a0c 100644 --- a/COT/helpers/vmdktool.py +++ b/COT/helpers/vmdktool.py @@ -24,7 +24,6 @@ import os.path import platform -import COT.helpers from COT.helpers.helper import Helper logger = logging.getLogger(__name__) @@ -72,7 +71,7 @@ def install_helper(self): if not (Helper.apt_install('zlib1g-dev') or Helper.yum_install('zlib-devel')): raise NotImplementedError("Not sure how to install 'zlib'") - with COT.helpers.download_and_expand( + with self.download_and_expand_tgz( 'http://people.freebsd.org/~brian/' 'vmdktool/vmdktool-1.4.tar.gz') as d: new_d = os.path.join(d, "vmdktool-1.4") diff --git a/docs/COT.helpers.api.rst b/docs/COT.helpers.api.rst index 638c1ed..6702bb8 100644 --- a/docs/COT.helpers.api.rst +++ b/docs/COT.helpers.api.rst @@ -2,5 +2,4 @@ =========================== .. automodule:: COT.helpers.api - :exclude-members: TemporaryDirectory diff --git a/docs/COT.helpers.helper.rst b/docs/COT.helpers.helper.rst index c58b096..7cf931d 100644 --- a/docs/COT.helpers.helper.rst +++ b/docs/COT.helpers.helper.rst @@ -3,4 +3,4 @@ .. automodule:: COT.helpers.helper :special-members: __init__ - :exclude-members: confirm + :exclude-members: TemporaryDirectory From 260cfb7bf803efe3a00b6035f27f3dae3ad204a1 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 4 Oct 2016 10:38:48 -0400 Subject: [PATCH 17/59] Add isoinfo helper --- COT/helpers/__init__.py | 4 ++ COT/helpers/api.py | 40 +++++++++-- COT/helpers/fatdisk.py | 30 +++++++++ COT/helpers/isoinfo.py | 115 ++++++++++++++++++++++++++++++++ COT/helpers/tests/test_api.py | 14 ++-- COT/install_helpers.py | 1 + COT/tests/test_inject_config.py | 24 +++++-- 7 files changed, 213 insertions(+), 15 deletions(-) create mode 100644 COT/helpers/isoinfo.py diff --git a/COT/helpers/__init__.py b/COT/helpers/__init__.py index 5673fc2..e294ba9 100644 --- a/COT/helpers/__init__.py +++ b/COT/helpers/__init__.py @@ -28,6 +28,7 @@ ~COT.helpers.api.convert_disk_image ~COT.helpers.api.create_disk_image ~COT.helpers.api.get_disk_capacity + ~COT.helpers.api.get_disk_file_listing ~COT.helpers.api.get_disk_format ~COT.helpers.api.install_file ~COT.helpers.api.create_install_dir @@ -49,6 +50,7 @@ COT.helpers.api COT.helpers.helper COT.helpers.fatdisk + COT.helpers.isoinfo COT.helpers.mkisofs COT.helpers.ovftool COT.helpers.qemu_img @@ -60,6 +62,7 @@ create_disk_image, create_install_dir, get_disk_capacity, + get_disk_file_listing, get_disk_format, install_file, ) @@ -72,6 +75,7 @@ 'create_disk_image', 'create_install_dir', 'get_disk_capacity', + 'get_disk_file_listing', 'get_disk_format', 'install_file', ) diff --git a/COT/helpers/api.py b/COT/helpers/api.py index 6c75de2..2870e32 100644 --- a/COT/helpers/api.py +++ b/COT/helpers/api.py @@ -31,6 +31,7 @@ create_disk_image create_install_dir get_disk_capacity + get_disk_file_listing get_disk_format install_file """ @@ -43,6 +44,7 @@ from .helper import Helper, guess_file_format_from_path from .fatdisk import FatDisk +from .isoinfo import IsoInfo from .mkisofs import MkIsoFS from .ovftool import OVFTool from .qemu_img import QEMUImg @@ -51,6 +53,7 @@ logger = logging.getLogger(__name__) FATDISK = FatDisk() +ISOINFO = IsoInfo() MKISOFS = MkIsoFS() OVFTOOL = OVFTool() QEMUIMG = QEMUImg() @@ -67,9 +70,17 @@ def get_disk_format(file_path): :param str file_path: Path to disk image file to inspect. :return: ``(format, subformat)`` - * ``format`` may be ``'vmdk'``, ``'raw'``, or ``'qcow2'`` - * ``subformat`` may be ``None``, or various strings for ``'vmdk'`` files. + * ``format`` may be "iso", "vmdk", "raw", or "qcow2" + * ``subformat`` may be ``None``, or various strings for "vmdk" files. """ + # isoinfo can identify ISO files, otherwise returning None + (file_format, subformat) = ISOINFO.get_disk_format(file_path) + if file_format: + return (file_format, subformat) + + # QEMUIMG can identify various disk image formats, but guesses 'raw' + # for any arbitrary file it doesn't identify. Thus, 'raw' results should be + # considered suspect, as warned in the docstring above. file_format = QEMUIMG.get_disk_format(file_path) if file_format == 'vmdk': @@ -100,6 +111,24 @@ def get_disk_capacity(file_path): return QEMUIMG.get_disk_capacity(file_path) +def get_disk_file_listing(file_path): + """Get the list of files on the given disk. + + :param str file_path: Path to disk image file to inspect. + :return: List of file paths, or None on failure + :raise NotImplementedError: if getting a file listing from the + given file type is not yet supported. + """ + (file_format, _) = get_disk_format(file_path) + if file_format == "iso": + return ISOINFO.get_disk_file_listing(file_path) + elif file_format == "raw": + return FATDISK.get_disk_file_listing(file_path) + else: + raise NotImplementedError("No support for getting a file listing from " + "a %s image (%s)", file_format, file_path) + + def convert_disk_image(file_path, output_dir, new_format, new_subformat=None): """Convert the given disk image to the requested format/subformat. @@ -120,8 +149,8 @@ def convert_disk_image(file_path, output_dir, new_format, new_subformat=None): * :attr:`file_path`, if no conversion was required * or a file path in :attr:`output_dir` containing the converted image - :raise ValueUnsupportedError: if the :attr:`new_format` and/or - :attr:`new_subformat` are not supported conversion targets. + :raise NotImplementedError: if the requested conversion is not + yet supported. """ curr_format, curr_subformat = get_disk_format(file_path) @@ -188,6 +217,9 @@ def create_disk_image(file_path, file_format=None, :param capacity: Disk capacity. A string like '16M' or '1G'. :param list contents: List of file paths to package into the created image. If not specified, the image will be left blank and unformatted. + + :raise NotImplementedError: if creation of the given image type is not + yet supported. """ if not capacity and not contents: raise RuntimeError("Either capacity or contents must be specified!") diff --git a/COT/helpers/fatdisk.py b/COT/helpers/fatdisk.py index 86dfd4c..138725a 100644 --- a/COT/helpers/fatdisk.py +++ b/COT/helpers/fatdisk.py @@ -23,6 +23,7 @@ import os import os.path import platform +import re from COT.helpers.helper import Helper @@ -39,6 +40,7 @@ class FatDisk(Helper): install_helper create_raw_image + get_disk_file_listing """ def __init__(self): @@ -127,3 +129,31 @@ def create_raw_image(self, file_path, contents, capacity=None): self.call_helper([file_path, 'fileadd', content_file, os.path.basename(content_file)]) logger.info("All requested files successfully added to %s", file_path) + + def get_disk_file_listing(self, file_path): + """Get the list of files on the given raw disk image. + + :param str file_path: Path to disk image file to inspect. + :return: List of file paths, or None on failure + """ + output = self.call_helper([file_path, "ls"]) + # Output looks like: + # + # -----aD 13706 2016 Aug 04 input.ovf + # Listed 1 entry + # + # where all we really want is the 'input.ovf' + result = [] + for line in output.split("\n"): + if not output: + continue + if re.match(r"^Listed", line): + continue + fields = line.split() + if not fields: + continue + if len(fields) < 6: + logger.warning("Unexpected line: %s", line) + continue + result.append(fields[5]) + return result diff --git a/COT/helpers/isoinfo.py b/COT/helpers/isoinfo.py new file mode 100644 index 0000000..163be78 --- /dev/null +++ b/COT/helpers/isoinfo.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# +# isoinfo.py - Helper for 'isoinfo' +# +# September 2016, Glenn F. Matthews +# Copyright (c) 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. + +"""Give COT access to isoinfo for inspecting ISO images. + +http://cdrecord.org/ +https://www.gnu.org/software/xorriso/ +""" + +import logging +import re + +from .helper import Helper, HelperError + +logger = logging.getLogger(__name__) + + +class IsoInfo(Helper): + """Helper provider for ``isoinfo``. + + http://cdrecord.org/ + + **Methods** + + .. autosummary:: + :nosignatures: + + install + get_disk_format + get_disk_file_listing + """ + + def __init__(self): + """Initializer""" + super(IsoInfo, self).__init__("isoinfo", + version_regexp=r"isoinfo ([0-9.]+)") + + def install(self): + """Install isoinfo.""" + if self.should_not_be_installed_but_is(): + return True + logger.info("Installing 'isoinfo'...") + # Same providers as mkisofs/genisoimage as it's in the same package. + return (Helper.port_install('cdrtools') or + Helper.yum_install('genisoimage') or + Helper.apt_install('genisoimage')) + + def get_disk_format(self, file_path): + """Get the major disk image format of the given file. + + :param str file_path: Path to disk image file to inspect. + :return: ``(format, subformat)``, such as: + + * ``(None, None)`` - file is not an ISO + * ``("iso", "")`` - ISO without Rock Ridge or Joliet extensions + * ``("iso", "J")`` - ISO with Joliet extensions + * ``("iso", "R")`` - ISO with Rock Ridge extensions + * ``("iso", "JR")`` - ISO with Joliet + Rock Ridge + """ + try: + output = self.call_helper(['-i', file_path, '-d']) + except HelperError: + # Not an ISO + return (None, None) + + # If no exception, isoinfo recognized it as an ISO file. + subformat = "" + if re.search(r"Joliet.*found", output): + subformat += "J" + if re.search(r"Rock Ridge.*found", output): + subformat += "R" + return ('iso', subformat) + + def get_disk_file_listing(self, file_path): + """Get the list of files on the given ISO. + + :param str file_path: Path to ISO file to inspect. + :return: List of file paths, or None on failure + """ + (iso, subformat) = self.get_disk_format(file_path) + if iso != "iso": + return None + args = ["-i", file_path, "-f"] + if "J" in subformat: + args.append("-J") + if "R" in subformat: + args.append("-R") + output = self.call_helper(args) + result = [] + for line in output.split("\n"): + # discard non-file output lines + if not line or line[0] != "/": + continue + # Non-Rock-Ridge, Non-Joliet filenames look like this in isoinfo: + # /IOSXR_CONFIG.TXT;1 + # but the actual filename thus is: + # /iosxr_config.txt + if "J" not in subformat and "R" not in subformat and ";1" in line: + line = line.lower()[:-2] + # Strip the leading '/' + result.append(line[1:]) + return result diff --git a/COT/helpers/tests/test_api.py b/COT/helpers/tests/test_api.py index 57fa854..f059533 100644 --- a/COT/helpers/tests/test_api.py +++ b/COT/helpers/tests/test_api.py @@ -25,9 +25,9 @@ from COT.tests.ut import COT_UT from COT.helpers import ( create_disk_image, convert_disk_image, get_disk_format, - get_disk_capacity, create_install_dir, install_file, + get_disk_capacity, get_disk_file_listing, create_install_dir, install_file, + HelperError, HelperNotFoundError, ) -from COT.helpers import HelperError, HelperNotFoundError logger = logging.getLogger(__name__) @@ -194,7 +194,9 @@ def test_create_iso_with_contents(self): create_disk_image(disk_path, contents=[self.input_ovf]) except HelperNotFoundError as e: self.fail(e.strerror) - # TODO check ISO contents + # Check contents + self.assertEqual(get_disk_file_listing(disk_path), + [os.path.basename(self.input_ovf)]) # Creation of empty disks is tested implicitly in other test classes # above - no need to repeat that here @@ -214,7 +216,8 @@ def test_create_raw_with_contents(self): self.assertEqual(capacity, "8388608") except HelperNotFoundError as e: self.fail(e.strerror) - # TODO check raw file contents + self.assertEqual(get_disk_file_listing(disk_path), + [os.path.basename(self.input_ovf)]) # Again, but now force the disk size try: @@ -230,7 +233,8 @@ def test_create_raw_with_contents(self): self.assertEqual(capacity, "67108864") except HelperNotFoundError as e: self.fail(e.strerror) - # TODO check raw file contents + self.assertEqual(get_disk_file_listing(disk_path), + [os.path.basename(self.input_ovf)]) @mock.patch('COT.helpers.helper.Helper._check_call') diff --git a/COT/install_helpers.py b/COT/install_helpers.py index 33631d5..b69b292 100644 --- a/COT/install_helpers.py +++ b/COT/install_helpers.py @@ -181,6 +181,7 @@ def run(self): """Verify all helper tools and install any that are missing.""" from COT.helpers.fatdisk import FatDisk from COT.helpers.mkisofs import MkIsoFS + # isoinfo comes with mkisofs so we skip it here from COT.helpers.ovftool import OVFTool from COT.helpers.qemu_img import QEMUImg from COT.helpers.vmdktool import VmdkTool diff --git a/COT/tests/test_inject_config.py b/COT/tests/test_inject_config.py index bb7acc2..6b05f18 100644 --- a/COT/tests/test_inject_config.py +++ b/COT/tests/test_inject_config.py @@ -25,6 +25,7 @@ from COT.inject_config import COTInjectConfig from COT.data_validation import InvalidInputError from COT.platforms import CSR1000V, IOSv, IOSXRv, IOSXRvLC +from COT.helpers import get_disk_file_listing class TestCOTInjectConfig(COT_UT): @@ -89,6 +90,7 @@ def test_inject_config_iso(self): self.instance.run() self.assertLogged(**self.OVERWRITING_DISK_ITEM) self.instance.finished() + config_iso = os.path.join(self.temp_dir, 'config.iso') self.check_diff(""" @@ -102,8 +104,11 @@ def test_inject_config_iso(self): + ovf:/file/config.iso 8""" .format(cfg_size=self.FILE_SIZE['sample_cfg.txt'], - config_size=os.path.getsize(os.path.join( - self.temp_dir, 'config.iso')))) + config_size=os.path.getsize(config_iso))) + # The sample_cfg.text should be renamed to the platform-specific + # file name for bootstrap config - in this case, config.txt + self.assertEqual(get_disk_file_listing(config_iso), + ["config.txt"]) def test_inject_config_iso_secondary(self): """Inject secondary config file on an ISO.""" @@ -123,6 +128,7 @@ def test_inject_config_iso_secondary(self): '2CPU-2GB-1NIC', 'VMXNET3', 'NIC type')) self.assertLogged(**self.invalid_hardware_warning( '2CPU-2GB-1NIC', '2048 MiB', 'RAM')) + config_iso = os.path.join(self.temp_dir, 'config.iso') self.check_diff(""" @@ -136,8 +142,11 @@ def test_inject_config_iso_secondary(self): + ovf:/file/config.iso 8""" .format(cfg_size=self.FILE_SIZE['sample_cfg.txt'], - config_size=os.path.getsize(os.path.join( - self.temp_dir, 'config.iso')))) + config_size=os.path.getsize(config_iso))) + # The sample_cfg.text should be renamed to the platform-specific + # file name for secondary bootstrap config + self.assertEqual(get_disk_file_listing(config_iso), + ["iosxr_config_admin.txt"]) def test_inject_config_vmdk(self): """Inject config file on a VMDK.""" @@ -151,6 +160,7 @@ def test_inject_config_vmdk(self): # to be OVF standard compliant, the new File must be created in the # same order relative to the other Files as the existing Disk is # to the other Disks. + config_vmdk = os.path.join(self.temp_dir, 'config.vmdk') self.check_diff(file1=self.iosv_ovf, expected=""" @@ -177,8 +187,10 @@ def test_inject_config_vmdk(self): + Configuration disk flash2""" .format(input_size=self.FILE_SIZE['input.vmdk'], - config_size=os.path.getsize(os.path.join( - self.temp_dir, 'config.vmdk')))) + config_size=os.path.getsize(config_vmdk))) + # TODO - we don't currently have a way to check VMDK file listing + # self.assertEqual(get_disk_file_listing(config_vmdk), + # ["ios_config.txt"]) def test_inject_config_repeatedly(self): """inject-config repeatedly.""" From ad80be03decd27169fc0208275b61e1327b01585 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 26 Sep 2016 16:12:16 -0400 Subject: [PATCH 18/59] Add extra-files option to inject-config. Fix various style issues and test gaps from previous commit(s). --- COT/data_validation.py | 4 +- COT/helpers/isoinfo.py | 35 ++++------- COT/helpers/mkisofs.py | 14 +++-- COT/helpers/tests/test_mkisofs.py | 18 +++++- COT/inject_config.py | 99 +++++++++++++++++++------------ COT/tests/test_inject_config.py | 70 +++++++++++++++++++++- docs/COT.helpers.isoinfo.rst | 4 ++ 7 files changed, 172 insertions(+), 72 deletions(-) create mode 100644 docs/COT.helpers.isoinfo.rst diff --git a/COT/data_validation.py b/COT/data_validation.py index a7ed472..2c81a79 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -246,11 +246,11 @@ def file_checksum(path_or_obj, checksum_type): except AttributeError: file_obj = open(path_or_obj, 'rb') - BLOCKSIZE = 65536 + blocksize = 65536 try: while True: - buf = file_obj.read(BLOCKSIZE) + buf = file_obj.read(blocksize) if len(buf) == 0: break h.update(buf) diff --git a/COT/helpers/isoinfo.py b/COT/helpers/isoinfo.py index 163be78..34d85f6 100644 --- a/COT/helpers/isoinfo.py +++ b/COT/helpers/isoinfo.py @@ -38,25 +38,16 @@ class IsoInfo(Helper): .. autosummary:: :nosignatures: - install get_disk_format get_disk_file_listing """ def __init__(self): - """Initializer""" + """Initializer.""" super(IsoInfo, self).__init__("isoinfo", version_regexp=r"isoinfo ([0-9.]+)") - def install(self): - """Install isoinfo.""" - if self.should_not_be_installed_but_is(): - return True - logger.info("Installing 'isoinfo'...") - # Same providers as mkisofs/genisoimage as it's in the same package. - return (Helper.port_install('cdrtools') or - Helper.yum_install('genisoimage') or - Helper.apt_install('genisoimage')) + # No install support as this is provided by MkIsoFS class. def get_disk_format(self, file_path): """Get the major disk image format of the given file. @@ -65,10 +56,8 @@ def get_disk_format(self, file_path): :return: ``(format, subformat)``, such as: * ``(None, None)`` - file is not an ISO - * ``("iso", "")`` - ISO without Rock Ridge or Joliet extensions - * ``("iso", "J")`` - ISO with Joliet extensions - * ``("iso", "R")`` - ISO with Rock Ridge extensions - * ``("iso", "JR")`` - ISO with Joliet + Rock Ridge + * ``("iso", None)`` - ISO without Rock Ridge or Joliet extensions + * ``("iso", "Rock Ridge")`` - ISO with Rock Ridge extensions """ try: output = self.call_helper(['-i', file_path, '-d']) @@ -77,11 +66,10 @@ def get_disk_format(self, file_path): return (None, None) # If no exception, isoinfo recognized it as an ISO file. - subformat = "" - if re.search(r"Joliet.*found", output): - subformat += "J" + subformat = None if re.search(r"Rock Ridge.*found", output): - subformat += "R" + subformat = "Rock Ridge" + # At this time we don't care about Joliet extensions return ('iso', subformat) def get_disk_file_listing(self, file_path): @@ -94,21 +82,20 @@ def get_disk_file_listing(self, file_path): if iso != "iso": return None args = ["-i", file_path, "-f"] - if "J" in subformat: - args.append("-J") - if "R" in subformat: + if subformat == "Rock Ridge": args.append("-R") + # At this time we don't support Joliet extensions output = self.call_helper(args) result = [] for line in output.split("\n"): # discard non-file output lines if not line or line[0] != "/": continue - # Non-Rock-Ridge, Non-Joliet filenames look like this in isoinfo: + # Non-Rock-Ridge filenames look like this in isoinfo: # /IOSXR_CONFIG.TXT;1 # but the actual filename thus is: # /iosxr_config.txt - if "J" not in subformat and "R" not in subformat and ";1" in line: + if subformat != "Rock Ridge" and ";1" in line: line = line.lower()[:-2] # Strip the leading '/' result.append(line[1:]) diff --git a/COT/helpers/mkisofs.py b/COT/helpers/mkisofs.py index 0a109d5..56b3dc1 100644 --- a/COT/helpers/mkisofs.py +++ b/COT/helpers/mkisofs.py @@ -97,18 +97,24 @@ def install_helper(self): "See http://cdrecord.org/") logger.info("Successfully installed '%s'", self.name) - def create_iso(self, file_path, contents): + def create_iso(self, file_path, contents, rock_ridge=True): """Create a new ISO image at the requested location. :param str file_path: Desired location of new disk image :param list contents: List of file paths to package into the created image. + :param bool rock_ridge: Set to False to skip inclusion of Rock Ridge + extensions in the ISO. """ logger.info("Calling %s to create an ISO image", self.name) # mkisofs and genisoimage take the same parameters, conveniently, # while xorriso needs to be asked to pretend to be mkisofs - args = ['-output', file_path, '-full-iso9660-filenames', - '-iso-level', '2'] + contents + args = [] if self.name == 'xorriso': - args = ['-as', 'mkisofs'] + args + args += ['-as', 'mkisofs'] + args += ['-output', file_path, '-full-iso9660-filenames', + '-iso-level', '2', '-allow-lowercase'] + if rock_ridge: + args.append('-r') + args += contents self.call_helper(args) diff --git a/COT/helpers/tests/test_mkisofs.py b/COT/helpers/tests/test_mkisofs.py index ce84e79..faaab90 100644 --- a/COT/helpers/tests/test_mkisofs.py +++ b/COT/helpers/tests/test_mkisofs.py @@ -20,10 +20,12 @@ import subprocess from distutils.version import StrictVersion +import os import mock from COT.helpers.tests.test_helper import HelperUT from COT.helpers.mkisofs import MkIsoFS +from COT.helpers.isoinfo import IsoInfo class TestMkIsoFS(HelperUT): @@ -84,7 +86,7 @@ def find_one(name): self.helper.create_iso('foo.iso', [self.input_ovf]) mock_call_helper.assert_called_with( ['-output', 'foo.iso', '-full-iso9660-filenames', - '-iso-level', '2', self.input_ovf]) + '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) @mock.patch('distutils.spawn.find_executable') @mock.patch("COT.helpers.mkisofs.MkIsoFS.call_helper") @@ -102,7 +104,7 @@ def find_one(name): self.helper.create_iso('foo.iso', [self.input_ovf]) mock_call_helper.assert_called_with( ['-output', 'foo.iso', '-full-iso9660-filenames', - '-iso-level', '2', self.input_ovf]) + '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) @mock.patch('distutils.spawn.find_executable') @mock.patch("COT.helpers.mkisofs.MkIsoFS.call_helper") @@ -120,7 +122,7 @@ def find_one(name): self.helper.create_iso('foo.iso', [self.input_ovf]) mock_call_helper.assert_called_with( ['-as', 'mkisofs', '-output', 'foo.iso', '-full-iso9660-filenames', - '-iso-level', '2', self.input_ovf]) + '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) @mock.patch('COT.helpers.helper.Helper._check_output') @mock.patch('subprocess.check_call') @@ -172,3 +174,13 @@ def test_install_helper_apt_get_xorriso(self, ['sudo', 'apt-get', '-q', 'install', 'genisoimage'], ['apt-get', '-q', 'install', 'xorriso']]) self.assertEqual(self.helper.name, 'xorriso') + + def test_create_iso_non_rockridge(self): + """Create a non-Rock-Ridge ISO.""" + dest_file = os.path.join(self.temp_dir, "test.iso") + self.helper.create_iso(dest_file, [self.input_ovf], rock_ridge=False) + (file_format, subformat) = IsoInfo().get_disk_format(dest_file) + self.assertEqual(file_format, "iso") + self.assertEqual(subformat, None) + self.assertEqual(IsoInfo().get_disk_file_listing(dest_file), + [os.path.basename(self.input_ovf)]) diff --git a/COT/inject_config.py b/COT/inject_config.py index 40df48c..8dba6fc 100644 --- a/COT/inject_config.py +++ b/COT/inject_config.py @@ -38,7 +38,8 @@ class COTInjectConfig(COTSubmodule): Attributes: :attr:`config_file`, - :attr:`secondary_config_file` + :attr:`secondary_config_file`, + :attr:`extra_files` """ def __init__(self, ui): @@ -46,6 +47,7 @@ def __init__(self, ui): super(COTInjectConfig, self).__init__(ui) self._config_file = None self._secondary_config_file = None + self._extra_files = [] @property def config_file(self): @@ -59,14 +61,15 @@ def config_file(self): @config_file.setter def config_file(self, value): - value = str(value) - if not os.path.exists(value): - raise InvalidInputError("Primary config file {0} does not exist!" - .format(value)) - if not self.vm.platform.CONFIG_TEXT_FILE: - raise InvalidInputError( - "Configuration file not supported for platform {0}" - .format(self.vm.platform.__name__)) + if value is not None: + value = str(value) + if not os.path.exists(value): + raise InvalidInputError("Primary config file {0} not found!" + .format(value)) + if not self.vm.platform.CONFIG_TEXT_FILE: + raise InvalidInputError( + "Configuration file not supported for platform {0}" + .format(self.vm.platform.__name__)) self._config_file = value @property @@ -81,30 +84,41 @@ def secondary_config_file(self): @secondary_config_file.setter def secondary_config_file(self, value): - value = str(value) - if not os.path.exists(value): - raise InvalidInputError("Secondary config file {0} does not exist!" - .format(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__)) + if value is not None: + value = str(value) + if not os.path.exists(value): + raise InvalidInputError("Secondary config file {0} not found!" + .format(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__)) self._secondary_config_file = value + @property + def extra_files(self): + """Additional files to be embedded as-is. + + :raise InvalidInputError: if any file in the list does not exist + """ + return self._extra_files + + @extra_files.setter + def extra_files(self, values): + for path in values: + if not os.path.exists(path): + raise InvalidInputError("File {0} not found!".format(path)) + self._extra_files = values + def ready_to_run(self): """Check whether the module is ready to :meth:`run`. :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ - # Need some work to do! - work_to_do = False - if self.config_file is not None: - work_to_do = True - elif self.secondary_config_file is not None: - work_to_do = True - - if not work_to_do: - return False, "No configuration files specified - nothing to do!" + if not (self.config_file or + self.secondary_config_file or + self.extra_files): + return False, "No files specified - nothing to do!" return super(COTInjectConfig, self).ready_to_run() def run(self): @@ -157,6 +171,9 @@ def run(self): shutil.copy(self.secondary_config_file, dest) config_files.append(dest) + # Extra files are packaged as-is + config_files += self.extra_files + # Package the config files into a disk image if platform.BOOTSTRAP_DISK_TYPE == 'cdrom': bootstrap_file = os.path.join(vm.working_dir, 'config.iso') @@ -191,23 +208,29 @@ def create_subparser(self): aliases=['add-bootstrap'], help="Inject a configuration file into an OVF package", usage=self.UI.fill_usage("inject-config", [ - "PACKAGE -c CONFIG_FILE [-o OUTPUT]", - "PACKAGE -s SECONDARY_CONFIG_FILE [-o OUTPUT]", - "PACKAGE -c CONFIG_FILE -s SECONDARY_CONFIG_FILE [-o OUTPUT]", + "PACKAGE [-o OUTPUT] [-c CONFIG_FILE] " + "[-s SECONDARY_CONFIG_FILE] [-e EXTRA_FILE [EXTRA_FILE2 ...]]", ]), - description="""Add one or more "bootstrap" configuration """ - """file(s) to the given OVF or OVA.""") + description=""" +Add one or more "bootstrap" configuration file(s) to the given OVF or OVA. +These files will be packaged into a virtual hard disk, or virtual CD-ROM, +as appropriate to the target platform. Any specified primary and secondary +config files will be renamed if necessary to meet expectations of the target +platform, while any files provided with the --extra-files option will +be included as-is and will not be renamed.""") p.add_argument('-o', '--output', - help="""Name/path of new VM package to create """ - """instead of updating the existing package""") + help="Name/path of new VM package to create " + "instead of updating the existing package") p.add_argument('-c', '--config-file', - help="""Primary configuration text file to embed""") + help="Text file to embed as primary configuration") p.add_argument('-s', '--secondary-config-file', - help="""Secondary configuration text file to embed """ - """(currently only supported in IOS XRv for """ - """admin config)""") + help="Text file to embed as secondary configuration" + " (currently only used for IOS XR admin config)") + p.add_argument('-e', '--extra-files', nargs='+', + metavar=('EXTRA_FILE', 'EXTRA_FILE2'), + help="Additional file(s) to include as-is") p.add_argument('PACKAGE', - help="""Package, OVF descriptor or OVA file to edit""") + help="Package, OVF descriptor or OVA file to edit") p.set_defaults(instance=self) diff --git a/COT/tests/test_inject_config.py b/COT/tests/test_inject_config.py index 6b05f18..20f55a3 100644 --- a/COT/tests/test_inject_config.py +++ b/COT/tests/test_inject_config.py @@ -47,15 +47,35 @@ def setUp(self): def test_readiness(self): """Test ready_to_run() under various combinations of parameters.""" self.instance.package = self.input_ovf + # IOSXRv is the only platform that supports both primary and secondary + # config, so fake out our platform type appropriately. + self.set_vm_platform(IOSXRv) + ready, reason = self.instance.ready_to_run() self.assertFalse(ready) - self.assertTrue(re.search("No configuration files", reason)) + self.assertTrue(re.search("No files specified", reason)) self.assertRaises(InvalidInputError, self.instance.run) self.instance.config_file = self.config_file ready, reason = self.instance.ready_to_run() self.assertTrue(ready) + self.instance.config_file = None + ready, reason = self.instance.ready_to_run() + self.assertFalse(ready) + + self.instance.secondary_config_file = self.config_file + ready, reason = self.instance.ready_to_run() + self.assertTrue(ready) + + self.instance.secondary_config_file = None + ready, reason = self.instance.ready_to_run() + self.assertFalse(ready) + + self.instance.extra_files = [self.config_file] + ready, reason = self.instance.ready_to_run() + self.assertTrue(ready) + def test_invalid_always_args(self): """Test input values that are always invalid.""" self.instance.package = self.input_ovf @@ -63,6 +83,8 @@ def test_invalid_always_args(self): self.instance.config_file = 0 with self.assertRaises(InvalidInputError): self.instance.secondary_config_file = 0 + with self.assertRaises(InvalidInputError): + self.instance.extra_files = [self.input_ovf, '/foo/bar'] def test_valid_by_platform(self): """Test input values whose validity depends on the platform.""" @@ -257,3 +279,49 @@ def test_find_parent_fail_no_parent(self): self.instance.vm.find_device_location, cpu_item) self.assertLogged(levelname="ERROR", msg="Item has no .*Parent element") + + def test_inject_config_primary_secondary_extra(self): + """Test injection of primary and secondary files and extras.""" + self.instance.package = self.input_ovf + # IOSXRv supports secondary config + self.set_vm_platform(IOSXRv) + self.instance.config_file = self.config_file + self.instance.secondary_config_file = self.config_file + self.instance.extra_files = [self.minimal_ovf, self.vmware_ovf] + self.instance.run() + self.assertLogged(**self.OVERWRITING_DISK_ITEM) + self.instance.finished() + self.assertLogged(**self.invalid_hardware_warning( + '4CPU-4GB-3NIC', 'VMXNET3', 'NIC type')) + self.assertLogged(**self.invalid_hardware_warning( + '1CPU-1GB-1NIC', 'VMXNET3', 'NIC type')) + self.assertLogged(**self.invalid_hardware_warning( + '1CPU-1GB-1NIC', '1024 MiB', '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')) + config_iso = os.path.join(self.temp_dir, 'config.iso') + self.check_diff(""" + ++ + +... + false ++ Configuration disk + CD-ROM 2 ++ ovf:/file/config.iso + 8""" + .format(cfg_size=self.FILE_SIZE['sample_cfg.txt'], + config_size=os.path.getsize(config_iso))) + self.assertEqual( + get_disk_file_listing(config_iso), + [ + "iosxr_config.txt", + "iosxr_config_admin.txt", + "minimal.ovf", + "vmware.ovf", + ] + ) diff --git a/docs/COT.helpers.isoinfo.rst b/docs/COT.helpers.isoinfo.rst new file mode 100644 index 0000000..ea8bf09 --- /dev/null +++ b/docs/COT.helpers.isoinfo.rst @@ -0,0 +1,4 @@ +``COT.helpers.isoinfo`` module +============================== + +.. automodule:: COT.helpers.isoinfo From da01e465aaeb650b399b04d518510758c2805ad6 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 4 Oct 2016 16:03:53 -0400 Subject: [PATCH 19/59] Update changelog - oops --- CHANGELOG.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 238676d..7289036 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,31 @@ Change Log All notable changes to the COT project will be documented in this file. This project adheres to `Semantic Versioning`_. +`Unreleased`_ +------------- + +**Added** + +- ``cot inject-config --extra-files`` parameter (`#53`_). +- :func:`COT.helpers.get_disk_file_listing`, currently used only in UT scripts. +- Helper class for ``isoinfo`` (a companion to ``mkisofs``). + +**Changed** + +- Refactored the monolithic ``COT/platforms.py`` file into a proper submodule. +- :func:`~COT.helpers.mkisofs.MkIsoFs.create_iso` now adds Rock Ridge extensions + by default. + +**Removed** + +- :func:`get_checksum` is no longer part of the ``COT.helpers`` public API. + (It's now the method :func:`~COT.data_validation.file_checksum` in + ``COT.data_validation``, where it really belonged from the start). +- :func:`download_and_expand` is no longer part of the ``COT.helpers`` public + API. (It's now the static method + :func:`~COT.helpers.helper.Helper.download_and_expand_tgz` + on class :class:`~COT.helpers.helper.Helper`.) + `1.7.4`_ - 2016-09-21 --------------------- @@ -519,6 +544,7 @@ Initial public release. .. _#50: https://github.com/glennmatthews/cot/issues/50 .. _#51: https://github.com/glennmatthews/cot/issues/51 .. _#52: https://github.com/glennmatthews/cot/issues/52 +.. _#53: https://github.com/glennmatthews/cot/issues/53 .. _Semantic Versioning: http://semver.org/ .. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/ From 38317598c1abe62513268668baef34862378c389 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 4 Oct 2016 16:24:12 -0400 Subject: [PATCH 20/59] Split tests into individual files --- COT/platforms/tests/test_cisco_csr1000v.py | 91 ++++ COT/platforms/tests/test_cisco_iosv.py | 93 ++++ COT/platforms/tests/test_cisco_iosxrv.py | 136 ++++++ COT/platforms/tests/test_cisco_iosxrv_9000.py | 81 ++++ COT/platforms/tests/test_cisco_nxosv.py | 84 ++++ COT/platforms/tests/test_generic.py | 57 +++ COT/platforms/tests/test_platforms.py | 426 ------------------ 7 files changed, 542 insertions(+), 426 deletions(-) create mode 100644 COT/platforms/tests/test_cisco_csr1000v.py create mode 100644 COT/platforms/tests/test_cisco_iosv.py create mode 100644 COT/platforms/tests/test_cisco_iosxrv.py create mode 100644 COT/platforms/tests/test_cisco_iosxrv_9000.py create mode 100644 COT/platforms/tests/test_cisco_nxosv.py create mode 100644 COT/platforms/tests/test_generic.py delete mode 100644 COT/platforms/tests/test_platforms.py diff --git a/COT/platforms/tests/test_cisco_csr1000v.py b/COT/platforms/tests/test_cisco_csr1000v.py new file mode 100644 index 0000000..e6048f9 --- /dev/null +++ b/COT/platforms/tests/test_cisco_csr1000v.py @@ -0,0 +1,91 @@ +# test_cisco_csr1000v.py - Unit test cases for Cisco CSR1000V 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 CSR1000V platform.""" + +import unittest +from COT.platforms.cisco_csr1000v import CSR1000V +from COT.data_validation import ( + ValueUnsupportedError, ValueTooLowError, ValueTooHighError +) + + +class TestCSR1000V(unittest.TestCase): + """Test cases for Cisco CSR 1000V platform handling.""" + + cls = CSR1000V + + def test_controller_type_for_device(self): + """Test platform-specific logic for device controllers.""" + self.assertEqual(self.cls.controller_type_for_device('harddisk'), + 'scsi') + self.assertEqual(self.cls.controller_type_for_device('cdrom'), + 'ide') + # fallthrough to parent class + self.assertEqual(self.cls.controller_type_for_device('dvd'), + 'ide') + + def test_nic_name(self): + """Test NIC name construction.""" + self.assertEqual(self.cls.guess_nic_name(1), + "GigabitEthernet1") + self.assertEqual(self.cls.guess_nic_name(2), + "GigabitEthernet2") + self.assertEqual(self.cls.guess_nic_name(3), + "GigabitEthernet3") + self.assertEqual(self.cls.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(ValueUnsupportedError, + self.cls.validate_cpu_count, 3) + self.cls.validate_cpu_count(4) + self.assertRaises(ValueTooHighError, self.cls.validate_cpu_count, 5) + + 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.assertRaises(ValueTooHighError, + self.cls.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) + + 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.assertRaises(ValueUnsupportedError, + self.cls.validate_nic_type, "PCNet32") + self.cls.validate_nic_type("virtio") + self.cls.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) diff --git a/COT/platforms/tests/test_cisco_iosv.py b/COT/platforms/tests/test_cisco_iosv.py new file mode 100644 index 0000000..31d9a52 --- /dev/null +++ b/COT/platforms/tests/test_cisco_iosv.py @@ -0,0 +1,93 @@ +# test_cisco_iosv.py - Unit test cases for Cisco IOSv 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 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.""" + pass + +from COT.platforms.cisco_iosv import IOSv +from COT.data_validation import ( + ValueUnsupportedError, ValueTooLowError, ValueTooHighError +) + +logging.getLogger('COT').addHandler(NullHandler()) + + +class TestIOSv(unittest.TestCase): + """Test cases for Cisco IOSv platform handling.""" + + cls = IOSv + + def test_nic_name(self): + """Test NIC name construction.""" + self.assertEqual(self.cls.guess_nic_name(1), + "GigabitEthernet0/0") + self.assertEqual(self.cls.guess_nic_name(2), + "GigabitEthernet0/1") + self.assertEqual(self.cls.guess_nic_name(3), + "GigabitEthernet0/2") + self.assertEqual(self.cls.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) + + 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.assertRaises(ValueTooHighError, + self.cls.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) + + 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.assertRaises(ValueUnsupportedError, + self.cls.validate_nic_type, "PCNet32") + self.assertRaises(ValueUnsupportedError, + self.cls.validate_nic_type, "virtio") + self.assertRaises(ValueUnsupportedError, + self.cls.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) diff --git a/COT/platforms/tests/test_cisco_iosxrv.py b/COT/platforms/tests/test_cisco_iosxrv.py new file mode 100644 index 0000000..bc04559 --- /dev/null +++ b/COT/platforms/tests/test_cisco_iosxrv.py @@ -0,0 +1,136 @@ +# 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. +# 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 IOSXRv class and its subclasses.""" + +import unittest +from COT.platforms.cisco_iosxrv import IOSXRv, IOSXRvRP, IOSXRvLC +from COT.data_validation import ( + ValueUnsupportedError, ValueTooLowError, ValueTooHighError +) + + +class TestIOSXRv(unittest.TestCase): + """Test cases for Cisco IOS XRv platform handling.""" + + cls = IOSXRv + + def test_nic_name(self): + """Test NIC name construction.""" + self.assertEqual(self.cls.guess_nic_name(1), + "MgmtEth0/0/CPU0/0") + self.assertEqual(self.cls.guess_nic_name(2), + "GigabitEthernet0/0/0/0") + self.assertEqual(self.cls.guess_nic_name(3), + "GigabitEthernet0/0/0/1") + self.assertEqual(self.cls.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) + + 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.assertRaises(ValueTooHighError, + self.cls.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) + # 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.assertRaises(ValueUnsupportedError, + self.cls.validate_nic_type, "PCNet32") + self.cls.validate_nic_type("virtio") + self.assertRaises(ValueUnsupportedError, + self.cls.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) + + +class TestIOSXRvRP(TestIOSXRv): + """Test cases for Cisco IOS XRv HA-capable RP platform handling.""" + + cls = IOSXRvRP + + # Inherit all test cases from IOSXRv class, except where overridden below: + + def test_nic_name(self): + """Test NIC name construction. + + 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), + "fabric") + self.assertEqual(self.cls.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) + + +class TestIOSXRvLC(TestIOSXRv): + """Test cases for Cisco IOS XRv line card platform handling.""" + + cls = IOSXRvLC + + # Inherit all test cases from IOSXRv class, except where overridden below: + + def test_nic_name(self): + """Test NIC name construction. + + An LC has a fabric but no MgmtEth. + """ + self.assertEqual(self.cls.guess_nic_name(1), + "fabric") + self.assertEqual(self.cls.guess_nic_name(2), + "GigabitEthernet0/{SLOT}/0/0") + self.assertEqual(self.cls.guess_nic_name(3), + "GigabitEthernet0/{SLOT}/0/1") + self.assertEqual(self.cls.guess_nic_name(4), + "GigabitEthernet0/{SLOT}/0/2") + + def test_serial_count(self): + """Test serial port range limits. + + 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) diff --git a/COT/platforms/tests/test_cisco_iosxrv_9000.py b/COT/platforms/tests/test_cisco_iosxrv_9000.py new file mode 100644 index 0000000..bf2731c --- /dev/null +++ b/COT/platforms/tests/test_cisco_iosxrv_9000.py @@ -0,0 +1,81 @@ +# test_cisco_iosxrv_9000.py - Unit test cases for Cisco IOS XRv9k 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 IOSXRv9000 class.""" + +import unittest +from COT.platforms.cisco_iosxrv_9000 import IOSXRv9000 +from COT.data_validation import ( + ValueUnsupportedError, ValueTooLowError, ValueTooHighError +) + + +class TestIOSXRv9000(unittest.TestCase): + """Test cases for Cisco IOS XRv 9000 platform handling.""" + + cls = IOSXRv9000 + + def test_nic_name(self): + """Test NIC name construction.""" + self.assertEqual(self.cls.guess_nic_name(1), + "MgmtEth0/0/CPU0/0") + self.assertEqual(self.cls.guess_nic_name(2), + "CtrlEth") + self.assertEqual(self.cls.guess_nic_name(3), + "DevEth") + self.assertEqual(self.cls.guess_nic_name(4), + "GigabitEthernet0/0/0/0") + self.assertEqual(self.cls.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) + + 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.assertRaises(ValueTooHighError, + self.cls.validate_memory_amount, 32769) + + 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) + # 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.assertRaises(ValueUnsupportedError, + self.cls.validate_nic_type, "PCNet32") + self.cls.validate_nic_type("virtio") + self.cls.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) diff --git a/COT/platforms/tests/test_cisco_nxosv.py b/COT/platforms/tests/test_cisco_nxosv.py new file mode 100644 index 0000000..e252698 --- /dev/null +++ b/COT/platforms/tests/test_cisco_nxosv.py @@ -0,0 +1,84 @@ +# test_cisco_nxosv.py - Unit test cases for Cisco NXOSv 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 NXOSv platform.""" + +import unittest +from COT.platforms.cisco_nxosv import NXOSv +from COT.data_validation import ( + ValueUnsupportedError, ValueTooLowError, ValueTooHighError +) + + +class TestNXOSv(unittest.TestCase): + """Test cases for Cisco NX-OSv platform handling.""" + + cls = NXOSv + + def test_nic_name(self): + """Test NIC name construction.""" + self.assertEqual(self.cls.guess_nic_name(1), + "mgmt0") + self.assertEqual(self.cls.guess_nic_name(2), + "Ethernet2/1") + self.assertEqual(self.cls.guess_nic_name(3), + "Ethernet2/2") + self.assertEqual(self.cls.guess_nic_name(4), + "Ethernet2/3") + # ... + self.assertEqual(self.cls.guess_nic_name(49), + "Ethernet2/48") + self.assertEqual(self.cls.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) + + 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.assertRaises(ValueTooHighError, + self.cls.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) + # 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.assertRaises(ValueUnsupportedError, + self.cls.validate_nic_type, "PCNet32") + self.cls.validate_nic_type("virtio") + self.assertRaises(ValueUnsupportedError, + self.cls.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) diff --git a/COT/platforms/tests/test_generic.py b/COT/platforms/tests/test_generic.py new file mode 100644 index 0000000..e50cc17 --- /dev/null +++ b/COT/platforms/tests/test_generic.py @@ -0,0 +1,57 @@ +# 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_platforms.py b/COT/platforms/tests/test_platforms.py deleted file mode 100644 index d379138..0000000 --- a/COT/platforms/tests/test_platforms.py +++ /dev/null @@ -1,426 +0,0 @@ -# platforms.py - Unit test cases for COT platform handling -# -# January 2014, 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 platforms provided by COT.platforms.""" - -import unittest -from COT.platforms import GenericPlatform -from COT.platforms import CSR1000V, IOSv, NXOSv -from COT.platforms import IOSXRv, IOSXRvRP, IOSXRvLC, IOSXRv9000 -from COT.data_validation import ValueUnsupportedError -from COT.data_validation import ValueTooLowError, ValueTooHighError - - -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) - - -class TestIOSXRv(unittest.TestCase): - """Test cases for Cisco IOS XRv platform handling.""" - - cls = IOSXRv - - def test_nic_name(self): - """Test NIC name construction.""" - self.assertEqual(self.cls.guess_nic_name(1), - "MgmtEth0/0/CPU0/0") - self.assertEqual(self.cls.guess_nic_name(2), - "GigabitEthernet0/0/0/0") - self.assertEqual(self.cls.guess_nic_name(3), - "GigabitEthernet0/0/0/1") - self.assertEqual(self.cls.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) - - 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.assertRaises(ValueTooHighError, - self.cls.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) - # 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.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "PCNet32") - self.cls.validate_nic_type("virtio") - self.assertRaises(ValueUnsupportedError, - self.cls.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) - - -class TestIOSXRvRP(TestIOSXRv): - """Test cases for Cisco IOS XRv HA-capable RP platform handling.""" - - cls = IOSXRvRP - - # Inherit all test cases from IOSXRv class, except where overridden below: - - def test_nic_name(self): - """Test NIC name construction. - - 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), - "fabric") - self.assertEqual(self.cls.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) - - -class TestIOSXRvLC(TestIOSXRv): - """Test cases for Cisco IOS XRv line card platform handling.""" - - cls = IOSXRvLC - - # Inherit all test cases from IOSXRv class, except where overridden below: - - def test_nic_name(self): - """Test NIC name construction. - - An LC has a fabric but no MgmtEth. - """ - self.assertEqual(self.cls.guess_nic_name(1), - "fabric") - self.assertEqual(self.cls.guess_nic_name(2), - "GigabitEthernet0/{SLOT}/0/0") - self.assertEqual(self.cls.guess_nic_name(3), - "GigabitEthernet0/{SLOT}/0/1") - self.assertEqual(self.cls.guess_nic_name(4), - "GigabitEthernet0/{SLOT}/0/2") - - def test_serial_count(self): - """Test serial port range limits. - - 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) - - -class TestIOSXRv9000(unittest.TestCase): - """Test cases for Cisco IOS XRv 9000 platform handling.""" - - cls = IOSXRv9000 - - def test_nic_name(self): - """Test NIC name construction.""" - self.assertEqual(self.cls.guess_nic_name(1), - "MgmtEth0/0/CPU0/0") - self.assertEqual(self.cls.guess_nic_name(2), - "CtrlEth") - self.assertEqual(self.cls.guess_nic_name(3), - "DevEth") - self.assertEqual(self.cls.guess_nic_name(4), - "GigabitEthernet0/0/0/0") - self.assertEqual(self.cls.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) - - 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.assertRaises(ValueTooHighError, - self.cls.validate_memory_amount, 32769) - - 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) - # 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.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "PCNet32") - self.cls.validate_nic_type("virtio") - self.cls.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) - - -class TestCSR1000V(unittest.TestCase): - """Test cases for Cisco CSR 1000V platform handling.""" - - cls = CSR1000V - - def test_controller_type_for_device(self): - """Test platform-specific logic for device controllers.""" - self.assertEqual(self.cls.controller_type_for_device('harddisk'), - 'scsi') - self.assertEqual(self.cls.controller_type_for_device('cdrom'), - 'ide') - # fallthrough to parent class - self.assertEqual(self.cls.controller_type_for_device('dvd'), - 'ide') - - def test_nic_name(self): - """Test NIC name construction.""" - self.assertEqual(self.cls.guess_nic_name(1), - "GigabitEthernet1") - self.assertEqual(self.cls.guess_nic_name(2), - "GigabitEthernet2") - self.assertEqual(self.cls.guess_nic_name(3), - "GigabitEthernet3") - self.assertEqual(self.cls.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(ValueUnsupportedError, - self.cls.validate_cpu_count, 3) - self.cls.validate_cpu_count(4) - self.assertRaises(ValueTooHighError, self.cls.validate_cpu_count, 5) - - 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.assertRaises(ValueTooHighError, - self.cls.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) - - 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.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "PCNet32") - self.cls.validate_nic_type("virtio") - self.cls.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) - - -class TestIOSv(unittest.TestCase): - """Test cases for Cisco IOSv platform handling.""" - - cls = IOSv - - def test_nic_name(self): - """Test NIC name construction.""" - self.assertEqual(self.cls.guess_nic_name(1), - "GigabitEthernet0/0") - self.assertEqual(self.cls.guess_nic_name(2), - "GigabitEthernet0/1") - self.assertEqual(self.cls.guess_nic_name(3), - "GigabitEthernet0/2") - self.assertEqual(self.cls.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) - - 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.assertRaises(ValueTooHighError, - self.cls.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) - - 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.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "PCNet32") - self.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "virtio") - self.assertRaises(ValueUnsupportedError, - self.cls.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) - - -class TestNXOSv(unittest.TestCase): - """Test cases for Cisco NX-OSv platform handling.""" - - cls = NXOSv - - def test_nic_name(self): - """Test NIC name construction.""" - self.assertEqual(self.cls.guess_nic_name(1), - "mgmt0") - self.assertEqual(self.cls.guess_nic_name(2), - "Ethernet2/1") - self.assertEqual(self.cls.guess_nic_name(3), - "Ethernet2/2") - self.assertEqual(self.cls.guess_nic_name(4), - "Ethernet2/3") - # ... - self.assertEqual(self.cls.guess_nic_name(49), - "Ethernet2/48") - self.assertEqual(self.cls.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) - - 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.assertRaises(ValueTooHighError, - self.cls.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) - # 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.assertRaises(ValueUnsupportedError, - self.cls.validate_nic_type, "PCNet32") - self.cls.validate_nic_type("virtio") - self.assertRaises(ValueUnsupportedError, - self.cls.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) From f40cb526a5709ed5a92766393938c853cae45645 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 5 Oct 2016 10:10:39 -0400 Subject: [PATCH 21/59] Fail gracefully if isoinfo is not present (as on Travis-CI) --- COT/helpers/api.py | 25 ++++++++++++---- COT/helpers/isoinfo.py | 2 +- COT/helpers/tests/test_api.py | 30 ++++++++++++++------ COT/helpers/tests/test_mkisofs.py | 14 ++++++--- COT/tests/test_inject_config.py | 47 ++++++++++++++++++++----------- 5 files changed, 82 insertions(+), 36 deletions(-) diff --git a/COT/helpers/api.py b/COT/helpers/api.py index 2870e32..210f75b 100644 --- a/COT/helpers/api.py +++ b/COT/helpers/api.py @@ -42,7 +42,7 @@ from distutils.version import StrictVersion -from .helper import Helper, guess_file_format_from_path +from .helper import Helper, HelperError, guess_file_format_from_path from .fatdisk import FatDisk from .isoinfo import IsoInfo from .mkisofs import MkIsoFS @@ -72,11 +72,26 @@ def get_disk_format(file_path): * ``format`` may be "iso", "vmdk", "raw", or "qcow2" * ``subformat`` may be ``None``, or various strings for "vmdk" files. + + :raises HelperError: if the file doesn't exist or is otherwise unreadable """ - # isoinfo can identify ISO files, otherwise returning None - (file_format, subformat) = ISOINFO.get_disk_format(file_path) - if file_format: - return (file_format, subformat) + if not os.path.exists(file_path): + raise HelperError(2, "No such file or directory: '{0}'" + .format(file_path)) + # IsoInfo is not available if we only have xorriso (as seen in Travis-CI) + if ISOINFO.path: + # isoinfo can identify ISO files, otherwise returning None + (file_format, subformat) = ISOINFO.get_disk_format(file_path) + if file_format: + return (file_format, subformat) + else: + # Try to at least detect ISO files by file magic number + with open(file_path, 'rb') as f: + for offset in (0x8001, 0x8801, 0x9001): + f.seek(offset) + magic = f.read(5).decode('ascii', 'ignore') + if magic == "CD001": + return ("iso", None) # QEMUIMG can identify various disk image formats, but guesses 'raw' # for any arbitrary file it doesn't identify. Thus, 'raw' results should be diff --git a/COT/helpers/isoinfo.py b/COT/helpers/isoinfo.py index 34d85f6..5263d61 100644 --- a/COT/helpers/isoinfo.py +++ b/COT/helpers/isoinfo.py @@ -47,7 +47,7 @@ def __init__(self): super(IsoInfo, self).__init__("isoinfo", version_regexp=r"isoinfo ([0-9.]+)") - # No install support as this is provided by MkIsoFS class. + # No install support as this is provided by MkIsoFS class or not at all. def get_disk_format(self, file_path): """Get the major disk image format of the given file. diff --git a/COT/helpers/tests/test_api.py b/COT/helpers/tests/test_api.py index f059533..bdcafc7 100644 --- a/COT/helpers/tests/test_api.py +++ b/COT/helpers/tests/test_api.py @@ -28,6 +28,7 @@ get_disk_capacity, get_disk_file_listing, create_install_dir, install_file, HelperError, HelperNotFoundError, ) +from COT.helpers.api import ISOINFO logger = logging.getLogger(__name__) @@ -194,9 +195,12 @@ def test_create_iso_with_contents(self): create_disk_image(disk_path, contents=[self.input_ovf]) except HelperNotFoundError as e: self.fail(e.strerror) - # Check contents - self.assertEqual(get_disk_file_listing(disk_path), - [os.path.basename(self.input_ovf)]) + if ISOINFO.path: + # Check contents + self.assertEqual(get_disk_file_listing(disk_path), + [os.path.basename(self.input_ovf)]) + else: + logger.info("isoinfo not available, not checking disk contents") # Creation of empty disks is tested implicitly in other test classes # above - no need to repeat that here @@ -216,10 +220,15 @@ def test_create_raw_with_contents(self): self.assertEqual(capacity, "8388608") except HelperNotFoundError as e: self.fail(e.strerror) - self.assertEqual(get_disk_file_listing(disk_path), - [os.path.basename(self.input_ovf)]) - - # Again, but now force the disk size + if ISOINFO.path: + self.assertEqual(get_disk_file_listing(disk_path), + [os.path.basename(self.input_ovf)]) + else: + logger.info("isoinfo not available, not checking disk contents") + + def test_create_raw_with_contents_and_size(self): + """Creation of raw disk image of a specified size with files.""" + disk_path = os.path.join(self.temp_dir, "out.img") try: create_disk_image(disk_path, contents=[self.input_ovf], capacity="64M") @@ -233,8 +242,11 @@ def test_create_raw_with_contents(self): self.assertEqual(capacity, "67108864") except HelperNotFoundError as e: self.fail(e.strerror) - self.assertEqual(get_disk_file_listing(disk_path), - [os.path.basename(self.input_ovf)]) + if ISOINFO.path: + self.assertEqual(get_disk_file_listing(disk_path), + [os.path.basename(self.input_ovf)]) + else: + logger.info("isoinfo not available, not checking disk contents") @mock.patch('COT.helpers.helper.Helper._check_call') diff --git a/COT/helpers/tests/test_mkisofs.py b/COT/helpers/tests/test_mkisofs.py index faaab90..7e60264 100644 --- a/COT/helpers/tests/test_mkisofs.py +++ b/COT/helpers/tests/test_mkisofs.py @@ -17,6 +17,7 @@ """Unit test cases for the COT.helpers.mkisofs submodule.""" +import logging import subprocess from distutils.version import StrictVersion @@ -25,7 +26,9 @@ from COT.helpers.tests.test_helper import HelperUT from COT.helpers.mkisofs import MkIsoFS -from COT.helpers.isoinfo import IsoInfo +from COT.helpers.api import get_disk_format, get_disk_file_listing, ISOINFO + +logger = logging.getLogger(__name__) class TestMkIsoFS(HelperUT): @@ -179,8 +182,11 @@ def test_create_iso_non_rockridge(self): """Create a non-Rock-Ridge ISO.""" dest_file = os.path.join(self.temp_dir, "test.iso") self.helper.create_iso(dest_file, [self.input_ovf], rock_ridge=False) - (file_format, subformat) = IsoInfo().get_disk_format(dest_file) + (file_format, subformat) = get_disk_format(dest_file) self.assertEqual(file_format, "iso") self.assertEqual(subformat, None) - self.assertEqual(IsoInfo().get_disk_file_listing(dest_file), - [os.path.basename(self.input_ovf)]) + if ISOINFO.path: + self.assertEqual(get_disk_file_listing(dest_file), + [os.path.basename(self.input_ovf)]) + else: + logger.info("isoinfo not available, not checking disk contents") diff --git a/COT/tests/test_inject_config.py b/COT/tests/test_inject_config.py index 20f55a3..6c15647 100644 --- a/COT/tests/test_inject_config.py +++ b/COT/tests/test_inject_config.py @@ -16,6 +16,7 @@ """Unit test cases for the COT.inject_config.COTInjectConfig class.""" +import logging import os.path import re from pkg_resources import resource_filename @@ -26,6 +27,9 @@ from COT.data_validation import InvalidInputError from COT.platforms import CSR1000V, IOSv, IOSXRv, IOSXRvLC from COT.helpers import get_disk_file_listing +from COT.helpers.api import ISOINFO + +logger = logging.getLogger(__name__) class TestCOTInjectConfig(COT_UT): @@ -127,10 +131,13 @@ def test_inject_config_iso(self): 8""" .format(cfg_size=self.FILE_SIZE['sample_cfg.txt'], config_size=os.path.getsize(config_iso))) - # The sample_cfg.text should be renamed to the platform-specific - # file name for bootstrap config - in this case, config.txt - self.assertEqual(get_disk_file_listing(config_iso), - ["config.txt"]) + if ISOINFO.path: + # The sample_cfg.text should be renamed to the platform-specific + # file name for bootstrap config - in this case, config.txt + self.assertEqual(get_disk_file_listing(config_iso), + ["config.txt"]) + else: + logger.info("isoinfo not available, not checking disk contents") def test_inject_config_iso_secondary(self): """Inject secondary config file on an ISO.""" @@ -165,10 +172,13 @@ def test_inject_config_iso_secondary(self): 8""" .format(cfg_size=self.FILE_SIZE['sample_cfg.txt'], config_size=os.path.getsize(config_iso))) - # The sample_cfg.text should be renamed to the platform-specific - # file name for secondary bootstrap config - self.assertEqual(get_disk_file_listing(config_iso), - ["iosxr_config_admin.txt"]) + if ISOINFO.path: + # The sample_cfg.text should be renamed to the platform-specific + # file name for secondary bootstrap config + self.assertEqual(get_disk_file_listing(config_iso), + ["iosxr_config_admin.txt"]) + else: + logger.info("isoinfo not available, not checking disk contents") def test_inject_config_vmdk(self): """Inject config file on a VMDK.""" @@ -316,12 +326,15 @@ def test_inject_config_primary_secondary_extra(self): 8""" .format(cfg_size=self.FILE_SIZE['sample_cfg.txt'], config_size=os.path.getsize(config_iso))) - self.assertEqual( - get_disk_file_listing(config_iso), - [ - "iosxr_config.txt", - "iosxr_config_admin.txt", - "minimal.ovf", - "vmware.ovf", - ] - ) + if ISOINFO.path: + self.assertEqual( + get_disk_file_listing(config_iso), + [ + "iosxr_config.txt", + "iosxr_config_admin.txt", + "minimal.ovf", + "vmware.ovf", + ] + ) + else: + logger.info("isoinfo not available, not checking disk contents") From ec963b19134be478ef88537ea83a0db55939be39 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 14 Oct 2016 16:52:30 -0400 Subject: [PATCH 22/59] Refactor COT.helpers submodule and create COT.disks submodule. Most disk file handling is now done in COT.disks with help from COT.helpers, instead of COT.helpers doing double-duty. --- .coveragerc | 1 + .pylintrc | 2 +- COT/add_disk.py | 24 +- COT/cli.py | 4 +- COT/deploy_esxi.py | 10 +- COT/disks/__init__.py | 107 ++++ COT/disks/disk.py | 122 ++++ COT/disks/iso.py | 129 ++++ COT/disks/qcow2.py | 42 ++ COT/disks/raw.py | 106 ++++ COT/disks/tests/__init__.py | 13 + COT/disks/tests/test_api.py | 79 +++ COT/disks/tests/test_disk_representation.py | 65 ++ COT/disks/tests/test_iso.py | 132 ++++ COT/disks/tests/test_qcow2.py | 36 ++ COT/disks/tests/test_raw.py | 61 ++ COT/disks/tests/test_vmdk.py | 87 +++ COT/disks/vmdk.py | 108 ++++ COT/helpers/__init__.py | 75 ++- COT/helpers/api.py | 276 --------- COT/helpers/apt_get.py | 52 ++ COT/helpers/fatdisk.py | 140 +---- COT/helpers/gcc.py | 32 + COT/helpers/helper.py | 637 ++++++++++---------- COT/helpers/isoinfo.py | 83 +-- COT/helpers/make.py | 32 + COT/helpers/mkisofs.py | 132 ++-- COT/helpers/ovftool.py | 54 +- COT/helpers/port.py | 49 ++ COT/helpers/qemu_img.py | 153 +---- COT/helpers/tests/test_api.py | 334 ---------- COT/helpers/tests/test_fatdisk.py | 108 ++-- COT/helpers/tests/test_genisoimage.py | 58 ++ COT/helpers/tests/test_helper.py | 239 +++++--- COT/helpers/tests/test_mkisofs.py | 159 +---- COT/helpers/tests/test_ovftool.py | 27 +- COT/helpers/tests/test_qemu_img.py | 96 +-- COT/helpers/tests/test_vmdktool.py | 89 ++- COT/helpers/tests/test_xorriso.py | 73 +++ COT/helpers/vmdktool.py | 108 +--- COT/helpers/yum.py | 39 ++ COT/inject_config.py | 14 +- COT/install_helpers.py | 29 +- COT/ovf/ovf.py | 37 +- COT/ovf/tests/test_ovf.py | 12 +- COT/tests/test_add_disk.py | 16 +- COT/tests/test_inject_config.py | 18 +- COT/tests/test_install_helpers.py | 67 +- COT/tests/ut.py | 15 +- COT/ui_shared.py | 4 +- COT/vm_description.py | 17 +- docs/COT.disks.iso.rst | 4 + docs/COT.disks.qcow2.rst | 4 + docs/COT.disks.raw.rst | 4 + docs/COT.disks.rst | 5 + docs/COT.disks.vmdk.rst | 4 + docs/COT.helpers.api.rst | 5 - docs/COT.helpers.apt_get.rst | 4 + docs/COT.helpers.gcc.rst | 4 + docs/COT.helpers.make.rst | 4 + docs/COT.helpers.port.rst | 4 + docs/COT.helpers.yum.rst | 4 + docs/index.rst | 1 + setup.py | 2 +- 64 files changed, 2434 insertions(+), 2017 deletions(-) create mode 100644 COT/disks/__init__.py create mode 100644 COT/disks/disk.py create mode 100644 COT/disks/iso.py create mode 100644 COT/disks/qcow2.py create mode 100644 COT/disks/raw.py create mode 100644 COT/disks/tests/__init__.py create mode 100644 COT/disks/tests/test_api.py create mode 100644 COT/disks/tests/test_disk_representation.py create mode 100644 COT/disks/tests/test_iso.py create mode 100644 COT/disks/tests/test_qcow2.py create mode 100644 COT/disks/tests/test_raw.py create mode 100644 COT/disks/tests/test_vmdk.py create mode 100644 COT/disks/vmdk.py delete mode 100644 COT/helpers/api.py create mode 100644 COT/helpers/apt_get.py create mode 100644 COT/helpers/gcc.py create mode 100644 COT/helpers/make.py create mode 100644 COT/helpers/port.py delete mode 100644 COT/helpers/tests/test_api.py create mode 100644 COT/helpers/tests/test_genisoimage.py create mode 100644 COT/helpers/tests/test_xorriso.py create mode 100644 COT/helpers/yum.py create mode 100644 docs/COT.disks.iso.rst create mode 100644 docs/COT.disks.qcow2.rst create mode 100644 docs/COT.disks.raw.rst create mode 100644 docs/COT.disks.rst create mode 100644 docs/COT.disks.vmdk.rst delete mode 100644 docs/COT.helpers.api.rst create mode 100644 docs/COT.helpers.apt_get.rst create mode 100644 docs/COT.helpers.gcc.rst create mode 100644 docs/COT.helpers.make.rst create mode 100644 docs/COT.helpers.port.rst create mode 100644 docs/COT.helpers.yum.rst diff --git a/.coveragerc b/.coveragerc index 5626556..de8b4f3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,6 +3,7 @@ branch = True omit = COT/tests/* + COT/disks/tests/* COT/helpers/tests/* COT/ovf/tests/* COT/platforms/tests/* diff --git a/.pylintrc b/.pylintrc index 2372bbc..74264cb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -65,7 +65,7 @@ max-public-methods=75 # default: max-module-lines: 1000 # current worst offender: OVF -max-module-lines=2700 +max-module-lines=2750 [LOGGING] diff --git a/COT/add_disk.py b/COT/add_disk.py index c0bc4e6..4fe5a3f 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -40,6 +40,7 @@ import logging import os.path +from COT.disks import disk_representation_from_file from .data_validation import InvalidInputError, ValueUnsupportedError from .data_validation import check_for_conflict, device_address, match_or_die from .submodule import COTSubmodule @@ -110,7 +111,7 @@ def __init__(self, ui): @property def disk_image(self): - """Path to disk image file to add to the VM. + """Disk image file to add to the VM. :raises: :exc:`.InvalidInputError` if the file does not exist. """ @@ -118,10 +119,7 @@ def disk_image(self): @disk_image.setter def disk_image(self, value): - if not os.path.exists(value): - raise InvalidInputError("Specified disk '{0}' does not exist!" - .format(value)) - self._disk_image = value + self._disk_image = disk_representation_from_file(value) @property def address(self): @@ -453,7 +451,11 @@ def add_disk_worker(vm, :param ui: User interface in effect. :type ui: instance of :class:`~COT.ui_shared.UI` or subclass. - :param str disk_image: path to disk image to add to the VM. + + :param disk_image: Disk image to add to the VM. + :type disk_image: instance of :class:`~COT.disks.DiskRepresentation` or + subclass. + :param str disk_type: Disk type: ``'cdrom'`` or ``'harddisk'``. If not specified, will be derived automatically from the disk_image file name extension. @@ -475,15 +477,17 @@ def add_disk_worker(vm, :param str diskname: Name for disk device :param str description: Description of disk device """ + disk_path = disk_image.path if disk_type is None: - disk_type = guess_disk_type_from_extension(disk_image) + disk_type = guess_disk_type_from_extension(disk_path) logger.warning("New disk type not specified, guessing it should " "be '%s' based on file extension", disk_type) # Convert the disk to a new format if needed... disk_image = vm.convert_disk_if_needed(disk_image, disk_type) + disk_path = disk_image.path - disk_file = os.path.basename(disk_image) + disk_file = os.path.basename(disk_path) (file_obj, disk, ctrl_item, disk_item) = \ search_for_elements(vm, disk_file, file_id, controller, address) @@ -500,7 +504,7 @@ def add_disk_worker(vm, validate_elements(vm, file_obj, disk, disk_item, ctrl_item, file_id, controller) - confirm_elements(vm, ui, file_obj, disk_image, disk, disk_item, disk_type, + confirm_elements(vm, ui, file_obj, disk_path, disk, disk_item, disk_type, controller, ctrl_item, subtype) # OK - let's add things! @@ -512,7 +516,7 @@ def add_disk_worker(vm, file_id = disk_file # First, the File - file_obj = vm.add_file(disk_image, file_id, file_obj, disk) + file_obj = vm.add_file(disk_path, file_id, file_obj, disk) # Next, the Disk disk = vm.add_disk(disk_image, file_id, disk_type, disk) diff --git a/COT/cli.py b/COT/cli.py index 7ed8b13..95b0a29 100644 --- a/COT/cli.py +++ b/COT/cli.py @@ -138,8 +138,8 @@ def __init__(self, terminal_width=None): if _argcomplete: argcomplete.autocomplete(self.parser) - import COT.helpers.helper - COT.helpers.helper.confirm = self.confirm + from COT.helpers import Helper + Helper.UI = self @property def terminal_width(self): diff --git a/COT/deploy_esxi.py b/COT/deploy_esxi.py index da69c4c..a1ec10b 100644 --- a/COT/deploy_esxi.py +++ b/COT/deploy_esxi.py @@ -44,9 +44,9 @@ from pyVmomi import vim from pyVim.connect import SmartConnection -from .data_validation import ValueUnsupportedError -from .deploy import COTDeploy -from .helpers.ovftool import OVFTool +from COT.data_validation import ValueUnsupportedError +from COT.deploy import COTDeploy +from COT.helpers import helpers logger = logging.getLogger(__name__) @@ -212,7 +212,7 @@ def __init__(self, ui): self._locator = None self._ovftool_args = [] - self.ovftool = OVFTool() + self.ovftool = helpers['ovftool'] @property def ovftool_args(self): @@ -359,7 +359,7 @@ def run(self): ovftool_args = self.fixup_ovftool_args(ovftool_args, target) logger.info("Deploying VM...") - self.ovftool.call_helper(ovftool_args, capture_output=False) + self.ovftool.call(ovftool_args, capture_output=False) # VMWare has confirmed that they have no plan to implement serial port # orchestration in ovftool, so we have to do it ourselves now that diff --git a/COT/disks/__init__.py b/COT/disks/__init__.py new file mode 100644 index 0000000..cb42838 --- /dev/null +++ b/COT/disks/__init__.py @@ -0,0 +1,107 @@ +# October 2016, Glenn F. Matthews +# Copyright (c) 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. + +"""Package for handling various disk file types (VMDK, ISO, QCOW2, etc.). + +Tries to provide an API that abstracts away differences in how the +various types need to be operated on (e.g., qemu-img, mkisofs, etc.). + +API +--- + +.. autosummary:: + :nosignatures: + + convert_disk + create_disk + disk_representation_from_file + +Disk modules +------------ + +.. autosummary:: + :toctree: + + COT.disks.iso + COT.disks.qcow2 + COT.disks.raw + COT.disks.vmdk +""" + +import os + +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. + + :param disk_image: Existing disk image as input. + :type disk_image: :class:`~COT.disks.DiskRepresentation` or subclass. + :param str new_directory: Directory to create new image under + :param str new_format: Format to convert to. + :param str new_subformat: (optional) Sub-format to convert to. + :return: new instance of :class:`~COT.disks.DiskRepresentation` subclass. + """ + 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. + + :param str disk_format: Disk format such as 'iso' or 'vmdk'. + For the other parameters, see :class:`~COT.disks.DiskRepresentation`. + + :return: new instance of :class:`~COT.disks.DiskRepresentation` subclass. + """ + 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. + + :param str file_path: Path of existing file to represent. + + :return: new instance of :class:`~COT.disks.DiskRepresentation` subclass. + """ + 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', +) diff --git a/COT/disks/disk.py b/COT/disks/disk.py new file mode 100644 index 0000000..4eec61d --- /dev/null +++ b/COT/disks/disk.py @@ -0,0 +1,122 @@ +# October 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. + +"""Abstract base class for representations of disk image files.""" + +import logging +import os +import re + +from COT.helpers import helpers, HelperError + +logger = logging.getLogger(__name__) + + +class DiskRepresentation(object): + """Abstract disk image file representation.""" + + 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.""" + 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() + + @property + def path(self): + """System path to this disk file.""" + return self._path + + @property + def disk_subformat(self): + """Sub-format of the disk, such as 'rockridge' or 'streamOptimized'.""" + return self._disk_subformat + + @property + def capacity(self): + """Capacity of this disk image, in bytes.""" + # default implementation - qemu-img handles most types we need + if self._capacity is None: + output = helpers['qemu-img'].call(['info', self.path]) + match = re.search(r"(\d+) bytes", output) + if not match: + raise RuntimeError("Did not find byte count in the output " + "from qemu-img:\n{0}" + .format(output)) + self._capacity = match.group(1) + logger.verbose("Disk %s capacity is %s bytes", self.path, + self._capacity) + return self._capacity + + @property + def files(self): + """List of files embedded in this disk image.""" + if self._files is not None: + return self._files + raise NotImplementedError("Unable to determine file contents") + + @classmethod + def from_other_image(cls, input_image, output_dir, output_subformat=None): + """Convert the other disk image into an image of this type. + + :param DiskRepresentation input_image: Existing image representation. + :param str output_dir: Output directory to store the new image in. + :param str output_subformat: Any relevant subformat information. + :rtype: instance of DiskRepresentation or subclass + """ + raise NotImplementedError("Not a valid target for conversion") + + @classmethod + def file_is_this_type(cls, path): + """Check if the given file is image type represented by this class.""" + if not os.path.exists(path): + raise HelperError(2, "No such file or directory: '{0}'" + .format(path)) + + # Default implementation using qemu-img + output = helpers['qemu-img'].call(['info', path]) + # Read the format from the output + match = re.search(r"file format: (\S*)", output) + if not match: + raise RuntimeError("Did not find file format string in " + "the output from qemu-img:\n{0}" + .format(output)) + file_format = match.group(1) + return file_format == cls.disk_format + + 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: + raise RuntimeError("Capacity and/or files must be specified!") + self._create_file() + + def _create_file(self): + """Worker function for create_file().""" + # Default implementation - create a blank disk using qemu-img + if self._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]) diff --git a/COT/disks/iso.py b/COT/disks/iso.py new file mode 100644 index 0000000..2cb3a6d --- /dev/null +++ b/COT/disks/iso.py @@ -0,0 +1,129 @@ +# October 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. + +"""Handling of ISO files.""" + +import os +import re + +from COT.disks.disk import DiskRepresentation +from COT.helpers import helpers, HelperError + + +class ISO(DiskRepresentation): + """ISO 9660 disk image file representation.""" + + disk_format = "iso" + + @property + def disk_subformat(self): + """ISO sub-format. + + Possible values: + + - "" - not Rock Ridge + - "rockridge" - has Rock Ridge extensions + """ + if self._disk_subformat is None: + output = helpers['isoinfo'].call(['-i', self.path, '-d']) + if re.search(r"Rock Ridge.*found", output): + self._disk_subformat = "rockridge" + else: + # At this time we don't care about Joliet extensions + self._disk_subformat = "" + return self._disk_subformat + + @property + def files(self): + """The list of files contained in this ISO.""" + if self._files is None: + if helpers['isoinfo']: # TODO + args = ["-i", self.path, "-f"] + if self.disk_subformat == "rockridge": + args.append("-R") + # At this time we don't support Joliet extensions + output = helpers['isoinfo'].call(args) + result = [] + for line in output.split("\n"): + # discard non-file output lines + if not line or line[0] != "/": + continue + # Non-Rock-Ridge filenames look like this in isoinfo: + # /IOSXR_CONFIG.TXT;1 + # but the actual filename thus is: + # /iosxr_config.txt + if self.disk_subformat != "rockridge" and ";1" in line: + line = line.lower()[:-2] + # Strip the leading '/' + result.append(line[1:]) + self._files = result + return self._files + + def _create_file(self): + """Create an ISO file.""" + if not self._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', + '-iso-level', '2', '-allow-lowercase'] + if self._disk_subformat == 'rockridge': + args.append('-r') + args += self.files + # TODO require_any_of + if helpers['mkisofs']: + helpers['mkisofs'].call(args) + elif helpers['genisoimage']: + helpers['genisoimage'].call(args) + elif helpers['xorriso']: + args = ['-as', 'mkisofs'] + args + helpers['xorriso'].call(args) + + self._files = None + + @classmethod + def file_is_this_type(cls, path): + """Detect whether the given file is an ISO image.""" + if not os.path.exists(path): + raise HelperError(2, "No such file or directory: '{0}'" + .format(path)) + if helpers['isoinfo']: + try: + helpers['isoinfo'].call(['-i', path, '-d']) + return True + except HelperError: + # Not an ISO + return False + + # else, try to detect ISO files by file magic number + with open(path, 'rb') as f: + for offset in (0x8001, 0x8801, 0x9001): + f.seek(offset) + magic = f.read(5).decode('ascii', 'ignore') + if magic == "CD001": + return True + return False + + @classmethod + def from_other_image(cls, input_image, output_dir, output_subformat=None): + """Convert the other disk image into an image of this type. + + :param DiskRepresentation input_image: Existing image representation. + :param str output_dir: Output directory to store the new image in. + :param str output_subformat: Any relevant subformat information. + :rtype: instance of DiskRepresentation or subclass + """ + raise NotImplementedError("Not a valid target for conversion") diff --git a/COT/disks/qcow2.py b/COT/disks/qcow2.py new file mode 100644 index 0000000..e12c38b --- /dev/null +++ b/COT/disks/qcow2.py @@ -0,0 +1,42 @@ +# October 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. + +"""Handling of QCOW2 files.""" + +import os + +from COT.disks.disk import DiskRepresentation +from COT.helpers import helpers + + +class QCOW2(DiskRepresentation): + """QCOW2 disk image file representation.""" + + disk_format = "qcow2" + + @classmethod + def from_other_image(cls, input_image, output_dir, output_subformat=None): + """Convert the other disk image into an image of this type. + + :param DiskRepresentation input_image: Existing image representation. + :param str output_dir: Output directory to store the new image in. + :param str output_subformat: Any relevant subformat information. + :rtype: instance of DiskRepresentation or subclass + """ + file_name = os.path.basename(input_image.path) + (file_prefix, _) = os.path.splitext(file_name) + output_path = os.path.join(output_dir, file_prefix + ".qcow2") + helpers['qemu-img'].call(['convert', + '-O', 'qcow2', + input_image.path, + output_path]) + return cls(output_path) diff --git a/COT/disks/raw.py b/COT/disks/raw.py new file mode 100644 index 0000000..1f2a627 --- /dev/null +++ b/COT/disks/raw.py @@ -0,0 +1,106 @@ +# October 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. + +"""Handling of raw disk image files.""" + +import logging +import os +import re + +from COT.disks.disk import DiskRepresentation +from COT.helpers import helpers + +logger = logging.getLogger(__name__) + + +class RAW(DiskRepresentation): + """Raw disk image file representation.""" + + disk_format = "raw" + + @property + def files(self): + """List of files on the FAT32 file system of this disk.""" + if self._files is None and self.path and os.path.exists(self.path): + output = helpers['fatdisk'].call([self.path, "ls"]) + # Output looks like: + # + # -----aD 13706 2016 Aug 04 input.ovf + # Listed 1 entry + # + # where all we really want is the 'input.ovf' + result = [] + for line in output.split("\n"): + if not output: + continue + if re.match(r"^Listed", line): + continue + fields = line.split() + if not fields: + continue + if len(fields) < 6: + logger.warning("Unexpected line: %s", line) + continue + result.append(fields[5]) + 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 = 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 from_other_image(cls, input_image, output_dir, output_subformat=None): + """Convert the other disk image into an image of this type. + + :param DiskRepresentation input_image: Existing image representation. + :param str output_dir: Output directory to store the new image in. + :param str output_subformat: Any relevant subformat information. + :rtype: instance of DiskRepresentation or subclass + """ + file_name = os.path.basename(input_image.path) + file_prefix, _ = os.path.splitext(file_name) + output_path = os.path.join(output_dir, file_prefix + ".img") + helpers['qemu-img'].call(['convert', + '-O', 'raw', + input_image.path, + output_path]) + return cls(output_path) diff --git a/COT/disks/tests/__init__.py b/COT/disks/tests/__init__.py new file mode 100644 index 0000000..cbbfebe --- /dev/null +++ b/COT/disks/tests/__init__.py @@ -0,0 +1,13 @@ +# October 2016, Glenn F. Matthews +# Copyright (c) 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 COT.disks package and its submodules.""" diff --git a/COT/disks/tests/test_api.py b/COT/disks/tests/test_api.py new file mode 100644 index 0000000..b3717c7 --- /dev/null +++ b/COT/disks/tests/test_api.py @@ -0,0 +1,79 @@ +#!/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 vmdk images.""" + dr = COT.disks.disk_representation_from_file(self.input_iso) + self.assertEqual(dr.disk_format, "iso") + 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) diff --git a/COT/disks/tests/test_disk_representation.py b/COT/disks/tests/test_disk_representation.py new file mode 100644 index 0000000..16bf846 --- /dev/null +++ b/COT/disks/tests/test_disk_representation.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# +# test_disk.py - Unit test cases for DiskRepresentation class. +# +# 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 DiskRepresentation class.""" + +import logging +import os +import mock + +from COT.tests.ut import COT_UT +from COT.disks.disk import DiskRepresentation +from COT.helpers import HelperError + +logger = logging.getLogger(__name__) + + +class TestDisk(COT_UT): + """Test Disk class.""" + + @mock.patch('COT.helpers.helper.check_output') + def test_capacity_qemu_error(self, mock_check_output): + """Test error handline if qemu-img reports an error.""" + mock_check_output.return_value = "qemu-img info: unsupported command" + with self.assertRaises(RuntimeError): + assert DiskRepresentation(path=self.blank_vmdk).capacity + + def test_from_other_image(self): + """No default from_other_image logic.""" + self.assertRaises(NotImplementedError, + DiskRepresentation.from_other_image, + self.blank_vmdk, self.temp_dir) + + def test_files(self): + """No default files getter logic.""" + with self.assertRaises(NotImplementedError): + assert DiskRepresentation(path=self.blank_vmdk).files + + def test_file_is_this_type_missing_file(self): + """file_is_this_type raises an error if file doesn't exist.""" + self.assertRaises(HelperError, + DiskRepresentation.file_is_this_type, "/foo/bar") + + 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) + + def test_create_file_insufficient_info(self): + """Can't create a file with neither files nor capacity.""" + self.assertRaises(RuntimeError, + DiskRepresentation, + path=os.path.join(self.temp_dir, "foo")) diff --git a/COT/disks/tests/test_iso.py b/COT/disks/tests/test_iso.py new file mode 100644 index 0000000..fd2f423 --- /dev/null +++ b/COT/disks/tests/test_iso.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +# +# test_iso.py - Unit test cases for ISO disk representation. +# +# 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 ISO subclass of DiskRepresentation.""" + +import logging +import os +import mock + +from COT.tests.ut import COT_UT +from COT.disks import ISO +from COT.helpers import helpers, HelperError + +logger = logging.getLogger(__name__) + +# pylint: disable=protected-access + + +class TestISO(COT_UT): + """Test cases for ISO class.""" + + 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]) + if helpers['isoinfo']: + self.assertEqual(iso.files, + [os.path.basename(self.input_ovf)]) + # Our default create format is rockridge + self.assertEqual(iso.disk_subformat, "rockridge") + else: + logger.info("isoinfo not available, not checking disk contents") + + 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="") + if helpers['isoinfo']: + self.assertEqual(iso.files, + [os.path.basename(self.input_ovf)]) + # Our default create format is rockridge + self.assertEqual(iso.disk_subformat, "") + else: + logger.info("isoinfo not available, not checking disk contents") + + def test_create_without_files(self): + """Can't create an empty ISO.""" + self.assertRaises(RuntimeError, + ISO, + path=os.path.join(self.temp_dir, "out.iso"), + capacity="100") + + @mock.patch("COT.helpers.mkisofs.MkISOFS.call") + def test_create_with_mkisofs(self, mock_call): + """Creation of an ISO with mkisofs (default).""" + helpers['mkisofs']._installed = True + ISO(path='foo.iso', files=[self.input_ovf]) + mock_call.assert_called_with( + ['-output', 'foo.iso', '-full-iso9660-filenames', + '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) + + @mock.patch("COT.helpers.mkisofs.GenISOImage.call") + 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='foo.iso', files=[self.input_ovf]) + mock_call.assert_called_with( + ['-output', 'foo.iso', '-full-iso9660-filenames', + '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) + + @mock.patch("COT.helpers.mkisofs.XorrISO.call") + def test_create_with_xorriso(self, mock_call): + """Creation of an ISO with xorriso as last resort.""" + helpers['mkisofs']._installed = False + helpers['genisoimage']._installed = False + helpers['xorriso']._installed = True + ISO(path='foo.iso', files=[self.input_ovf]) + mock_call.assert_called_with( + ['-as', 'mkisofs', '-output', 'foo.iso', '-full-iso9660-filenames', + '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) + + @mock.patch("COT.helpers.mkisofs.MkISOFS.call") + 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='foo.iso', files=[self.input_ovf], disk_subformat="") + mock_call.assert_called_with( + ['-output', 'foo.iso', '-full-iso9660-filenames', + '-iso-level', '2', '-allow-lowercase', self.input_ovf]) + + def test_file_is_this_type_nonexistent(self): + """Call file_is_this_type should fail if file doesn't exist.""" + self.assertRaises(HelperError, + ISO.file_is_this_type, "/foo/bar") + + def test_file_is_this_type_isoinfo(self): + """The file_is_this_type API should use isoinfo if available.""" + if helpers['isoinfo']: + self.assertTrue(ISO.file_is_this_type(self.input_iso)) + self.assertFalse(ISO.file_is_this_type(self.blank_vmdk)) + # TODO - check call at least. + + def test_file_is_this_type_noisoinfo(self): + """The file_is_this_type API should work if isoinfo isn't available.""" + _isoinfo = helpers['isoinfo'] + helpers['isoinfo'] = False + try: + self.assertTrue(ISO.file_is_this_type(self.input_iso)) + self.assertFalse(ISO.file_is_this_type(self.blank_vmdk)) + finally: + helpers['isoinfo'] = _isoinfo + + def test_from_other_image_unsupported(self): + """No support for from_other_image.""" + self.assertRaises(NotImplementedError, + ISO.from_other_image, + self.blank_vmdk, self.temp_dir) diff --git a/COT/disks/tests/test_qcow2.py b/COT/disks/tests/test_qcow2.py new file mode 100644 index 0000000..c96bb87 --- /dev/null +++ b/COT/disks/tests/test_qcow2.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# +# test_qcow2.py - Unit test cases for QCOW2 disk representation. +# +# 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 QCOW2 subclass of DiskRepresentation.""" + +import logging +import os + +from COT.tests.ut import COT_UT +from COT.disks import QCOW2 + +logger = logging.getLogger(__name__) + + +class TestQCOW2(COT_UT): + """Test cases for QCOW2 class.""" + + def test_init_with_files_unsupported(self): + """Creation of a QCOW2 with specific file contents is not supported.""" + self.assertRaises(NotImplementedError, + QCOW2, + path=os.path.join(self.temp_dir, "out.qcow2"), + files=[self.input_ovf]) diff --git a/COT/disks/tests/test_raw.py b/COT/disks/tests/test_raw.py new file mode 100644 index 0000000..a7a9e01 --- /dev/null +++ b/COT/disks/tests/test_raw.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# +# test_raw.py - Unit test cases for RAW disk representation. +# +# 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 RAW subclass of DiskRepresentation.""" + +import logging +import os + +from COT.tests.ut import COT_UT +from COT.disks import RAW, disk_representation_from_file + +logger = logging.getLogger(__name__) + + +class TestRAW(COT_UT): + """Test cases for RAW disk image representation.""" + + def test_convert_from_vmdk(self): + """Test conversion of a RAW image from a VMDK.""" + old = disk_representation_from_file(self.blank_vmdk) + raw = RAW.from_other_image(old, self.temp_dir) + + self.assertEqual(raw.disk_format, 'raw') + self.assertEqual(raw.disk_subformat, None) + + 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") + 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]) + 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") + 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 new file mode 100644 index 0000000..f88c6b0 --- /dev/null +++ b/COT/disks/tests/test_vmdk.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# +# test_vmdk.py - Unit test cases for VMDK disk representation. +# +# 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 VMDK subclass of DiskRepresentation.""" + +import logging +import os + +from distutils.version import StrictVersion +import mock + +from COT.tests.ut import COT_UT +from COT.disks import VMDK, disk_representation_from_file +from COT.helpers import helpers + +logger = logging.getLogger(__name__) + + +class TestVMDK(COT_UT): + """Test cases for VMDK class.""" + + def other_format_to_vmdk_stream_optimized_test(self, disk_format): + """Test conversion of raw to vmdk streamOptimized.""" + 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) + vmdk = VMDK.from_other_image(old, self.temp_dir, 'streamOptimized') + + self.assertEqual(vmdk.disk_format, 'vmdk') + self.assertEqual(vmdk.disk_subformat, 'streamOptimized') + + @mock.patch('COT.helpers.qemu_img.QEMUImg.version', + new_callable=mock.PropertyMock, + return_value=StrictVersion("1.0.0")) + def test_disk_conversion_old_qemu(self, _): + """Test disk conversion flows with older qemu-img version.""" + self.other_format_to_vmdk_stream_optimized_test('raw') + self.other_format_to_vmdk_stream_optimized_test('qcow2') + self.other_format_to_vmdk_stream_optimized_test('vmdk') + + @mock.patch('COT.helpers.qemu_img.QEMUImg.version', + new_callable=mock.PropertyMock, + return_value=StrictVersion("2.1.0")) + def test_disk_conversion_new_qemu(self, _): + """Test disk conversion flows with newer qemu-img version.""" + self.other_format_to_vmdk_stream_optimized_test('raw') + self.other_format_to_vmdk_stream_optimized_test('qcow2') + self.other_format_to_vmdk_stream_optimized_test('vmdk') + + def test_capacity(self): + """Check capacity of several VMDK files.""" + vmdk1 = VMDK(path=self.blank_vmdk) + self.assertEqual(vmdk1.capacity, "536870912") + + vmdk2 = VMDK(path=self.input_vmdk) + self.assertEqual(vmdk2.capacity, "1073741824") + + def test_create_default(self): + """Default creation logic.""" + vmdk = VMDK(path=os.path.join(self.temp_dir, "foo.vmdk"), + capacity="16M") + self.assertEqual(vmdk.path, os.path.join(self.temp_dir, "foo.vmdk")) + self.assertEqual(vmdk.disk_format, "vmdk") + self.assertEqual(vmdk.disk_subformat, "monolithicSparse") + + 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") + self.assertEqual(vmdk.path, os.path.join(self.temp_dir, "foo.vmdk")) + self.assertEqual(vmdk.disk_format, "vmdk") + self.assertEqual(vmdk.disk_subformat, "streamOptimized") diff --git a/COT/disks/vmdk.py b/COT/disks/vmdk.py new file mode 100644 index 0000000..a859c14 --- /dev/null +++ b/COT/disks/vmdk.py @@ -0,0 +1,108 @@ +# October 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. + +"""Handling of VMDK files.""" + +import logging +import os +import re + +from distutils.version import StrictVersion + +from COT.disks.disk import DiskRepresentation +from COT.helpers import helpers, HelperNotFoundError + +logger = logging.getLogger(__name__) + + +class VMDK(DiskRepresentation): + """VMDK disk image file representation.""" + + disk_format = "vmdk" + + @property + def disk_subformat(self): + """Disk subformat, such as 'streamOptimized'.""" + if self._disk_subformat is None: + # Look at the VMDK file header to determine the sub-format + with open(self.path, 'rb') as f: + # The header contains a mix of binary and ASCII, so ignore + # any errors in decoding binary data to strings + header = f.read(1000).decode('ascii', 'ignore') + # Detect the VMDK format from the output: + match = re.search('createType="(.*)"', header) + if not match: + raise RuntimeError( + "Could not find VMDK 'createType' in the " + "file header:\n{0}".format(header)) + vmdk_format = match.group(1) + logger.info("VMDK sub-format is '%s'", vmdk_format) + self._disk_subformat = vmdk_format + return self._disk_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. + + :param DiskRepresentation input_image: Existing image representation. + :param str output_dir: Output directory to store the new image in. + :param str output_subformat: Any relevant subformat information. + :rtype: instance of DiskRepresentation or subclass + """ + file_name = os.path.basename(input_image.path) + (file_prefix, _) = os.path.splitext(file_name) + output_path = os.path.join(output_dir, file_prefix + ".vmdk") + if output_subformat == "streamOptimized": + # TODO + if (helpers['qemu-img'] and + helpers['qemu-img'].version >= StrictVersion("2.1.0")): + helpers['qemu-img'].call(['convert', + '-O', 'vmdk', + '-o', 'subformat=streamOptimized', + input_image.path, + output_path]) + elif helpers['vmdktool']: + if input_image.disk_format != 'raw': + # vmdktool needs a raw image as input + from COT.disks import RAW + temp_image = RAW.from_other_image(input_image, output_dir) + output_image = cls.from_other_image(temp_image, + output_dir, + output_subformat) + os.remove(temp_image.path) + return output_image + + # Note that vmdktool takes its arguments in unusual order - + # output file comes before input file + helpers['vmdktool'].call(['-z9', + '-v', output_path, + input_image.path]) + else: + raise HelperNotFoundError("No helper program available.") + else: + raise NotImplementedError("No support for subformat '%s'", + output_subformat) + 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 = "monolithicSparse" + + 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 diff --git a/COT/helpers/__init__.py b/COT/helpers/__init__.py index e294ba9..d8b72a6 100644 --- a/COT/helpers/__init__.py +++ b/COT/helpers/__init__.py @@ -1,5 +1,5 @@ # February 2015, Glenn F. Matthews -# Copyright (c) 2015 the COT project developers. +# Copyright (c) 2015-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. # @@ -11,13 +11,7 @@ # distributed except according to the terms contained in the LICENSE.txt file. """ -Provide various non-Python helper programs that COT makes use of. - -In general, COT submodules should work through the APIs provided in -:mod:`COT.helpers.api` rather than accessing individual helper program classes. -This gives us the flexibility to change the specific set of helper programs -that are used to provide any given functionality with minimal impact to COT -as a whole. +Provides a common interface for interacting with various non-Python programs. API --- @@ -25,13 +19,8 @@ .. autosummary:: :nosignatures: - ~COT.helpers.api.convert_disk_image - ~COT.helpers.api.create_disk_image - ~COT.helpers.api.get_disk_capacity - ~COT.helpers.api.get_disk_file_listing - ~COT.helpers.api.get_disk_format - ~COT.helpers.api.install_file - ~COT.helpers.api.create_install_dir + helpers + package_managers Exceptions ---------- @@ -47,35 +36,57 @@ .. autosummary:: :toctree: - COT.helpers.api COT.helpers.helper + COT.helpers.apt_get COT.helpers.fatdisk + COT.helpers.gcc COT.helpers.isoinfo + COT.helpers.make COT.helpers.mkisofs COT.helpers.ovftool + COT.helpers.port COT.helpers.qemu_img COT.helpers.vmdktool + COT.helpers.yum """ -from .api import ( - convert_disk_image, - create_disk_image, - create_install_dir, - get_disk_capacity, - get_disk_file_listing, - get_disk_format, - install_file, +from .helper import ( + Helper, PackageManager, helpers, package_managers, + HelperError, HelperNotFoundError, ) -from .helper import HelperError, HelperNotFoundError +from .apt_get import AptGet # 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 + +# 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 + + +for cls in Helper.__subclasses__(): + if cls is PackageManager: + continue + ins = cls() + helpers[ins.name] = ins + + +for cls in PackageManager.__subclasses__(): + ins = cls() + package_managers[ins.name] = ins + __all__ = ( 'HelperError', 'HelperNotFoundError', - 'convert_disk_image', - 'create_disk_image', - 'create_install_dir', - 'get_disk_capacity', - 'get_disk_file_listing', - 'get_disk_format', - 'install_file', + 'helpers', + 'package_managers', ) diff --git a/COT/helpers/api.py b/COT/helpers/api.py deleted file mode 100644 index 210f75b..0000000 --- a/COT/helpers/api.py +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/env python -# -# api.py - API to abstract away operations that require third-party -# helper software not part of a standard Python distro. -# -# April 2014, 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 for abstract access to third-party helper tools. - -Abstracts away operations that require third-party helper programs, -especially those that are not available through PyPI. - -The actual helper programs are provided by individual classes in this package. - -**Functions** - -.. autosummary:: - :nosignatures: - - convert_disk_image - create_disk_image - create_install_dir - get_disk_capacity - get_disk_file_listing - get_disk_format - install_file -""" - -import logging -import os -import re - -from distutils.version import StrictVersion - -from .helper import Helper, HelperError, guess_file_format_from_path -from .fatdisk import FatDisk -from .isoinfo import IsoInfo -from .mkisofs import MkIsoFS -from .ovftool import OVFTool -from .qemu_img import QEMUImg -from .vmdktool import VmdkTool - -logger = logging.getLogger(__name__) - -FATDISK = FatDisk() -ISOINFO = IsoInfo() -MKISOFS = MkIsoFS() -OVFTOOL = OVFTool() -QEMUIMG = QEMUImg() -VMDKTOOL = VmdkTool() - - -def get_disk_format(file_path): - """Get the disk image format of the given file. - - .. warning:: - If :attr:`file_path` refers to a file which is not a disk image at all, - this function will return ``('raw', None)``. - - :param str file_path: Path to disk image file to inspect. - :return: ``(format, subformat)`` - - * ``format`` may be "iso", "vmdk", "raw", or "qcow2" - * ``subformat`` may be ``None``, or various strings for "vmdk" files. - - :raises HelperError: if the file doesn't exist or is otherwise unreadable - """ - if not os.path.exists(file_path): - raise HelperError(2, "No such file or directory: '{0}'" - .format(file_path)) - # IsoInfo is not available if we only have xorriso (as seen in Travis-CI) - if ISOINFO.path: - # isoinfo can identify ISO files, otherwise returning None - (file_format, subformat) = ISOINFO.get_disk_format(file_path) - if file_format: - return (file_format, subformat) - else: - # Try to at least detect ISO files by file magic number - with open(file_path, 'rb') as f: - for offset in (0x8001, 0x8801, 0x9001): - f.seek(offset) - magic = f.read(5).decode('ascii', 'ignore') - if magic == "CD001": - return ("iso", None) - - # QEMUIMG can identify various disk image formats, but guesses 'raw' - # for any arbitrary file it doesn't identify. Thus, 'raw' results should be - # considered suspect, as warned in the docstring above. - file_format = QEMUIMG.get_disk_format(file_path) - - if file_format == 'vmdk': - # Look at the VMDK file header to determine the sub-format - with open(file_path, 'rb') as f: - # The header contains a fun mix of binary and ASCII, so ignore - # any errors in decoding binary data to strings - header = f.read(1000).decode('ascii', 'ignore') - # Detect the VMDK format from the output: - match = re.search('createType="(.*)"', header) - if not match: - raise RuntimeError("Could not find VMDK 'createType' in the " - "file header:\n{0}".format(header)) - vmdk_format = match.group(1) - logger.info("VMDK sub-format is '%s'", vmdk_format) - return (file_format, vmdk_format) - else: - # No known/applicable sub-format - return (file_format, None) - - -def get_disk_capacity(file_path): - """Get the storage capacity of the given disk image. - - :param str file_path: Path to disk image file to inspect - :return: Disk capacity, in bytes - """ - return QEMUIMG.get_disk_capacity(file_path) - - -def get_disk_file_listing(file_path): - """Get the list of files on the given disk. - - :param str file_path: Path to disk image file to inspect. - :return: List of file paths, or None on failure - :raise NotImplementedError: if getting a file listing from the - given file type is not yet supported. - """ - (file_format, _) = get_disk_format(file_path) - if file_format == "iso": - return ISOINFO.get_disk_file_listing(file_path) - elif file_format == "raw": - return FATDISK.get_disk_file_listing(file_path) - else: - raise NotImplementedError("No support for getting a file listing from " - "a %s image (%s)", file_format, file_path) - - -def convert_disk_image(file_path, output_dir, new_format, new_subformat=None): - """Convert the given disk image to the requested format/subformat. - - If the disk is already in this format then it is unchanged; - otherwise, will convert to a new disk in the specified output_dir - and return its path. - - Current supported conversions: - - * .vmdk (any format) to .vmdk (streamOptimized) - * .img to .vmdk (streamOptimized) - - :param str file_path: Disk image file to inspect/convert - :param str output_dir: Directory to place converted image into, if needed - :param str new_format: Desired final format - :param str new_subformat: Desired final subformat - :return: - * :attr:`file_path`, if no conversion was required - * or a file path in :attr:`output_dir` containing the converted image - - :raise NotImplementedError: if the requested conversion is not - yet supported. - """ - curr_format, curr_subformat = get_disk_format(file_path) - - if curr_format == new_format and curr_subformat == new_subformat: - logger.info("Disk image %s is already in '%s' format - " - "no conversion required.", file_path, - (new_format if not new_subformat else - (new_format + "," + new_subformat))) - return file_path - - file_name = os.path.basename(file_path) - (file_string, _) = os.path.splitext(file_name) - - new_file_path = None - # any temporary file we should delete before returning - temp_path = None - - if new_format == 'vmdk' and new_subformat == 'streamOptimized': - new_file_path = os.path.join(output_dir, file_string + '.vmdk') - # QEMU only supports streamOptimized images in versions >= 2.1.0 - if QEMUIMG.version >= StrictVersion("2.1.0"): - new_file_path = QEMUIMG.convert_disk_image( - file_path, output_dir, new_format, new_subformat) - else: - # Older versions of qemu-img don't support streamOptimized VMDKs, - # so we have to use qemu-img + vmdktool to get the desired result. - - # We have to pass through raw format on the way, even if the - # existing image is a non-streamOptimized vmdk. - if curr_format != 'raw': - # Use qemu-img to convert to raw format - temp_path = QEMUIMG.convert_disk_image( - file_path, output_dir, 'raw') - file_path = temp_path - - # Use vmdktool to convert raw image to stream-optimized VMDK - new_file_path = VMDKTOOL.convert_disk_image( - file_path, output_dir, new_format, new_subformat) - - else: - raise NotImplementedError( - "no support for converting disk to {0} / {1}" - .format(new_format, new_subformat)) - - logger.info("Successfully converted from (%s,%s) to (%s,%s)", - curr_format, curr_subformat, new_format, new_subformat) - - if temp_path is not None: - os.remove(temp_path) - - return new_file_path - - -def create_disk_image(file_path, file_format=None, - capacity=None, contents=None): - """Create a new disk image at the requested location. - - Either :attr:`capacity` or :attr:`contents` or both must be specified. - - :param str file_path: Desired location of new disk image - :param str file_format: Desired image format (if not specified, this will - be derived from the file extension of :attr:`file_path`) - - :param capacity: Disk capacity. A string like '16M' or '1G'. - :param list contents: List of file paths to package into the created image. - If not specified, the image will be left blank and unformatted. - - :raise NotImplementedError: if creation of the given image type is not - yet supported. - """ - if not capacity and not contents: - raise RuntimeError("Either capacity or contents must be specified!") - - if not file_format: - file_format = guess_file_format_from_path(file_path) - - if not contents: - QEMUIMG.create_blank_disk(file_path, capacity, file_format) - elif file_format == 'iso': - MKISOFS.create_iso(file_path, contents) - elif file_format == 'raw' or file_format == 'img': - FATDISK.create_raw_image(file_path, contents, capacity) - else: - # We could create a raw image then convert it to the - # desired format but there's no use case for that at present. - raise NotImplementedError( - "unable to create disk of format {0} with the given contents" - .format(file_format)) - - -def install_file(src, dest): - """Copy the given src to the given dest, using sudo if needed. - - :param str src: Source path. - :param str dest: Destination path. - :return: True - """ - return Helper.install_file(src, dest) - - -def create_install_dir(directory, permissions=493): # 493 == 0o755 - """Check whether the given target directory exists, and create if not. - - :param str directory: Directory to check/create. - :param int permissions: Permissions bits to set on the directory. - :return: True - """ - return Helper.make_install_dir(directory, permissions) diff --git a/COT/helpers/apt_get.py b/COT/helpers/apt_get.py new file mode 100644 index 0000000..3949f09 --- /dev/null +++ b/COT/helpers/apt_get.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# +# apt_get.py - Wrapper for the 'apt-get' package manager. +# +# October 2016, Glenn F. Matthews +# Copyright (c) 2015-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. + +"""Wrapper for the 'apt-get' package manager.""" + +import logging +import re + +from COT.helpers.helper import PackageManager, helpers + +logger = logging.getLogger(__name__) + + +class AptGet(PackageManager): + """The 'apt-get' package manager utility.""" + + _updated = False + + def __init__(self): + """Initializer.""" + super(AptGet, self).__init__("apt-get", version_regexp="apt ([0-9.]+)") + + def install_package(self, package): + """Install the requested package if needed. + + :param str package: Name of the package to install. + """ + # Check whether it's already installed + if re.search(r"install ok installed", + helpers['dpkg'].call(['-s', package], + require_success=False)): + return + # Update the repository if needed + if not AptGet._updated: + self.call(['-q', 'update'], + capture_output=False, retry_with_sudo=True) + AptGet._updated = True + self.call(['-q', 'install', package], + capture_output=False, retry_with_sudo=True) diff --git a/COT/helpers/fatdisk.py b/COT/helpers/fatdisk.py index 138725a..c3e9ced 100644 --- a/COT/helpers/fatdisk.py +++ b/COT/helpers/fatdisk.py @@ -23,69 +23,50 @@ import os import os.path import platform -import re -from COT.helpers.helper import Helper +from COT.helpers.helper import Helper, helpers, package_managers, check_call logger = logging.getLogger(__name__) class FatDisk(Helper): - """Helper provider for ``fatdisk`` (http://github.com/goblinhack/fatdisk). - - **Methods** - - .. autosummary:: - :nosignatures: - - install_helper - create_raw_image - get_disk_file_listing - """ + """Wrapper for ``fatdisk`` (http://github.com/goblinhack/fatdisk).""" def __init__(self): """Initializer.""" - super(FatDisk, self).__init__("fatdisk", - version_regexp="version ([0-9.]+)") - - def _install_linux_prereqs(self): - """Install Linux tools that are prerequisites for fatdisk.""" - # Fatdisk installation requires make - if not self.find_executable('make'): - logger.info("fatdisk requires 'make'... installing 'make'") - if not (Helper.apt_install('make') or - Helper.yum_install('make')): - raise NotImplementedError("Not sure how to install 'make'") - assert self.find_executable('make') - - # Fatdisk requires clang or gcc or g++ - if not (self.find_executable('clang') or - self.find_executable('gcc') or - self.find_executable('g++')): - logger.info("fatdisk requires a C compiler... installing 'gcc'") - if not (Helper.apt_install('gcc') or - Helper.yum_install('gcc')): - raise NotImplementedError( - "Not sure how to install a C compiler") - assert (self.find_executable('clang') or - self.find_executable('gcc') or - self.find_executable('g++')) - - def install_helper(self): + super(FatDisk, self).__init__( + "fatdisk", + info_uri="http://github.com/goblinhack/fatdisk", + version_regexp="version ([0-9.]+)") + + @property + def installable(self): + """Whether COT is capable of installing this program on this system.""" + return (package_managers['port'] or + (platform.system() == 'Linux' and + (helpers['make'] or helpers['make'].installable) and + (helpers['clang'] or helpers['gcc'] or + helpers['g++'] or helpers['gcc'].installable))) + + def _install(self): """Install ``fatdisk``.""" - if self.should_not_be_installed_but_is(): - return - logger.info("Installing 'fatdisk'...") - if Helper.port_install('fatdisk'): - pass + if package_managers['port']: + package_managers['port'].install_package('fatdisk') elif platform.system() == 'Linux': - self._install_linux_prereqs() + # Fatdisk installation requires make + helpers['make'].install() + + # Fatdisk requires clang or gcc or g++ + # TODO + if not (helpers['clang'] or helpers['gcc'] or helpers['g++']): + helpers['gcc'].install() + with self.download_and_expand_tgz( 'https://github.com/goblinhack/' 'fatdisk/archive/v1.0.0-beta.tar.gz') as d: new_d = os.path.join(d, 'fatdisk-1.0.0-beta') logger.info("Compiling 'fatdisk'") - self._check_call(['./RUNME'], cwd=new_d) + check_call(['./RUNME'], cwd=new_d) destdir = os.getenv('DESTDIR', '') prefix = os.getenv('PREFIX', '/usr/local') # os.path.join doesn't like absolute paths in the middle @@ -94,66 +75,5 @@ def install_helper(self): destination = os.path.join(destdir, prefix, 'bin') logger.info("Compilation complete, installing to " + destination) - self.make_install_dir(destination) - self.install_file(os.path.join(new_d, 'fatdisk'), destination) - else: - raise NotImplementedError( - "Not sure how to install 'fatdisk'.\n" - "See https://github.com/goblinhack/fatdisk") - logger.info("Successfully installed 'fatdisk'") - - def create_raw_image(self, file_path, contents, capacity=None): - """Create a new FAT32-formatted raw image at the requested location. - - :param str file_path: Desired location of new disk image - :param list contents: List of file paths to package into the created - image. - :param capacity: (optional) Disk capacity. A string like '16M' or '1G'. - """ - if not capacity: - # What size disk do we need to contain the requested file(s)? - capacity_val = 0 - for content_file in contents: - capacity_val += os.path.getsize(content_file) - # Round capacity to the next larger multiple of 8 MB - # just to be safe... - capacity = "{0}M".format(((capacity_val/1024/1024/8) + 1)*8) - logger.verbose( - "To contain files %s, disk capacity of %s will be %s", - contents, file_path, capacity) - logger.info("Calling fatdisk to create and format a raw disk image") - self.call_helper([file_path, 'format', 'size', capacity, 'fat32']) - for content_file in contents: - logger.verbose("Calling fatdisk to add %s to the image", - content_file) - self.call_helper([file_path, 'fileadd', content_file, - os.path.basename(content_file)]) - logger.info("All requested files successfully added to %s", file_path) - - def get_disk_file_listing(self, file_path): - """Get the list of files on the given raw disk image. - - :param str file_path: Path to disk image file to inspect. - :return: List of file paths, or None on failure - """ - output = self.call_helper([file_path, "ls"]) - # Output looks like: - # - # -----aD 13706 2016 Aug 04 input.ovf - # Listed 1 entry - # - # where all we really want is the 'input.ovf' - result = [] - for line in output.split("\n"): - if not output: - continue - if re.match(r"^Listed", line): - continue - fields = line.split() - if not fields: - continue - if len(fields) < 6: - logger.warning("Unexpected line: %s", line) - continue - result.append(fields[5]) - return result + self.mkdir(destination) + self.cp(os.path.join(new_d, 'fatdisk'), destination) diff --git a/COT/helpers/gcc.py b/COT/helpers/gcc.py new file mode 100644 index 0000000..8810981 --- /dev/null +++ b/COT/helpers/gcc.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# +# gcc.py - Helper for 'gcc' +# +# October 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. + +"""Give COT access to ``gcc`` command for building other helpers.""" + +from COT.helpers.helper import Helper + + +class GCC(Helper): + """Helper provider for ``gcc`` command.""" + + _provider_packages = { + 'apt-get': 'gcc', + 'yum': 'gcc', + } + + def __init__(self): + """Initializer.""" + super(GCC, self).__init__("gcc") diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 41d804e..ae27429 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -14,13 +14,40 @@ # of COT, including this file, may be copied, modified, propagated, or # distributed except according to the terms contained in the LICENSE.txt file. -"""Interface for providers of non-Python helper programs. +"""Common interface for providers of non-Python helper programs. Provides the ability to install the program if not already present, and the ability to run the program as well. + +**Classes** + +.. autosummary:: + :nosignatures: + + HelperNotFoundError + HelperError + Helper + PackageManager + +**Attributes** + +.. autosummary:: + :nosignatures: + + helpers + package_managers + +**Functions** + +.. autosummary:: + :nosignatures: + + check_call + check_output """ import logging + import os import os.path import contextlib @@ -28,6 +55,24 @@ import re import shutil import subprocess + +try: + from subprocess import check_output as _check_output +except ImportError: + # Python 2.6 doesn't have subprocess.check_output. + # Implement it ourselves: + def _check_output(args, **kwargs): + process = subprocess.Popen(args, + stdout=subprocess.PIPE, + **kwargs) + stdout, _ = process.communicate() + retcode = process.poll() + if retcode: + e = subprocess.CalledProcessError(retcode, " ".join(args)) + e.output = stdout + raise e + return stdout + import tarfile import distutils.spawn from distutils.version import StrictVersion @@ -60,19 +105,6 @@ def TemporaryDirectory(suffix='', # noqa: N802 shutil.rmtree(tempdir) -def guess_file_format_from_path(file_path): - """Guess the preferred file format based on file path/extension.""" - file_format = os.path.splitext(file_path)[1][1:] - if not file_format: - raise RuntimeError( - "Unable to guess file format from desired filename {0}" - .format(file_path)) - if file_format == 'img': - file_format = 'raw' - logger.debug("Guessed file format is %s", file_format) - return file_format - - class HelperNotFoundError(OSError): """A helper program cannot be located.""" @@ -81,34 +113,46 @@ class HelperError(EnvironmentError): """A helper program exited with non-zero return code.""" -class Helper(object): - """A provider of a non-Python helper program. +class HelperDict(dict): + """Dictionary of Helper objects by name. - **Class Properties** + Similar to :class:`collections.defaultdict` but takes the key + as a parameter to the factory. + """ + + def __init__(self, factory, *args, **kwargs): + """Create the given dictionary with the given factory class/method.""" + super(HelperDict, self).__init__(*args, **kwargs) + self.factory = factory + + def __missing__(self, key): + """Method called when accessing a non-existent key. + + Automatically populate the given key with an instance of the factory. + """ + self[key] = self.factory(key) + return self[key] - .. autosummary:: - :nosignatures: - PACKAGE_MANAGERS +class Helper(object): + """A provider of a non-Python helper program. - **Class Methods** + **Static Methods** .. autosummary:: :nosignatures: - confirm + cp download_and_expand_tgz - apt_install - port_install - yum_install - find_executable - make_install_dir - install_file + mkdir **Instance Properties** .. autosummary:: name + info_uri + installable + installed path version @@ -117,30 +161,160 @@ class Helper(object): .. autosummary:: :nosignatures: - call_helper - install_helper + call + install """ - @classmethod - def confirm(cls, _prompt): - """Prompt user to confirm the requested operation. + def __init__(self, name, + info_uri=None, + version_args=None, + version_regexp="([0-9.]+)"): + """Initializer. + + :param name: Name of helper executable + :param list version_args: Args to pass to the helper to + get its version. Defaults to ``['--version']`` if unset. + :param version_regexp: Regexp to get the version number from + the output of the command. + """ + self._name = name + self._info_uri = info_uri + self._path = None + self._installed = None + self._version = None + if not version_args: + version_args = ['--version'] + self._version_args = version_args + self._version_regexp = version_regexp + + def __bool__(self): + """A helper is True if installed and False if not installed.""" + return self.installed + + # For Python 2.x compatibility: + __nonzero__ = __bool__ + + _provider_packages = {} + + UI = None + """User interface (if any) available to helpers.""" + + @property + def name(self): + """Name of the helper program.""" + return self._name + + @property + def info_uri(self): + """URI for more information about this helper.""" + return self._info_uri + + @property + def path(self): + """Discovered path to the helper.""" + if not self._path: + logger.verbose("Checking for helper executable %s", self.name) + self._path = distutils.spawn.find_executable(self.name) + if self._path: + logger.verbose("%s is at %s", self.name, self.path) + self._installed = True + else: + logger.verbose("No path to %s found", self.name) + return self._path + + @property + def installed(self): + """Whether this helper program is installed and available to run.""" + if self._installed is None: + self._installed = (self.path is not None) + return self._installed + + @property + def installable(self): + """Whether COT is capable of installing this program on this system.""" + for pm_name in self._provider_packages: + if package_managers[pm_name]: + return True + return False + + @property + def version(self): + """Release version of the associated helper program.""" + if self.installed and not self._version: + output = self.call(self._version_args, require_success=False) + match = re.search(self._version_regexp, output) + if not match: + raise RuntimeError("Unable to find version number in output:" + "\n{0}".format(output)) + self._version = StrictVersion(match.group(1)) + return self._version - Default method to be used when COT.helpers is run by itself without - the broader COT package. - The COT package will override this with e.g. the CLI.confirm method. + def call(self, args, + capture_output=True, **kwargs): + """Call the helper program with the given arguments. - :param str _prompt: Message to prompt the user with - :return: ``True`` (user confirms acceptance) or ``False`` - (user declines) + :param list args: List of arguments to the helper program. + :param boolean capture_output: If ``True``, stdout/stderr will be + redirected to a buffer and returned, instead of being displayed + to the user. + :param boolean require_success: if ``True``, an exception will be + raised if the helper exits with a non-zero status code. + :param boolean retry_with_sudo: if ``True``, if the helper fails, + will prepend ``sudo`` and retry one more time before giving up. + :return: Captured stdout/stderr (if :attr:`capture_output`), + else ``None``. """ - return True + if not self.path: + if self.UI and not self.UI.confirm( + "{0} does not appear to be installed.\nTry to install it?" + .format(self.name)): + raise HelperNotFoundError( + 1, + "Unable to proceed without helper program '{0}'. " + "Please install it and/or check your $PATH." + .format(self.name)) + self.install() + args.insert(0, self.name) + if capture_output: + return check_output(args, **kwargs) + else: + check_call(args, **kwargs) + return None + + def install(self): + """Install the helper program. + + :raise: :exc:`NotImplementedError` if not ``installable`` + :raise: :exc:`HelperError` if installation is attempted but fails. - PACKAGE_MANAGERS = { - "port": distutils.spawn.find_executable('port'), - "apt-get": distutils.spawn.find_executable('apt-get'), - "yum": distutils.spawn.find_executable('yum'), - } - """Class-level lookup for package manager executables.""" + Subclasses should not override this method but instead should provide + an appropriate implementation of the :meth:`_install` method. + """ + if self.installed: + return + if not self.installable: + msg = "Unsure how to install {0}.".format(self.name) + if self.info_uri: + msg += "\nRefer to {0} for information".format(self.info_uri) + raise NotImplementedError(msg) + logger.info("Installing '%s'...", self.name) + # Call the subclass implementation + self._install() + # Make sure it actually performed as promised + assert self.path, "after installing, path is {0}".format(self.path) + + logger.info("Successfully installed '%s'", self.name) + + def _install(self): + """Subclass-specific implementation of installation logic.""" + # Default implementation + for pm_name, packages in self._provider_packages.items(): + if not package_managers[pm_name]: + continue + if isinstance(packages, str): + packages = [packages] + for pkg in packages: + package_managers[pm_name].install_package(pkg) @staticmethod @contextlib.contextmanager @@ -182,57 +356,12 @@ def download_and_expand_tgz(url): logger.debug("Cleaning up temporary directory %s", d) @staticmethod - def find_executable(name): - """Wrapper for :func:`distutils.spawn.find_executable`.""" - return distutils.spawn.find_executable(name) - - _apt_updated = False - """Whether we have run 'apt-get update' yet.""" - - @classmethod - def apt_install(cls, package): - """Try to use ``apt-get`` to install a package.""" - if not cls.PACKAGE_MANAGERS['apt-get']: - return False - # check if it's already installed - msg = cls._check_output(['dpkg', '-s', package], require_success=False) - if re.search('install ok installed', msg): - return True - if not cls._apt_updated: - cls._check_call(['apt-get', '-q', 'update'], retry_with_sudo=True) - cls._apt_updated = True - cls._check_call(['apt-get', '-q', 'install', package], - retry_with_sudo=True) - return True - - _port_updated = False - """Whether we have run 'port selfupdate' yet.""" - - @classmethod - def port_install(cls, package): - """Try to use ``port`` to install a package.""" - if not cls.PACKAGE_MANAGERS['port']: - return False - if not cls._port_updated: - cls._check_call(['port', 'selfupdate'], retry_with_sudo=True) - cls._port_updated = True - cls._check_call(['port', 'install', package], retry_with_sudo=True) - return True - - @classmethod - def yum_install(cls, package): - """Try to use ``yum`` to install a package.""" - if not cls.PACKAGE_MANAGERS['yum']: - return False - cls._check_call(['yum', '--quiet', 'install', package], - retry_with_sudo=True) - return True - - @classmethod - def make_install_dir(cls, directory, permissions=493): # 493 == 0o755 + def mkdir(directory, permissions=493): # 493 == 0o755 """Check whether the given target directory exists, and create if not. - :param directory: Directory to check/create. + :param str directory: Directory to check/create. + :param permissions: Permission mask to set when creating a directory. + Default is ``0o755``. """ if os.path.isdir(directory): # TODO: permissions check, update permissions if needed @@ -248,16 +377,16 @@ def make_install_dir(cls, directory, permissions=493): # 493 == 0o755 logger.verbose("Directory %s creation failed, trying sudo", directory) try: - cls._check_call(['sudo', 'mkdir', '-p', - '--mode=%o' % permissions, - directory]) + check_call(['sudo', 'mkdir', '-p', + '--mode=%o' % permissions, + directory]) except HelperError: # That failed too - re-raise the original exception raise e return True - @classmethod - def install_file(cls, src, dest): + @staticmethod + def cp(src, dest): """Copy the given src to the given dest, using sudo if needed. :param str src: Source path. @@ -270,226 +399,130 @@ def install_file(cls, src, dest): except (OSError, IOError) as e: logger.verbose('Installation error, trying sudo.') try: - cls._check_call(['sudo', 'cp', src, dest]) + check_call(['sudo', 'cp', src, dest]) except HelperError: # That failed too - re-raise the original exception raise e return True - def __init__(self, name, version_args=None, - version_regexp="([0-9.]+"): - """Initializer. - - :param name: Name of helper executable - :param list version_args: Args to pass to the helper to - get its version. Defaults to ``['--version']`` if unset. - :param version_regexp: Regexp to get the version number from - the output of the command. - """ - self._name = name - self._path = None - self._version = None - if not version_args: - version_args = ['--version'] - self._version_args = version_args - self._version_regexp = version_regexp - @property - def name(self): - """Name of the helper program.""" - return self._name +helpers = HelperDict(Helper) +"""Dictionary of concrete Helper subclasses to be populated at load time.""" - @property - def path(self): - """Discovered path to the helper.""" - if not self._path: - logger.verbose("Checking for helper executable %s", self.name) - self._path = self.find_executable(self.name) - if self._path: - logger.verbose("%s is at %s", self.name, self.path) - else: - logger.verbose("No path to %s found", self.name) - return self._path - @property - def version(self): - """Release version of the associated helper program.""" - if self.path and not self._version: - output = self.call_helper(self._version_args, - require_success=False) - match = re.search(self._version_regexp, output) - if not match: - raise RuntimeError("Unable to find version number in output:" - "\n{0}".format(output)) - self._version = StrictVersion(match.group(1)) - return self._version +class PackageManager(Helper): + """Helper program with additional API method install_package().""" - def call_helper(self, args, capture_output=True, require_success=True): - """Call the helper program with the given arguments. + def install_package(self, package): + """Install the requested package if needed. - :param list args: List of arguments to the helper program. - :param boolean capture_output: If ``True``, stdout/stderr will be - redirected to a buffer and returned, instead of being displayed - to the user. - :param boolean require_success: if ``True``, an exception will be - raised if the helper exits with a non-zero status code. - :return: Captured stdout/stderr (if :attr:`capture_output`), - else ``None``. + :param str package: Name of the package to install. """ - if not self.path: - if not self.confirm("{0} does not appear to be installed.\n" - "Try to install it?" - .format(self.name)): - raise HelperNotFoundError( - 1, - "Unable to proceed without helper program '{0}'. " - "Please install it and/or check your $PATH." - .format(self.name)) - self.install_helper() - args.insert(0, self.name) - if capture_output: - return self._check_output(args, require_success) - else: - self._check_call(args, require_success) - return None + raise NotImplementedError("install_package not implemented!") - def should_not_be_installed_but_is(self): - """Check whether the tool is already installed. - :return: False, and logs a warning message, if installed - :return: True, if not installed - """ - if self.path: - logger.warning("Tried to install %s -- but it's already available " - "at %s!", self.name, self.path) - return True - return False +package_managers = HelperDict(PackageManager) +"""Dictionary of concrete PackageManager subclasses, populated at load time.""" - # Abstract interfaces to be implemented by subclasses - def install_helper(self): - """Install the helper program (abstract method). +def check_call(args, require_success=True, retry_with_sudo=False, **kwargs): + """Wrapper for :func:`subprocess.check_call`. - :raise: :exc:`NotImplementedError` as this method must be implemented - by a concrete subclass. - """ - if self.should_not_be_installed_but_is(): + Unlike :meth:`check_output` below, this does not redirect stdout + or stderr; all output from the subprocess will be sent to the system + stdout/stderr as normal. + + :param list args: Command to invoke and its associated args + :param boolean require_success: If ``False``, do not raise an error + when the command exits with a return code other than 0 + :param boolean retry_with_sudo: If ``True``, if the command gets + an exception, prepend ``sudo`` to the command and try again. + + :raise HelperNotFoundError: if the command doesn't exist + (instead of a :class:`OSError`) + :raise HelperError: if :attr:`require_success` is not ``False`` and + the command returns a value other than 0 (instead of a + :class:`CalledProcessError`). + :raise OSError: as :func:`subprocess.check_call`. + """ + cmd = args[0] + logger.info("Calling '%s'...", " ".join(args)) + try: + subprocess.check_call(args, **kwargs) + except OSError as e: + if retry_with_sudo and (e.errno == errno.EPERM or + e.errno == errno.EACCES): + check_call(['sudo'] + args, + require_success=require_success, + retry_with_sudo=False, + **kwargs) return - raise NotImplementedError("Unsure how to install %s", self.name) - - # Private methods - - @classmethod - def _check_call(cls, args, require_success=True, retry_with_sudo=False, - **kwargs): - """Wrapper for :func:`subprocess.check_call`. - - Unlike :meth:`check_output` below, this does not redirect stdout - or stderr; all output from the subprocess will be sent to the system - stdout/stderr as normal. - - :param list args: Command to invoke and its associated args - :param boolean require_success: If ``False``, do not raise an error - when the command exits with a return code other than 0 - :param boolean retry_with_sudo: If ``True``, if the command gets - an exception, prepend ``sudo`` to the command and try again. - - :raise HelperNotFoundError: if the command doesn't exist - (instead of a :class:`OSError`) - :raise HelperError: if :attr:`require_success` is not ``False`` and - the command returns a value other than 0 (instead of a - :class:`CalledProcessError`). - :raise OSError: as :func:`subprocess.check_call`. - """ - cmd = args[0] - logger.info("Calling '%s'...", " ".join(args)) - try: - subprocess.check_call(args, **kwargs) - except OSError as e: - if retry_with_sudo and (e.errno == errno.EPERM or - e.errno == errno.EACCES): - cls._check_call(['sudo'] + args, - require_success=require_success, - retry_with_sudo=False, - **kwargs) + if e.errno != errno.ENOENT: + raise + raise HelperNotFoundError(e.errno, + "Unable to locate helper program '{0}'. " + "Please check your $PATH.".format(cmd)) + except subprocess.CalledProcessError as e: + if require_success: + if retry_with_sudo: + check_call(['sudo'] + args, + require_success=require_success, + retry_with_sudo=False, + **kwargs) return - if e.errno != errno.ENOENT: - raise - raise HelperNotFoundError(e.errno, - "Unable to locate helper program '{0}'. " - "Please check your $PATH.".format(cmd)) - except subprocess.CalledProcessError as e: - if require_success: - if retry_with_sudo: - cls._check_call(['sudo'] + args, + raise HelperError(e.returncode, + "Helper program '{0}' exited with error {1}" + .format(cmd, e.returncode)) + logger.info("...done") + logger.debug("%s exited successfully", cmd) + + +def check_output(args, require_success=True, retry_with_sudo=False, **kwargs): + """Wrapper for :func:`subprocess.check_output`. + + Automatically redirects stderr to stdout, captures both to a buffer, + and generates a debug message with the stdout contents. + + :param list args: Command to invoke and its associated args + :param boolean require_success: If ``False``, do not raise an error + when the command exits with a return code other than 0 + :param boolean retry_with_sudo: If ``True``, if the command gets + an exception, prepend ``sudo`` to the command and try again. + + :return: Captured stdout/stderr from the command + + :raise HelperNotFoundError: if the command doesn't exist + (instead of a :class:`OSError`) + :raise HelperError: if :attr:`require_success` is not ``False`` and + the command returns a value other than 0 (instead of a + :class:`CalledProcessError`). + :raise OSError: as :func:`subprocess.check_call`. + """ + cmd = args[0] + logger.info("Calling '%s' and capturing its output...", " ".join(args)) + try: + stdout = _check_output(args, + stderr=subprocess.STDOUT, + **kwargs).decode('ascii', 'ignore') + except OSError as e: + if e.errno != errno.ENOENT: + raise + raise HelperNotFoundError(e.errno, + "Unable to locate helper program '{0}'. " + "Please check your $PATH.".format(cmd)) + except subprocess.CalledProcessError as e: + stdout = e.output.decode() + if require_success: + if retry_with_sudo: + return check_output(['sudo'] + args, require_success=require_success, retry_with_sudo=False, **kwargs) - return - raise HelperError(e.returncode, - "Helper program '{0}' exited with error {1}" - .format(cmd, e.returncode)) - logger.info("...done") - logger.debug("%s exited successfully", cmd) - - @classmethod - def _check_output(cls, args, require_success=True, **kwargs): - """Wrapper for :func:`subprocess.check_output`. - - Automatically redirects stderr to stdout, captures both to a buffer, - and generates a debug message with the stdout contents. - - :param list args: Command to invoke and its associated args - :param boolean require_success: If ``False``, do not raise an error - when the command exits with a return code other than 0 - - :return: Captured stdout/stderr from the command - - :raise HelperNotFoundError: if the command doesn't exist - (instead of a :class:`OSError`) - :raise HelperError: if :attr:`require_success` is not ``False`` and - the command returns a value other than 0 (instead of a - :class:`CalledProcessError`). - :raise OSError: as :func:`subprocess.check_call`. - """ - cmd = args[0] - logger.info("Calling '%s' and capturing its output...", " ".join(args)) - # In 2.7+ we can use subprocess.check_output(), but in 2.6, - # we have to work around its absence. - try: - if "check_output" not in dir(subprocess): - process = subprocess.Popen(args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - **kwargs) - stdout, _ = process.communicate() - retcode = process.poll() - if retcode and require_success: - raise subprocess.CalledProcessError(retcode, - " ".join(args)) - else: - stdout = (subprocess.check_output(args, - stderr=subprocess.STDOUT, - **kwargs) - .decode('ascii', 'ignore')) - except OSError as e: - if e.errno != errno.ENOENT: - raise - raise HelperNotFoundError(e.errno, - "Unable to locate helper program '{0}'. " - "Please check your $PATH.".format(cmd)) - except subprocess.CalledProcessError as e: - try: - stdout = e.output.decode() - except AttributeError: - # CalledProcessError doesn't have 'output' in 2.6 - stdout = u"(output unavailable)" - if require_success: - raise HelperError(e.returncode, - "Helper program '{0}' exited with error {1}:" - "\n> {2}\n{3}".format(cmd, e.returncode, - " ".join(args), - stdout)) - logger.info("...done") - logger.verbose("%s output:\n%s", cmd, stdout) - return stdout + raise HelperError(e.returncode, + "Helper program '{0}' exited with error {1}:" + "\n> {2}\n{3}".format(cmd, e.returncode, + " ".join(args), + stdout)) + logger.info("...done") + logger.verbose("%s output:\n%s", cmd, stdout) + return stdout diff --git a/COT/helpers/isoinfo.py b/COT/helpers/isoinfo.py index 5263d61..75c5149 100644 --- a/COT/helpers/isoinfo.py +++ b/COT/helpers/isoinfo.py @@ -17,86 +17,25 @@ """Give COT access to isoinfo for inspecting ISO images. http://cdrecord.org/ -https://www.gnu.org/software/xorriso/ """ -import logging -import re +from .helper import Helper -from .helper import Helper, HelperError -logger = logging.getLogger(__name__) - - -class IsoInfo(Helper): +class ISOInfo(Helper): """Helper provider for ``isoinfo``. http://cdrecord.org/ - - **Methods** - - .. autosummary:: - :nosignatures: - - get_disk_format - get_disk_file_listing """ + _provider_packages = { + 'apt-get': 'genisoimage', + 'port': 'cdrtools', + } + def __init__(self): """Initializer.""" - super(IsoInfo, self).__init__("isoinfo", - version_regexp=r"isoinfo ([0-9.]+)") - - # No install support as this is provided by MkIsoFS class or not at all. - - def get_disk_format(self, file_path): - """Get the major disk image format of the given file. - - :param str file_path: Path to disk image file to inspect. - :return: ``(format, subformat)``, such as: - - * ``(None, None)`` - file is not an ISO - * ``("iso", None)`` - ISO without Rock Ridge or Joliet extensions - * ``("iso", "Rock Ridge")`` - ISO with Rock Ridge extensions - """ - try: - output = self.call_helper(['-i', file_path, '-d']) - except HelperError: - # Not an ISO - return (None, None) - - # If no exception, isoinfo recognized it as an ISO file. - subformat = None - if re.search(r"Rock Ridge.*found", output): - subformat = "Rock Ridge" - # At this time we don't care about Joliet extensions - return ('iso', subformat) - - def get_disk_file_listing(self, file_path): - """Get the list of files on the given ISO. - - :param str file_path: Path to ISO file to inspect. - :return: List of file paths, or None on failure - """ - (iso, subformat) = self.get_disk_format(file_path) - if iso != "iso": - return None - args = ["-i", file_path, "-f"] - if subformat == "Rock Ridge": - args.append("-R") - # At this time we don't support Joliet extensions - output = self.call_helper(args) - result = [] - for line in output.split("\n"): - # discard non-file output lines - if not line or line[0] != "/": - continue - # Non-Rock-Ridge filenames look like this in isoinfo: - # /IOSXR_CONFIG.TXT;1 - # but the actual filename thus is: - # /iosxr_config.txt - if subformat != "Rock Ridge" and ";1" in line: - line = line.lower()[:-2] - # Strip the leading '/' - result.append(line[1:]) - return result + super(ISOInfo, self).__init__( + "isoinfo", + info_uri="http://cdrecord.org", + version_regexp=r"isoinfo ([0-9.]+)") diff --git a/COT/helpers/make.py b/COT/helpers/make.py new file mode 100644 index 0000000..ba374e0 --- /dev/null +++ b/COT/helpers/make.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# +# make.py - Helper for 'make' +# +# October 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. + +"""Give COT access to ``make`` command for building other helpers.""" + +from COT.helpers.helper import Helper + + +class Make(Helper): + """Helper provider for ``make`` command.""" + + _provider_packages = { + 'apt-get': 'make', + 'yum': 'make', + } + + def __init__(self): + """Initializer.""" + super(Make, self).__init__("make") diff --git a/COT/helpers/mkisofs.py b/COT/helpers/mkisofs.py index 56b3dc1..9d59109 100644 --- a/COT/helpers/mkisofs.py +++ b/COT/helpers/mkisofs.py @@ -18,103 +18,63 @@ http://cdrecord.org/ https://www.gnu.org/software/xorriso/ -""" -import logging +**Classes** + +.. autosummary:: + :nosignatures: -from .helper import Helper, HelperError + MkISOFS + GenISOImage + XorrISO +""" -logger = logging.getLogger(__name__) +from .helper import Helper -class MkIsoFS(Helper): - """Helper provider for ``mkisofs``, ``genisoimage``, or ``xorriso``. +class MkISOFS(Helper): + """Helper provider for ``mkisofs``. http://cdrecord.org/ - https://www.gnu.org/software/xorriso/ + """ + + _provider_packages = { + 'port': 'cdrtools', + } + + def __init__(self): + """Initializer.""" + super(MkISOFS, self).__init__("mkisofs", + version_regexp="mkisofs ([0-9.]+)") - **Methods** - .. autosummary:: - :nosignatures: +class GenISOImage(Helper): + """Helper provider for ``genisoimage``, a fork of mkisofs.""" - install_helper - create_iso + _provider_packages = { + 'apt-get': 'genisoimage', + 'yum': 'genisoimage', + } + + def __init__(self): + """Initializer.""" + super(GenISOImage, self).__init__( + "genisoimage", + version_regexp="genisoimage ([0-9.]+)") + + +class XorrISO(Helper): + """Helper provider for ``xorriso``. + + https://www.gnu.org/software/xorriso/ """ + _provider_packages = { + 'apt-get': 'xorriso', + } + def __init__(self): """Initializer.""" - super(MkIsoFS, self).__init__( - "mkisofs", - version_regexp="(?:mkisofs|genisoimage|xorriso) ([0-9.]+)") - - @property - def name(self): - """Either mkisofs, genisoimage, or xorriso depending on environment.""" - if not self._path: - self._path = self.find_executable("mkisofs") - if self._path: - self._name = "mkisofs" - if not self._path: - self._path = self.find_executable("genisoimage") - if self._path: - self._name = "genisoimage" - if not self._path: - self._path = self.find_executable("xorriso") - if self._path: - self._name = "xorriso" - return self._name - - @property - def path(self): - """Find ``mkisofs``, ``genisoimage``, or ``xorriso`` if available.""" - assert self.name - return self._path - - def install_helper(self): - """Install ``mkisofs``, ``genisoimage``, or ``xorriso``.""" - if self.should_not_be_installed_but_is(): - return - logger.info("Installing 'mkisofs' and/or 'genisoimage'...") - self._name = None - if Helper.port_install('cdrtools'): - self._name = 'mkisofs' - elif Helper.yum_install('genisoimage'): - self._name = "genisoimage" - else: - try: - if Helper.apt_install('genisoimage'): - self._name = "genisoimage" - except HelperError: - pass - - if not self._name: - if Helper.apt_install('xorriso'): - self._name = "xorriso" - else: - raise NotImplementedError( - "Unsure how to install mkisofs.\n" - "See http://cdrecord.org/") - logger.info("Successfully installed '%s'", self.name) - - def create_iso(self, file_path, contents, rock_ridge=True): - """Create a new ISO image at the requested location. - - :param str file_path: Desired location of new disk image - :param list contents: List of file paths to package into the created - image. - :param bool rock_ridge: Set to False to skip inclusion of Rock Ridge - extensions in the ISO. - """ - logger.info("Calling %s to create an ISO image", self.name) - # mkisofs and genisoimage take the same parameters, conveniently, - # while xorriso needs to be asked to pretend to be mkisofs - args = [] - if self.name == 'xorriso': - args += ['-as', 'mkisofs'] - args += ['-output', file_path, '-full-iso9660-filenames', - '-iso-level', '2', '-allow-lowercase'] - if rock_ridge: - args.append('-r') - args += contents - self.call_helper(args) + super(XorrISO, self).__init__( + "xorriso", + version_regexp="xorriso ([0-9.]+)") diff --git a/COT/helpers/ovftool.py b/COT/helpers/ovftool.py index 737c359..bbdb568 100644 --- a/COT/helpers/ovftool.py +++ b/COT/helpers/ovftool.py @@ -19,54 +19,38 @@ https://www.vmware.com/support/developer/ovf/ """ -import logging - from .helper import Helper -logger = logging.getLogger(__name__) - class OVFTool(Helper): """Helper provider for ``ovftool`` from VMware. https://www.vmware.com/support/developer/ovf/ - - **Methods** - - .. autosummary:: - :nosignatures: - - install_helper - validate_ovf """ def __init__(self): """Initializer.""" - super(OVFTool, self).__init__("ovftool", - version_regexp="ovftool ([0-9.]+)") - - def install_helper(self): - """Install ``ovftool``. + super(OVFTool, self).__init__( + "ovftool", + info_uri="https://www.vmware.com/support/developer/ovf/", + version_regexp="ovftool ([0-9.]+)") - :raise: :exc:`NotImplementedError` as VMware does not currently provide - any mechanism for automatic download of ovftool. - """ - if self.should_not_be_installed_but_is(): - return - raise NotImplementedError( - "No support for automated installation of ovftool, " - "as VMware requires a site login to download it.\n" - "See https://www.vmware.com/support/developer/ovf/") + @property + def installable(self): + """COT can't install ovftool because of VMware site restrictions.""" + return False - def validate_ovf(self, ovf_file): - """Use VMware's ``ovftool`` program to validate an OVF or OVA. + def install(self): + """Install the helper program. - This checks the file against the OVF standard and any VMware-specific - requirements. + :raise: :exc:`NotImplementedError` if not ``installable`` - :param str ovf_file: File to validate - :return: Output from ``ovftool`` - :raise HelperNotFoundError: if ``ovftool`` is not found. - :raise HelperError: if ``ovftool`` regards the file as invalid + We override the default install implementation to raise a more + detailed error message for ovftool. """ - return self.call_helper(['--schemaValidate', ovf_file]) + if not self.installed: + raise NotImplementedError( + "No support for automated installation of ovftool, as VMware " + "requires a site login to download it. See " + "https://www.vmware.com/support/developer/ovf/" + ) diff --git a/COT/helpers/port.py b/COT/helpers/port.py new file mode 100644 index 0000000..cc246ff --- /dev/null +++ b/COT/helpers/port.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# +# port.py - Wrapper for the MacPorts 'port' package manager. +# +# October 2016, Glenn F. Matthews +# Copyright (c) 2015-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. + +"""Wrapper for the MacPorts 'port' package manager.""" + +import logging + +from COT.helpers.helper import PackageManager + +logger = logging.getLogger(__name__) + + +class Port(PackageManager): + """The 'port' package manager utility.""" + + _updated = False + + def __init__(self): + """Initializer.""" + super(Port, self).__init__( + "port", + info_uri="https://www.macports.org/", + version_args=['version']) + + def install_package(self, package): + """Install the requested package if needed. + + :param str package: Name of the package to install. + """ + # Check for updates + if not Port._updated: + self.call(['selfupdate'], + capture_output=False, retry_with_sudo=True) + Port._updated = True + self.call(['install', package], + capture_output=False, retry_with_sudo=True) diff --git a/COT/helpers/qemu_img.py b/COT/helpers/qemu_img.py index 49f0e8f..a6e0fe9 100644 --- a/COT/helpers/qemu_img.py +++ b/COT/helpers/qemu_img.py @@ -19,158 +19,21 @@ http://www.qemu.org """ -import logging -import os.path -import re -from distutils.version import StrictVersion - -from .helper import Helper, guess_file_format_from_path - -logger = logging.getLogger(__name__) +from .helper import Helper class QEMUImg(Helper): - """Helper provider for ``qemu-img`` (http://www.qemu.org). - - **Methods** + """Helper provider for ``qemu-img`` (http://www.qemu.org).""" - .. autosummary:: - :nosignatures: - - install_helper - get_disk_format - get_disk_capacity - convert_disk_image - create_blank_disk - """ + _provider_packages = { + 'apt-get': 'qemu-utils', + 'port': 'qemu', + 'yum': 'qemu-img', + } def __init__(self): """Initializer.""" super(QEMUImg, self).__init__( "qemu-img", + info_uri="http://www.qemu.org", version_regexp="qemu-img version ([0-9.]+)") - self.vmdktool = None - - def install_helper(self): - """Install ``qemu-img``.""" - if self.should_not_be_installed_but_is(): - return - logger.info("Installing 'qemu-img'...") - if not (Helper.apt_install('qemu-utils') or - Helper.port_install('qemu') or - Helper.yum_install('qemu-img')): - raise NotImplementedError( - "Unsure how to install qemu-img.\n" - "See http://en.wikibooks.org/wiki/QEMU/Installing_QEMU") - logger.info("Successfully installed 'qemu-img'.") - - def get_disk_format(self, file_path): - """Get the major disk image format of the given file. - - .. warning:: - If :attr:`file_path` refers to a file which is not a disk image at - all, this function will return ``'raw'``. - - :param str file_path: Path to disk image file to inspect. - :return: Disk image format (``'vmdk'``, ``'raw'``, ``'qcow2'``, etc.) - """ - output = self.call_helper(['info', file_path]) - # Read the format from the output - match = re.search(r"file format: (\S*)", output) - if not match: - raise RuntimeError("Did not find file format string in " - "the output from qemu-img:\n{0}" - .format(output)) - file_format = match.group(1) - logger.info("File format of '%s' is '%s'", - os.path.basename(file_path), file_format) - return file_format - - def get_disk_capacity(self, file_path): - """Get the storage capacity of the given disk image. - - :param str file_path: Path to disk image file to inspect - :return: Disk capacity, in bytes - """ - output = self.call_helper(['info', file_path]) - match = re.search(r"(\d+) bytes", output) - if not match: - raise RuntimeError("Did not find byte count in the output from " - "qemu-img:\n{0}" - .format(output)) - capacity = match.group(1) - logger.verbose("Disk %s capacity is %s bytes", file_path, capacity) - return capacity - - def convert_disk_image(self, file_path, output_dir, - new_format, new_subformat=None): - """Convert the given disk image to the requested format/subformat. - - If the disk is already in this format then it is unchanged; - otherwise, will convert to a new disk in the specified output_dir - and return its path. - - Current supported conversions: - - * .vmdk (any format) to .vmdk (streamOptimized) - * .img to .vmdk (streamOptimized) - - :param str file_path: Disk image file to inspect/convert - :param str output_dir: Directory to place converted image into, if - needed - :param str new_format: Desired final format - :param str new_subformat: Desired final subformat - :return: - * :attr:`file_path`, if no conversion was required - * or a file path in :attr:`output_dir` containing the converted image - - :raise NotImplementedError: if the :attr:`new_format` and/or - :attr:`new_subformat` are not supported conversion targets. - """ - file_name = os.path.basename(file_path) - (file_string, _) = os.path.splitext(file_name) - - new_file_path = None - if new_format == 'raw': - new_file_path = os.path.join(output_dir, file_string + '.img') - logger.info("Invoking qemu-img to convert %s into raw image %s", - file_path, new_file_path) - self.call_helper(['convert', '-O', 'raw', - file_path, new_file_path]) - elif new_format == 'vmdk' and new_subformat == 'streamOptimized': - if self.version >= StrictVersion("2.1.0"): - new_file_path = os.path.join(output_dir, file_string + '.vmdk') - # qemu-img finally supports streamOptimized - yay! - logger.info("Invoking qemu-img to convert %s to " - "streamOptimized VMDK %s", - file_path, new_file_path) - self.call_helper(['convert', '-O', 'vmdk', - '-o', 'subformat=streamOptimized', - file_path, new_file_path]) - else: - raise NotImplementedError("qemu-img is unable to convert to " - "stream-optimized VMDK format prior " - "to version 2.1.0 - you have {0}" - .format(self.version)) - else: - raise NotImplementedError("No support for converting disk image " - "to format {0} / subformat {1}" - .format(new_format, new_subformat)) - - return new_file_path - - def create_blank_disk(self, file_path, capacity, file_format=None): - """Create an unformatted disk image at the requested location. - - :param str file_path: Desired location of new disk image - :param capacity: Disk capacity. A string like '16M' or '1G'. - :param str file_format: Desired image format (if not specified, this - will be derived from the file extension of :attr:`file_path`) - """ - if not file_format: - file_format = guess_file_format_from_path(file_path) - - logger.info("Calling qemu-img to create %s %s image", - capacity, file_format) - self.call_helper(['create', '-f', file_format, - file_path, capacity]) diff --git a/COT/helpers/tests/test_api.py b/COT/helpers/tests/test_api.py deleted file mode 100644 index bdcafc7..0000000 --- a/COT/helpers/tests/test_api.py +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/env python -# -# test_api.py - Unit test cases for COT.helpers.api module. -# -# April 2014, 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 COT.helpers.api module.""" - -import os -import logging - -from distutils.version import StrictVersion -import mock - -from COT.tests.ut import COT_UT -from COT.helpers import ( - create_disk_image, convert_disk_image, get_disk_format, - get_disk_capacity, get_disk_file_listing, create_install_dir, install_file, - HelperError, HelperNotFoundError, -) -from COT.helpers.api import ISOINFO - -logger = logging.getLogger(__name__) - - -class TestGetDiskFormat(COT_UT): - """Test cases for get_disk_format() function.""" - - def test_get_disk_format(self): - """Get format and subformat of various disk images.""" - # First, tests that just use qemu-img - try: - temp_disk = os.path.join(self.temp_dir, 'foo.img') - create_disk_image(temp_disk, capacity="16M") - (f, sf) = get_disk_format(temp_disk) - self.assertEqual(f, 'raw') - self.assertEqual(sf, None) - - temp_disk = os.path.join(self.temp_dir, 'foo.qcow2') - create_disk_image(temp_disk, capacity="1G") - (f, sf) = get_disk_format(temp_disk) - self.assertEqual(f, 'qcow2') - self.assertEqual(sf, None) - except HelperNotFoundError as e: - self.fail(e.strerror) - - # Now a test that uses both qemu-img and file inspection - try: - (f, sf) = get_disk_format(self.blank_vmdk) - self.assertEqual(f, 'vmdk') - self.assertEqual(sf, 'streamOptimized') - except HelperNotFoundError as e: - self.fail(e.strerror) - - def test_get_disk_format_no_file(self): - """Negative test - get_disk_format() for nonexistent file.""" - self.assertRaises(HelperError, get_disk_format, "") - self.assertRaises(HelperError, get_disk_format, "/foo/bar/baz") - - @mock.patch('COT.helpers.api.QEMUIMG.get_disk_format', - return_value='vmdk') - def test_bad_vmdk_header(self, _): - """Test corner case in VMDK subtype identification.""" - with self.assertRaises(RuntimeError) as cm: - get_disk_format(self.input_ovf) - self.assertRegex(cm.exception.args[0], - "Could not find VMDK 'createType' in the file header") - - -class TestConvertDiskImage(COT_UT): - """Test cases for convert_disk_image().""" - - def test_convert_no_work_needed(self): - """Convert a disk to its own format.""" - try: - new_disk_path = convert_disk_image(self.blank_vmdk, self.temp_dir, - 'vmdk', 'streamOptimized') - # No change -> don't create a new disk but just return existing. - self.assertEqual(new_disk_path, self.blank_vmdk) - except HelperNotFoundError as e: - self.fail(e.strerror) - - def raw_to_vmdk_stream_optimized_test(self): - """Test conversion of raw to vmdk streamOptimized.""" - temp_disk = os.path.join(self.temp_dir, "foo.img") - try: - create_disk_image(temp_disk, capacity="16M") - except HelperNotFoundError as e: - self.fail(e.strerror) - try: - new_disk_path = convert_disk_image(temp_disk, self.temp_dir, - 'vmdk', 'streamOptimized') - except HelperNotFoundError as e: - self.fail(e.strerror) - - (f, sf) = get_disk_format(new_disk_path) - self.assertEqual(f, 'vmdk') - self.assertEqual(sf, 'streamOptimized') - - def vmdk_to_vmdk_stream_optimized_test(self): - """Test conversion of unoptimized vmdk to streamOptimized.""" - temp_disk = os.path.join(self.temp_dir, "foo.vmdk") - create_disk_image(temp_disk, capacity="16M") - new_disk_path = convert_disk_image(temp_disk, self.temp_dir, - 'vmdk', 'streamOptimized') - (f, sf) = get_disk_format(new_disk_path) - self.assertEqual(f, 'vmdk') - self.assertEqual(sf, 'streamOptimized') - - def qcow2_to_vmdk_stream_optimized_test(self): - """Test conversion of qcow2 to vmdk streamOptimized.""" - try: - temp_disk = os.path.join(self.temp_dir, "foo.qcow2") - create_disk_image(temp_disk, capacity="16M") - new_disk_path = convert_disk_image(temp_disk, self.temp_dir, - 'vmdk', 'streamOptimized') - self.assertEqual(new_disk_path, - os.path.join(self.temp_dir, "foo.vmdk")) - (f, sf) = get_disk_format(new_disk_path) - self.assertEqual(f, 'vmdk') - self.assertEqual(sf, 'streamOptimized') - except HelperNotFoundError as e: - self.fail(e.strerror) - - @mock.patch('COT.helpers.qemu_img.QEMUImg.version', - new_callable=mock.PropertyMock, - return_value=StrictVersion("1.0.0")) - def test_disk_conversion_old_qemu(self, _): - """Test disk conversion flows with older qemu-img version.""" - self.raw_to_vmdk_stream_optimized_test() - self.vmdk_to_vmdk_stream_optimized_test() - self.qcow2_to_vmdk_stream_optimized_test() - - @mock.patch('COT.helpers.qemu_img.QEMUImg.version', - new_callable=mock.PropertyMock, - return_value=StrictVersion("2.1.0")) - def test_disk_conversion_new_qemu(self, _): - """Test disk conversion flows with newer qemu-img version.""" - self.raw_to_vmdk_stream_optimized_test() - self.vmdk_to_vmdk_stream_optimized_test() - self.qcow2_to_vmdk_stream_optimized_test() - - def test_convert_to_raw(self): - """No support for converting VMDK to RAW at present.""" - self.assertRaises(NotImplementedError, - convert_disk_image, - self.blank_vmdk, self.temp_dir, 'raw', None) - - -class TestCreateDiskImage(COT_UT): - """Test cases for create_disk_image().""" - - def test_create_invalid(self): - """Invalid arguments.""" - # Must specify contents or capacity - self.assertRaises(RuntimeError, - create_disk_image, - os.path.join(self.temp_dir, "out.iso")) - # If extension not given, cannot guess file format - self.assertRaises(RuntimeError, - create_disk_image, - os.path.join(self.temp_dir, "out"), - capacity="1M") - # Trying to create a VHD format image, not currently possible - self.assertRaises(HelperError, - create_disk_image, - os.path.join(self.temp_dir, "out.vhd"), - capacity="1M") - self.assertRaises(HelperError, - create_disk_image, - os.path.join(self.temp_dir, "out.vmdk"), - file_format="vhd", - capacity="1M") - # Don't know how to populate a qcow2 image with a file - self.assertRaises(NotImplementedError, - create_disk_image, - os.path.join(self.temp_dir, "out.vmdk"), - file_format="qcow2", - contents=[self.input_ovf]) - - def test_create_iso_with_contents(self): - """Creation of ISO image containing files.""" - disk_path = os.path.join(self.temp_dir, "out.iso") - try: - create_disk_image(disk_path, contents=[self.input_ovf]) - except HelperNotFoundError as e: - self.fail(e.strerror) - if ISOINFO.path: - # Check contents - self.assertEqual(get_disk_file_listing(disk_path), - [os.path.basename(self.input_ovf)]) - else: - logger.info("isoinfo not available, not checking disk contents") - - # Creation of empty disks is tested implicitly in other test classes - # above - no need to repeat that here - - def test_create_raw_with_contents(self): - """Creation of raw disk image containing files.""" - disk_path = os.path.join(self.temp_dir, "out.img") - try: - create_disk_image(disk_path, contents=[self.input_ovf]) - except HelperNotFoundError as e: - self.fail(e.strerror) - (f, sf) = get_disk_format(disk_path) - self.assertEqual(f, 'raw') - self.assertEqual(sf, None) - try: - capacity = get_disk_capacity(disk_path) - self.assertEqual(capacity, "8388608") - except HelperNotFoundError as e: - self.fail(e.strerror) - if ISOINFO.path: - self.assertEqual(get_disk_file_listing(disk_path), - [os.path.basename(self.input_ovf)]) - else: - logger.info("isoinfo not available, not checking disk contents") - - def test_create_raw_with_contents_and_size(self): - """Creation of raw disk image of a specified size with files.""" - disk_path = os.path.join(self.temp_dir, "out.img") - try: - create_disk_image(disk_path, contents=[self.input_ovf], - capacity="64M") - except HelperNotFoundError as e: - self.fail(e.strerror) - (f, sf) = get_disk_format(disk_path) - self.assertEqual(f, 'raw') - self.assertEqual(sf, None) - try: - capacity = get_disk_capacity(disk_path) - self.assertEqual(capacity, "67108864") - except HelperNotFoundError as e: - self.fail(e.strerror) - if ISOINFO.path: - self.assertEqual(get_disk_file_listing(disk_path), - [os.path.basename(self.input_ovf)]) - else: - logger.info("isoinfo not available, not checking disk contents") - - -@mock.patch('COT.helpers.helper.Helper._check_call') -@mock.patch('os.makedirs') -@mock.patch('os.path.exists', return_value=False) -@mock.patch('os.path.isdir', return_value=False) -class TestCreateInstallDir(COT_UT): - """Test cases for create_install_dir().""" - - def test_already_exists(self, mock_isdir, mock_exists, - mock_makedirs, mock_check_call): - """Test case where the target directory already exists.""" - mock_isdir.return_value = True - self.assertTrue(create_install_dir('/foo/bar')) - mock_isdir.assert_called_with('/foo/bar') - mock_exists.assert_not_called() - mock_makedirs.assert_not_called() - mock_check_call.assert_not_called() - - def test_not_directory(self, mock_isdir, mock_exists, - mock_makedirs, mock_check_call): - """Test case where a file exists at the target path.""" - mock_exists.return_value = True - self.assertRaises(RuntimeError, create_install_dir, '/foo/bar') - mock_isdir.assert_called_with('/foo/bar') - mock_exists.assert_called_with('/foo/bar') - mock_makedirs.assert_not_called() - mock_check_call.assert_not_called() - - def test_permission_ok(self, mock_isdir, mock_exists, - mock_makedirs, mock_check_call): - """Successfully create directory with user permissions.""" - self.assertTrue(create_install_dir('/foo/bar')) - mock_isdir.assert_called_with('/foo/bar') - mock_exists.assert_called_with('/foo/bar') - mock_makedirs.assert_called_with('/foo/bar', 493) # 493 == 0o755 - mock_check_call.assert_not_called() - - def test_need_sudo(self, mock_isdir, mock_exists, - mock_makedirs, mock_check_call): - """Directory creation needs sudo.""" - mock_makedirs.side_effect = OSError - self.assertTrue(create_install_dir('/foo/bar')) - mock_isdir.assert_called_with('/foo/bar') - mock_exists.assert_called_with('/foo/bar') - mock_makedirs.assert_called_with('/foo/bar', 493) # 493 == 0o755 - mock_check_call.assert_called_with( - ['sudo', 'mkdir', '-p', '--mode=755', '/foo/bar']) - - def test_nondefault_permissions(self, mock_isdir, mock_exists, - mock_makedirs, mock_check_call): - """Non-default permissions should be applied whether sudo or not.""" - # Non-sudo case - self.assertTrue(create_install_dir('/foo/bar', 511)) # 511 == 0o777 - mock_isdir.assert_called_with('/foo/bar') - mock_exists.assert_called_with('/foo/bar') - mock_makedirs.assert_called_with('/foo/bar', 511) - mock_check_call.assert_not_called() - - # Sudo case - mock_makedirs.reset_mock() - mock_makedirs.side_effect = OSError - self.assertTrue(create_install_dir('/foo/bar', 511)) # 511 == 0o777 - mock_makedirs.assert_called_with('/foo/bar', 511) - mock_check_call.assert_called_with( - ['sudo', 'mkdir', '-p', '--mode=777', '/foo/bar']) - - -@mock.patch('COT.helpers.helper.Helper._check_call') -@mock.patch('shutil.copy') -class TestInstallFile(COT_UT): - """Test cases for install_file().""" - - def test_permission_ok(self, mock_copy, mock_check_call): - """File copy succeeds with user permissions.""" - self.assertTrue(install_file('/foo', '/bar')) - mock_copy.assert_called_with('/foo', '/bar') - mock_check_call.assert_not_called() - - def test_need_sudo(self, mock_copy, mock_check_call): - """File copy needs sudo.""" - mock_copy.side_effect = OSError - self.assertTrue(install_file('/foo', '/bar')) - mock_copy.assert_called_with('/foo', '/bar') - mock_check_call.assert_called_with(['sudo', 'cp', '/foo', '/bar']) diff --git a/COT/helpers/tests/test_fatdisk.py b/COT/helpers/tests/test_fatdisk.py index 76817b3..29ec0cc 100644 --- a/COT/helpers/tests/test_fatdisk.py +++ b/COT/helpers/tests/test_fatdisk.py @@ -25,9 +25,12 @@ from COT.helpers.tests.test_helper import HelperUT from COT.helpers.fatdisk import FatDisk +from COT.helpers import helpers logger = logging.getLogger(__name__) +# pylint: disable=protected-access + @mock.patch('COT.helpers.fatdisk.FatDisk.download_and_expand_tgz', side_effect=HelperUT.stub_download_and_expand_tgz) @@ -40,49 +43,45 @@ def setUp(self): self.maxDiff = None super(TestFatDisk, self).setUp() - @mock.patch('COT.helpers.helper.Helper._check_output', + @mock.patch('COT.helpers.helper.check_output', return_value="fatdisk, version 1.0.0-beta") def test_get_version(self, *_): """Validate .version getter.""" + self.helper._installed = True self.assertEqual(StrictVersion("1.0.0"), self.helper.version) - @mock.patch('COT.helpers.helper.Helper._check_output') + @mock.patch('COT.helpers.helper.check_output') @mock.patch('subprocess.check_call') - def test_install_helper_already_present(self, mock_check_call, - mock_check_output, *_): + def test_install_already_present(self, + mock_check_call, + mock_check_output, + *_): """Trying to re-install is a no-op.""" - self.helper.install_helper() + self.helper._installed = True + self.helper.install() mock_check_output.assert_not_called() mock_check_call.assert_not_called() - self.assertLogged(**self.ALREADY_INSTALLED) @mock.patch('platform.system', return_value='Linux') @mock.patch('os.path.isdir', return_value=False) @mock.patch('os.path.exists', return_value=False) @mock.patch('os.makedirs', side_effect=OSError) - @mock.patch('distutils.spawn.find_executable') + @mock.patch('distutils.spawn.find_executable', return_value="/foo") @mock.patch('shutil.copy', return_value=True) - @mock.patch('COT.helpers.helper.Helper._check_output', return_value="") + @mock.patch('COT.helpers.helper.check_output', return_value="") @mock.patch('subprocess.check_call') - def test_install_helper_apt_get(self, - mock_check_call, - mock_check_output, - mock_copy, - mock_find_executable, - *_): + def test_install_apt_get(self, + mock_check_call, + mock_check_output, + mock_copy, + *_): """Test installation via 'apt-get'.""" self.enable_apt_install() - mock_find_executable.side_effect = [ - None, # 'fatdisk', - None, # 'make', pre-installation - '/bin/make', # post-installation - None, # 'clang' - None, # 'gcc', pre-installation - None, # 'g++' - '/bin/gcc', # post-installation - ] - - self.helper.install_helper() + helpers['dpkg']._installed = True + for name in ['make', 'clang', 'gcc', 'g++']: + helpers[name]._installed = False + + self.helper.install() self.assertSubprocessCalls( mock_check_output, [ @@ -106,17 +105,14 @@ def test_install_helper_apt_get(self, mock_check_output.reset_mock() mock_check_call.reset_mock() mock_check_output.return_value = 'install ok installed' - mock_find_executable.reset_mock() - mock_find_executable.side_effect = [ - None, # fatdisk - None, # fakeout make not here - '/bin/make', # actually it is here! - '/bin/clang', - ] + # fakeout! + helpers['make']._installed = False + self.helper._installed = False + os.environ['PREFIX'] = '/opt/local' os.environ['DESTDIR'] = '/home/cot' - self.helper.install_helper() + self.helper.install() self.assertSubprocessCalls( mock_check_output, [ @@ -132,7 +128,7 @@ def test_install_helper_apt_get(self, self.assertTrue(re.search("/fatdisk$", mock_copy.call_args[0][0])) self.assertEqual('/home/cot/opt/local/bin', mock_copy.call_args[0][1]) - def test_install_helper_port(self, *_): + def test_install_port(self, *_): """Test installation via 'port'.""" self.port_install_test('fatdisk') @@ -140,27 +136,19 @@ def test_install_helper_port(self, *_): @mock.patch('os.path.isdir', return_value=False) @mock.patch('os.path.exists', return_value=False) @mock.patch('os.makedirs', side_effect=OSError) - @mock.patch('distutils.spawn.find_executable') + @mock.patch('distutils.spawn.find_executable', return_value='/foo') @mock.patch('shutil.copy', return_value=True) @mock.patch('subprocess.check_call') - def test_install_helper_yum(self, - mock_check_call, - mock_copy, - mock_find_executable, - *_): + def test_install_yum(self, + mock_check_call, + mock_copy, + *_): """Test installation via 'yum'.""" self.enable_yum_install() - mock_find_executable.side_effect = [ - None, # vmdktool - None, # make, pre-installation - '/bin/make', # post-installation - None, # 'clang' - None, # 'gcc', pre-installation - None, # 'g++' - '/bin/gcc', # post-installation - ] - - self.helper.install_helper() + for name in ['make', 'clang', 'gcc', 'g++']: + helpers[name]._installed = False + + self.helper.install() self.assertSubprocessCalls( mock_check_call, [ @@ -174,14 +162,16 @@ def test_install_helper_yum(self, @mock.patch('platform.system', return_value='Linux') @mock.patch('distutils.spawn.find_executable', return_value=None) - def test_install_helper_linux_need_make_no_package_manager(self, *_): + def test_install_linux_need_make_no_package_manager(self, *_): """Linux installation requires yum or apt-get if 'make' missing.""" self.select_package_manager(None) + for name in ['make', 'clang', 'gcc', 'g++']: + helpers[name]._installed = False with self.assertRaises(NotImplementedError): - self.helper.install_helper() + self.helper.install() @staticmethod - def _find_make_only(name): # pylint: disable=no-self-use + def _find_make_only(name): """Stub for distutils.spawn.find_executable - only finds 'make'.""" logger.info("stub_find_executable(%s)", name) if name == 'make': @@ -197,6 +187,14 @@ def test_install_linux_need_compiler_no_package_manager(self, *_): """Linux installation requires yum or apt-get if 'gcc' missing.""" self.select_package_manager(None) + for name in ['clang', 'gcc', 'g++']: + helpers[name]._installed = False mock_find_exec.side_effect = self._find_make_only with self.assertRaises(NotImplementedError): - self.helper.install_helper() + self.helper.install() + + @mock.patch('platform.system', return_value='Darwin') + def test_install_helper_mac_no_package_manager(self, *_): + """Mac installation requires port.""" + self.select_package_manager(None) + self.assertRaises(NotImplementedError, self.helper.install) diff --git a/COT/helpers/tests/test_genisoimage.py b/COT/helpers/tests/test_genisoimage.py new file mode 100644 index 0000000..449c91c --- /dev/null +++ b/COT/helpers/tests/test_genisoimage.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# +# test_genisoimage.py - Unit test cases for GenISOImage class. +# +# 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 GenISOImage class.""" + +import logging + +from distutils.version import StrictVersion +import mock + +from COT.helpers.tests.test_helper import HelperUT +from COT.helpers.mkisofs import GenISOImage + +logger = logging.getLogger(__name__) + +# pylint: disable=protected-access + + +class TestGenISOImage(HelperUT): + """Test cases for GenISOImage helper class.""" + + def setUp(self): + """Test case setup function called automatically prior to each test.""" + self.helper = GenISOImage() + super(TestGenISOImage, self).setUp() + + @mock.patch('COT.helpers.helper.check_output', + return_value="genisoimage 1.1.11 (Linux)") + def test_get_version(self, _): + """Test .version getter logic for genisoimage.""" + self.helper._installed = True + self.assertEqual(StrictVersion("1.1.11"), self.helper.version) + + @mock.patch('COT.helpers.helper.check_output') + @mock.patch('subprocess.check_call') + def test_install_already_present(self, mock_check_call, mock_check_output): + """Don't re-install if already installed.""" + self.helper._installed = True + self.helper.install() + mock_check_output.assert_not_called() + mock_check_call.assert_not_called() + + def test_install_helper_apt_get(self): + """Test installation via 'apt-get' of genisoimage.""" + self.apt_install_test('genisoimage', 'genisoimage') diff --git a/COT/helpers/tests/test_helper.py b/COT/helpers/tests/test_helper.py index 2624e10..562cd22 100644 --- a/COT/helpers/tests/test_helper.py +++ b/COT/helpers/tests/test_helper.py @@ -24,22 +24,25 @@ import requests import mock +from COT.ui_shared import UI from COT.tests.ut import COT_UT -from COT.helpers.helper import Helper, TemporaryDirectory -from COT.helpers import HelperError, HelperNotFoundError +from COT.helpers.helper import TemporaryDirectory, check_call, check_output +from COT.helpers import ( + Helper, + HelperError, HelperNotFoundError, + helpers, package_managers, +) +from COT.helpers.port import Port +from COT.helpers.apt_get import AptGet logger = logging.getLogger(__name__) +# pylint: disable=protected-access + class HelperUT(COT_UT): """Generic class for testing Helper and subclasses thereof.""" - # commonly seen logger message for helpers - ALREADY_INSTALLED = { - 'levelname': 'WARNING', - 'msg': "Tried to install .* but it's already available .*", - } - def __init__(self, method_name='runTest'): """Add helper instance variable.""" self.helper = None @@ -55,21 +58,25 @@ def assertSubprocessCalls(self, mock_function, args_list): # noqa: N802 # and for the subprocess.check_[call|output] methods # we are testing here, # call_args_list[i][0][0] is the ith subprocess args. - [a[0][0] for a in mock_function.call_args_list]) + [a[0][0] for a in mock_function.call_args_list], + "\nExpected: {0}\nGot: {1}".format( + args_list, + [a[0][0] for a in mock_function.call_args_list])) def set_helper_version(self, ver): """Override the version number of the helper class.""" - self.helper._version = ver # pylint: disable=protected-access + self.helper._version = ver - def select_package_manager(self, name): # pylint: disable=no-self-use + @staticmethod + def select_package_manager(name): """Select the specified installer program for Helper to use.""" - for pm in Helper.PACKAGE_MANAGERS: - Helper.PACKAGE_MANAGERS[pm] = (pm == name) + for pm_name in package_managers: + package_managers[pm_name]._installed = (pm_name == name) def enable_apt_install(self): """Set flags and values to force an apt-get update and apt install.""" self.select_package_manager('apt-get') - Helper._apt_updated = False # pylint: disable=protected-access + AptGet._updated = False os.environ['PREFIX'] = '/usr/local' if 'DESTDIR' in os.environ: del os.environ['DESTDIR'] @@ -82,22 +89,23 @@ def enable_yum_install(self): del os.environ['DESTDIR'] def assertAptUpdated(self): # noqa: N802 - """Assert that the hidden _apt_updated flag is set.""" - # pylint: disable=protected-access - self.assertTrue(Helper._apt_updated) + """Assert that the hidden AptGet._updated flag is set.""" + self.assertTrue(AptGet._updated) @mock.patch('distutils.spawn.find_executable', return_value=None) def apt_install_test(self, pkgname, helpername, *_): """Test installation with 'dpkg' and 'apt-get'.""" + helpers['dpkg']._installed = True # Python 2.6 doesn't let us do multiple mocks in one 'with' - with mock.patch.object(self.helper, '_path', new=None): + with mock.patch.object(self.helper, '_path') as mock_path: with mock.patch('subprocess.check_call') as mock_check_call: with mock.patch( - 'COT.helpers.helper.Helper._check_output', - return_value="is not installed and no" + 'COT.helpers.helper.check_output', + return_value="is not installed and no " "information is available") as mock_check_output: + mock_path.return_value = (None, '/bin/' + helpername) self.enable_apt_install() - self.helper.install_helper() + self.helper.install() self.assertSubprocessCalls(mock_check_output, [['dpkg', '-s', pkgname]]) self.assertSubprocessCalls( @@ -111,7 +119,7 @@ def apt_install_test(self, pkgname, helpername, *_): # Make sure we don't 'apt-get update' again unnecessarily mock_check_call.reset_mock() mock_check_output.reset_mock() - self.helper.install_helper() + self.helper.install() self.assertSubprocessCalls(mock_check_output, [['dpkg', '-s', pkgname]]) self.assertSubprocessCalls( @@ -121,34 +129,33 @@ def apt_install_test(self, pkgname, helpername, *_): @mock.patch('distutils.spawn.find_executable', return_value=None) def port_install_test(self, portname, *_): """Test installation with 'port'.""" - # pylint: disable=protected-access self.select_package_manager('port') - Helper._port_updated = False + Port._updated = False # Python 2.6 doesn't let us use multiple contexts in one 'with' with mock.patch('subprocess.check_call') as mock_check_call: - with mock.patch.object(self.helper, '_path', new=None): - self.helper.install_helper() + with mock.patch.object(self.helper, '_path') as mock_path: + mock_path.return_value = (None, '/bin/' + portname) + self.helper.install() self.assertSubprocessCalls( mock_check_call, [['port', 'selfupdate'], ['port', 'install', portname]]) - self.assertTrue(Helper._port_updated) + self.assertTrue(Port._updated) # Make sure we don't call port selfupdate again unnecessarily mock_check_call.reset_mock() - self.helper.install_helper() + self.helper.install() self.assertSubprocessCalls( mock_check_call, [['port', 'install', portname]]) - @mock.patch('distutils.spawn.find_executable', return_value=None) def yum_install_test(self, pkgname, *_): """Test installation with yum.""" self.enable_yum_install() + self.helper._installed = False with mock.patch('subprocess.check_call') as mock_check_call: - with mock.patch.object(self.helper, '_path', new=None): - self.helper.install_helper() - mock_check_call.assert_called_with( - ['yum', '--quiet', 'install', pkgname]) + self.helper.install() + mock_check_call.assert_called_with( + ['yum', '--quiet', 'install', pkgname]) @staticmethod @contextlib.contextmanager @@ -162,28 +169,26 @@ def setUp(self): # subclass needs to set self.helper super(HelperUT, self).setUp() if self.helper: - self.helper._path = None # pylint: disable=protected-access - # save some environment properties for sanity - self._port = Helper.PACKAGE_MANAGERS['port'] - self._apt_get = Helper.PACKAGE_MANAGERS['apt-get'] - self._yum = Helper.PACKAGE_MANAGERS['yum'] + self.helper._path = None + self.helper._installed = False def tearDown(self): """Test case cleanup function called automatically after each test.""" - Helper.PACKAGE_MANAGERS['port'] = self._port - Helper.PACKAGE_MANAGERS['apt-get'] = self._apt_get - Helper.PACKAGE_MANAGERS['yum'] = self._yum + for helper in helpers.values(): + helper._installed = None + helper._path = None + helper._version = None super(HelperUT, self).tearDown() @mock.patch('distutils.spawn.find_executable', return_value=None) @mock.patch('platform.system', return_value='Windows') - def test_install_helper_unsupported(self, *_): + def test_install_unsupported(self, *_): """Unable to install without a package manager.""" self.select_package_manager(None) if self.helper: with mock.patch.object(self.helper, '_path', new=None): self.assertRaises(NotImplementedError, - self.helper.install_helper) + self.helper.install) class HelperGenericTest(HelperUT): @@ -196,21 +201,19 @@ def setUp(self): def test_check_call_helpernotfounderror(self): """HelperNotFoundError if executable doesn't exist.""" - # pylint: disable=protected-access self.assertRaises(HelperNotFoundError, - Helper._check_call, ["not_a_command"]) + check_call, ["not_a_command"]) self.assertRaises(HelperNotFoundError, - Helper._check_call, + check_call, ["not_a_command"], require_success=True) def test_check_call_helpererror(self): """HelperError if executable fails and require_success is set.""" - # pylint: disable=protected-access with self.assertRaises(HelperError) as cm: - Helper._check_call(["false"]) + check_call(["false"]) self.assertEqual(cm.exception.errno, 1) - Helper._check_call(["false"], require_success=False) + check_call(["false"], require_success=False) @mock.patch('subprocess.check_call') def test_check_call_permissions_needed(self, mock_check_call): @@ -222,17 +225,15 @@ def raise_oserror(args, **_): return mock_check_call.side_effect = raise_oserror - # pylint: disable=protected-access - # Without retry_on_sudo, we reraise the permissions error with self.assertRaises(OSError) as cm: - Helper._check_call(["false"]) + check_call(["false"]) self.assertEqual(cm.exception.errno, 13) mock_check_call.assert_called_once_with(["false"]) # With retry_on_sudo, we retry. mock_check_call.reset_mock() - Helper._check_call(["false"], retry_with_sudo=True) + check_call(["false"], retry_with_sudo=True) mock_check_call.assert_has_calls([ mock.call(['false']), mock.call(['sudo', 'false']), @@ -250,13 +251,13 @@ def raise_subprocess_error(args, **_): # Without retry_on_sudo, we reraise the permissions error with self.assertRaises(HelperError) as cm: - Helper._check_call(["false"]) + check_call(["false"]) self.assertEqual(cm.exception.errno, 1) mock_check_call.assert_called_once_with(["false"]) # With retry_on_sudo, we retry. mock_check_call.reset_mock() - Helper._check_call(["false"], retry_with_sudo=True) + check_call(["false"], retry_with_sudo=True) mock_check_call.assert_has_calls([ mock.call(['false']), mock.call(['sudo', 'false']), @@ -264,50 +265,53 @@ def raise_subprocess_error(args, **_): def test_check_output_helpernotfounderror(self): """HelperNotFoundError if executable doesn't exist.""" - # pylint: disable=protected-access self.assertRaises(HelperNotFoundError, - Helper._check_output, ["not_a_command"]) + check_output, ["not_a_command"]) self.assertRaises(HelperNotFoundError, - Helper._check_output, ["not_a_command"], + check_output, ["not_a_command"], require_success=True) def test_check_output_oserror(self): """OSError if requested command isn't an executable.""" - # pylint: disable=protected-access self.assertRaises(OSError, - Helper._check_output, self.input_ovf) + check_output, self.input_ovf) def test_check_output_helpererror(self): """HelperError if executable fails and require_success is set.""" - # pylint: disable=protected-access with self.assertRaises(HelperError) as cm: - Helper._check_output(["false"]) + check_output(["false"]) self.assertEqual(cm.exception.errno, 1) - Helper._check_output(["false"], require_success=False) + check_output(["false"], require_success=False) @mock.patch('distutils.spawn.find_executable', return_value=None) def test_helper_not_found(self, *_): """Make sure helper.path is None if find_executable fails.""" self.assertEqual(self.helper.path, None) - def test_install_helper_already_present(self): - """Make sure a warning is logged when attempting to re-install.""" - self.helper._path = True # pylint: disable=protected-access - self.helper.install_helper() - self.assertLogged(**self.ALREADY_INSTALLED) + @mock.patch('COT.helpers.Helper._install') + def test_install_already_present(self, mock_install): + """Make installation is not attempted unnecessarily.""" + self.helper._installed = True + self.helper.install() + mock_install.assert_not_called() - def test_call_helper_install(self): - """call_helper will call install_helper, which raises an error.""" + def test_call_install(self): + """call will call install, which raises an error.""" self.assertRaises(NotImplementedError, - self.helper.call_helper, ["Hello!"]) + self.helper.call, ["Hello!"]) - @mock.patch('COT.helpers.helper.Helper.confirm', return_value=False) - def test_call_helper_no_install(self, *_): + def test_call_no_install(self): """If not installed, and user declines, raise HelperNotFoundError.""" - self.assertRaises(HelperNotFoundError, - self.helper.call_helper, ["Hello!"]) + _ui = Helper.UI + Helper.UI = UI() + Helper.UI.default_confirm_response = False + try: + self.assertRaises(HelperNotFoundError, + self.helper.call, ["Hello!"]) + finally: + Helper.UI = _ui def test_download_and_expand_tgz(self): """Validate the download_and_expand_tgz() context_manager.""" @@ -330,3 +334,88 @@ def test_download_and_expand_tgz(self): self.fail("ConnectionError when trying to download from GitHub") # Temporary directory should be cleaned up when done self.assertFalse(os.path.exists(directory)) + + +@mock.patch('COT.helpers.helper.check_call') +@mock.patch('os.makedirs') +@mock.patch('os.path.exists', return_value=False) +@mock.patch('os.path.isdir', return_value=False) +class TestHelperMkDir(COT_UT): + """Test cases for Helper.mkdir().""" + + def test_already_exists(self, mock_isdir, mock_exists, + mock_makedirs, mock_check_call): + """Test case where the target directory already exists.""" + mock_isdir.return_value = True + self.assertTrue(Helper.mkdir('/foo/bar')) + mock_isdir.assert_called_with('/foo/bar') + mock_exists.assert_not_called() + mock_makedirs.assert_not_called() + mock_check_call.assert_not_called() + + def test_not_directory(self, mock_isdir, mock_exists, + mock_makedirs, mock_check_call): + """Test case where a file exists at the target path.""" + mock_exists.return_value = True + self.assertRaises(RuntimeError, Helper.mkdir, '/foo/bar') + mock_isdir.assert_called_with('/foo/bar') + mock_exists.assert_called_with('/foo/bar') + mock_makedirs.assert_not_called() + mock_check_call.assert_not_called() + + def test_permission_ok(self, mock_isdir, mock_exists, + mock_makedirs, mock_check_call): + """Successfully create directory with user permissions.""" + self.assertTrue(Helper.mkdir('/foo/bar')) + mock_isdir.assert_called_with('/foo/bar') + mock_exists.assert_called_with('/foo/bar') + mock_makedirs.assert_called_with('/foo/bar', 493) # 493 == 0o755 + mock_check_call.assert_not_called() + + def test_need_sudo(self, mock_isdir, mock_exists, + mock_makedirs, mock_check_call): + """Directory creation needs sudo.""" + mock_makedirs.side_effect = OSError + self.assertTrue(Helper.mkdir('/foo/bar')) + mock_isdir.assert_called_with('/foo/bar') + mock_exists.assert_called_with('/foo/bar') + mock_makedirs.assert_called_with('/foo/bar', 493) # 493 == 0o755 + mock_check_call.assert_called_with( + ['sudo', 'mkdir', '-p', '--mode=755', '/foo/bar']) + + def test_nondefault_permissions(self, mock_isdir, mock_exists, + mock_makedirs, mock_check_call): + """Non-default permissions should be applied whether sudo or not.""" + # Non-sudo case + self.assertTrue(Helper.mkdir('/foo/bar', 511)) # 511 == 0o777 + mock_isdir.assert_called_with('/foo/bar') + mock_exists.assert_called_with('/foo/bar') + mock_makedirs.assert_called_with('/foo/bar', 511) + mock_check_call.assert_not_called() + + # Sudo case + mock_makedirs.reset_mock() + mock_makedirs.side_effect = OSError + self.assertTrue(Helper.mkdir('/foo/bar', 511)) # 511 == 0o777 + mock_makedirs.assert_called_with('/foo/bar', 511) + mock_check_call.assert_called_with( + ['sudo', 'mkdir', '-p', '--mode=777', '/foo/bar']) + + +@mock.patch('COT.helpers.helper.check_call') +@mock.patch('shutil.copy') +class TestHelperCp(COT_UT): + """Test cases for Helper.cp().""" + + def test_permission_ok(self, mock_copy, mock_check_call): + """File copy succeeds with user permissions.""" + self.assertTrue(Helper.cp('/foo', '/bar')) + mock_copy.assert_called_with('/foo', '/bar') + mock_check_call.assert_not_called() + + def test_need_sudo(self, mock_copy, mock_check_call): + """File copy needs sudo.""" + mock_copy.side_effect = OSError + self.assertTrue(Helper.cp('/foo', '/bar')) + mock_copy.assert_called_with('/foo', '/bar') + mock_check_call.assert_called_with(['sudo', 'cp', '/foo', '/bar']) diff --git a/COT/helpers/tests/test_mkisofs.py b/COT/helpers/tests/test_mkisofs.py index 7e60264..6b0fa94 100644 --- a/COT/helpers/tests/test_mkisofs.py +++ b/COT/helpers/tests/test_mkisofs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # coding=utf-8 # -# test_mkisofs.py - Unit test cases for COT.helpers.mkisofs submodule. +# test_mkisofs.py - Unit test cases for MkISOFS helper class. # # March 2015, Glenn F. Matthews # Copyright (c) 2014-2016 the COT project developers. @@ -15,178 +15,47 @@ # 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 COT.helpers.mkisofs submodule.""" +"""Unit test cases for the MkISOFS helper class.""" import logging -import subprocess from distutils.version import StrictVersion -import os import mock from COT.helpers.tests.test_helper import HelperUT -from COT.helpers.mkisofs import MkIsoFS -from COT.helpers.api import get_disk_format, get_disk_file_listing, ISOINFO +from COT.helpers.mkisofs import MkISOFS logger = logging.getLogger(__name__) +# pylint: disable=protected-access -class TestMkIsoFS(HelperUT): - """Test cases for MkIsoFS helper class.""" + +class TestMkISOFS(HelperUT): + """Test cases for MkISOFS helper class.""" def setUp(self): """Test case setup function called automatically prior to each test.""" - self.helper = MkIsoFS() - super(TestMkIsoFS, self).setUp() + self.helper = MkISOFS() + super(TestMkISOFS, self).setUp() - @mock.patch('COT.helpers.helper.Helper._check_output', + @mock.patch('COT.helpers.helper.check_output', return_value=("mkisofs 3.00 (--) Copyright (C) 1993-1997 " "Eric Youngdale (C) 1997-2010 Jörg Schilling")) def test_get_version_mkisofs(self, _): """Test .version getter logic for mkisofs.""" + self.helper._installed = True self.assertEqual(StrictVersion("3.0"), self.helper.version) - @mock.patch('COT.helpers.helper.Helper._check_output', - return_value="genisoimage 1.1.11 (Linux)") - def test_get_version_genisoimage(self, _): - """Test .version getter logic for genisoimage.""" - self.assertEqual(StrictVersion("1.1.11"), self.helper.version) - - @mock.patch('COT.helpers.helper.Helper._check_output', return_value=""" -xorriso 1.3.2 : RockRidge filesystem manipulator, libburnia project. - -xorriso 1.3.2 -ISO 9660 Rock Ridge filesystem manipulator and CD/DVD/BD burn program -Copyright (C) 2013, Thomas Schmitt , libburnia project. -xorriso version : 1.3.2 -Version timestamp : 2013.08.07.110001 -Build timestamp : -none-given- -libisofs in use : 1.3.4 (min. 1.3.2) -libjte in use : 1.0.0 (min. 1.0.0) -libburn in use : 1.3.4 (min. 1.3.4) -libburn OS adapter: internal GNU/Linux SG_IO adapter sg-linux -libisoburn in use : 1.3.2 (min. 1.3.2) -Provided under GNU GPL version 2 or later. -There is NO WARRANTY, to the extent permitted by law. -""") - def test_get_version_xorriso(self, _): - """Test .version getter logic for xorriso.""" - self.assertEqual(StrictVersion("1.3.2"), self.helper.version) - - @mock.patch('distutils.spawn.find_executable') - @mock.patch("COT.helpers.mkisofs.MkIsoFS.call_helper") - def test_find_mkisofs(self, mock_call_helper, mock_find_executable): - """If mkisofs is found, use it.""" - def find_one(name): - """Find mkisofs but no other.""" - if name == "mkisofs": - return "/mkisofs" - return None - mock_find_executable.side_effect = find_one - self.assertEqual("mkisofs", self.helper.name) - self.assertEqual(self.helper.path, "/mkisofs") - - self.helper.create_iso('foo.iso', [self.input_ovf]) - mock_call_helper.assert_called_with( - ['-output', 'foo.iso', '-full-iso9660-filenames', - '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) - - @mock.patch('distutils.spawn.find_executable') - @mock.patch("COT.helpers.mkisofs.MkIsoFS.call_helper") - def test_find_genisoimage(self, mock_call_helper, mock_find_executable): - """If mkisofs is not found, but genisoimage is, use that.""" - def find_one(name): - """Find genisoimage but no other.""" - if name == "genisoimage": - return "/genisoimage" - return None - mock_find_executable.side_effect = find_one - self.assertEqual("genisoimage", self.helper.name) - self.assertEqual(self.helper.path, "/genisoimage") - - self.helper.create_iso('foo.iso', [self.input_ovf]) - mock_call_helper.assert_called_with( - ['-output', 'foo.iso', '-full-iso9660-filenames', - '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) - - @mock.patch('distutils.spawn.find_executable') - @mock.patch("COT.helpers.mkisofs.MkIsoFS.call_helper") - def test_find_xorriso(self, mock_call_helper, mock_find_executable): - """If mkisofs and genisoimage are not found, but xorriso is, use it.""" - def find_one(name): - """Find xorriso but no other.""" - if name == "xorriso": - return "/xorriso" - return None - mock_find_executable.side_effect = find_one - self.assertEqual("xorriso", self.helper.name) - self.assertEqual(self.helper.path, "/xorriso") - - self.helper.create_iso('foo.iso', [self.input_ovf]) - mock_call_helper.assert_called_with( - ['-as', 'mkisofs', '-output', 'foo.iso', '-full-iso9660-filenames', - '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) - - @mock.patch('COT.helpers.helper.Helper._check_output') + @mock.patch('COT.helpers.helper.check_output') @mock.patch('subprocess.check_call') def test_install_helper_already_present(self, mock_check_call, mock_check_output): """Don't re-install if already installed.""" - self.helper.install_helper() + self.helper._installed = True + self.helper.install() mock_check_output.assert_not_called() mock_check_call.assert_not_called() - self.assertLogged(**self.ALREADY_INSTALLED) def test_install_helper_port(self): """Test installation via 'port'.""" self.port_install_test('cdrtools') - - def test_install_helper_apt_get(self): - """Test installation via 'apt-get' of genisoimage.""" - self.apt_install_test('genisoimage', 'genisoimage') - - @mock.patch('distutils.spawn.find_executable', return_value=None) - @mock.patch('subprocess.check_call') - @mock.patch( - 'COT.helpers.helper.Helper._check_output', - return_value="is not installed and no information is available" - ) - def test_install_helper_apt_get_xorriso(self, - mock_check_output, - mock_check_call, - *_): - """Test installation via 'apt-get' of xorriso.""" - self.enable_apt_install() - mock_check_call.side_effect = [ - None, # apt-get update - subprocess.CalledProcessError( - 100, "Unable to locate package"), # install genisoimage - subprocess.CalledProcessError( - 100, "Unable to locate package"), # sudo install genisoimage - None, # install xorriso - ] - - self.helper.install_helper() - self.assertSubprocessCalls(mock_check_output, - [['dpkg', '-s', 'genisoimage'], - ['dpkg', '-s', 'xorriso']]) - self.assertSubprocessCalls( - mock_check_call, - [['apt-get', '-q', 'update'], - ['apt-get', '-q', 'install', 'genisoimage'], - ['sudo', 'apt-get', '-q', 'install', 'genisoimage'], - ['apt-get', '-q', 'install', 'xorriso']]) - self.assertEqual(self.helper.name, 'xorriso') - - def test_create_iso_non_rockridge(self): - """Create a non-Rock-Ridge ISO.""" - dest_file = os.path.join(self.temp_dir, "test.iso") - self.helper.create_iso(dest_file, [self.input_ovf], rock_ridge=False) - (file_format, subformat) = get_disk_format(dest_file) - self.assertEqual(file_format, "iso") - self.assertEqual(subformat, None) - if ISOINFO.path: - self.assertEqual(get_disk_file_listing(dest_file), - [os.path.basename(self.input_ovf)]) - else: - logger.info("isoinfo not available, not checking disk contents") diff --git a/COT/helpers/tests/test_ovftool.py b/COT/helpers/tests/test_ovftool.py index df9f591..a7c3ef7 100644 --- a/COT/helpers/tests/test_ovftool.py +++ b/COT/helpers/tests/test_ovftool.py @@ -21,6 +21,8 @@ from COT.helpers.tests.test_helper import HelperUT from COT.helpers.ovftool import OVFTool +# pylint: disable=protected-access + class TestOVFTool(HelperUT): """Test cases for OVFTool helper class.""" @@ -30,40 +32,31 @@ def setUp(self): self.helper = OVFTool() super(TestOVFTool, self).setUp() - @mock.patch('COT.helpers.helper.Helper._check_output', + @mock.patch('COT.helpers.helper.check_output', return_value="Error: Unknown option: 'version'") @mock.patch('distutils.spawn.find_executable', return_value="/fake/ovftool") def test_invalid_version(self, *_): """Negative test for .version getter logic.""" + self.helper._installed = True with self.assertRaises(RuntimeError): assert self.helper.version @mock.patch('distutils.spawn.find_executable', return_value="/fake/ovftool") - @mock.patch('COT.helpers.helper.Helper._check_output') + @mock.patch('COT.helpers.helper.check_output') @mock.patch('subprocess.check_call') - def test_install_helper_already_present(self, mock_check_call, - mock_check_output, *_): + def test_install_already_present(self, mock_check_call, + mock_check_output, *_): """Do nothing when trying to re-install.""" - self.helper.install_helper() + self.helper._installed = True + self.helper.install() mock_check_call.assert_not_called() mock_check_output.assert_not_called() - self.assertLogged(**self.ALREADY_INSTALLED) def test_install_helper_unsupported(self): """No support for automated installation of ovftool.""" with mock.patch('COT.helpers.ovftool.OVFTool.path', new_callable=mock.PropertyMock, return_value=None): with self.assertRaises(NotImplementedError): - self.helper.install_helper() - - @mock.patch('distutils.spawn.find_executable', - return_value="/fake/ovftool") - @mock.patch('COT.helpers.helper.Helper._check_output', return_value="") - def test_validate_ovf(self, mock_check_output, *_): - """Try the validate_ovf() API.""" - self.helper.validate_ovf(self.input_ovf) - self.assertSubprocessCalls( - mock_check_output, - [['ovftool', '--schemaValidate', self.input_ovf]]) + self.helper.install() diff --git a/COT/helpers/tests/test_qemu_img.py b/COT/helpers/tests/test_qemu_img.py index 81cfc3b..6b28c26 100644 --- a/COT/helpers/tests/test_qemu_img.py +++ b/COT/helpers/tests/test_qemu_img.py @@ -16,15 +16,14 @@ """Unit test cases for the COT.helpers.qemu_img submodule.""" -import os - from distutils.version import StrictVersion import mock from COT.helpers.tests.test_helper import HelperUT -from COT.helpers import HelperError from COT.helpers.qemu_img import QEMUImg +# pylint: disable=protected-access + class TestQEMUImg(HelperUT): """Test cases for QEMUImg helper class.""" @@ -34,7 +33,7 @@ def setUp(self): self.helper = QEMUImg() super(TestQEMUImg, self).setUp() - @mock.patch('COT.helpers.helper.Helper._check_output') + @mock.patch('COT.helpers.helper.check_output') def test_older_version(self, mock_check_output): """Test .version getter logic for older versions.""" mock_check_output.return_value = """ @@ -44,6 +43,7 @@ def test_older_version(self, mock_check_output): Command syntax: ...""" + self.helper._installed = True version = self.helper.version self.assertSubprocessCalls(mock_check_output, [['qemu-img', '--version']]) @@ -55,107 +55,39 @@ def test_older_version(self, mock_check_output): mock_check_output.assert_not_called() self.assertEqual(version, StrictVersion("1.4.2")) - @mock.patch('COT.helpers.helper.Helper._check_output') + @mock.patch('COT.helpers.helper.check_output') def test_newer_version(self, mock_check_output): """Test .version getter logic for newer versions.""" + self.helper._installed = True mock_check_output.return_value = \ "qemu-img version 2.1.2, Copyright (c) 2004-2008 Fabrice Bellard" self.assertEqual(self.helper.version, StrictVersion("2.1.2")) - @mock.patch('COT.helpers.helper.Helper._check_output') + @mock.patch('COT.helpers.helper.check_output') def test_invalid_version(self, mock_check_output): """Negative test for .version getter logic.""" + self.helper._installed = True mock_check_output.return_value = \ "qemu-img: error: unknown argument --version" with self.assertRaises(RuntimeError): assert self.helper.version @mock.patch('subprocess.check_call') - def test_install_helper_already_present(self, mock_check_call): + def test_install_already_present(self, mock_check_call): """Do nothing when trying to re-install.""" - self.helper.install_helper() + self.helper._installed = True + self.helper.install() mock_check_call.assert_not_called() - self.assertLogged(**self.ALREADY_INSTALLED) - def test_install_helper_apt_get(self): + def test_install_apt_get(self): """Test installation via 'apt-get'.""" self.apt_install_test('qemu-utils', 'qemu-img') - def test_install_helper_port(self): + def test_install_port(self): """Test installation via 'port'.""" self.port_install_test('qemu') - def test_install_helper_yum(self): + def test_install_yum(self): """Test installation via 'yum'.""" self.yum_install_test('qemu-img') - - def test_get_disk_format(self): - """Get format of various disk images.""" - self.assertEqual('vmdk', self.helper.get_disk_format(self.blank_vmdk)) - - temp_disk = os.path.join(self.temp_dir, 'foo.img') - self.helper.create_blank_disk(temp_disk, capacity="16M") - self.assertEqual('raw', self.helper.get_disk_format(temp_disk)) - - temp_disk = os.path.join(self.temp_dir, 'foo.qcow2') - self.helper.create_blank_disk(temp_disk, capacity="1G") - self.assertEqual('qcow2', self.helper.get_disk_format(temp_disk)) - - def test_get_disk_format_no_file(self): - """Negative test for get_disk_format() - no such file.""" - self.assertRaises(HelperError, self.helper.get_disk_format, "") - self.assertRaises(HelperError, self.helper.get_disk_format, - "/foo/bar/baz") - - @mock.patch('COT.helpers.helper.Helper._check_output') - def test_get_disk_format_not_available(self, mock_check_output): - """Negative test for get_disk_format() - bad command output.""" - # Haven't found a way yet to make qemu-img actually fail here - # without returning a non-zero RC and triggering a HelperError, - # so we'll have to fake it - mock_check_output.return_value = "qemu-img info: unsupported command" - self.assertRaises(RuntimeError, self.helper.get_disk_format, - "/foo/bar") - - def test_get_disk_capacity(self): - """Test the get_disk_capacity() method.""" - self.assertEqual("536870912", - self.helper.get_disk_capacity(self.blank_vmdk)) - - self.assertEqual("1073741824", - self.helper.get_disk_capacity(self.input_vmdk)) - - def test_get_disk_capacity_no_file(self): - """Negative test for get_disk_capacity() - no such file.""" - self.assertRaises(HelperError, self.helper.get_disk_capacity, "") - self.assertRaises(HelperError, self.helper.get_disk_capacity, - "/foo/bar/baz") - - @mock.patch('COT.helpers.helper.Helper._check_output') - def test_get_disk_capacity_not_available(self, mock_check_output): - """Negative test for get_disk_capacity() - bad command output.""" - # Haven't found a way yet to make qemu-img actually fail here - # without returning a non-zero RC and triggering a HelperError, - # so we'll have to fake it - mock_check_output.return_value = "qemu-img info: unsupported command" - self.assertRaises(RuntimeError, self.helper.get_disk_capacity, - "/foo/bar") - - def test_create_invalid(self): - """Invalid arguments.""" - # If extension not given, cannot guess file format - self.assertRaises(RuntimeError, - self.helper.create_blank_disk, - os.path.join(self.temp_dir, "out"), - capacity="1M") - - def test_convert_unsupported(self): - """Negative test for convert_disk_image() - unsupported formats.""" - with self.assertRaises(NotImplementedError): - self.helper.convert_disk_image(self.blank_vmdk, self.temp_dir, - 'vhd') - self.set_helper_version(StrictVersion("2.0.99")) - with self.assertRaises(NotImplementedError): - self.helper.convert_disk_image(self.blank_vmdk, self.temp_dir, - 'vmdk', 'streamOptimized') diff --git a/COT/helpers/tests/test_vmdktool.py b/COT/helpers/tests/test_vmdktool.py index 198bc00..f7d74e6 100644 --- a/COT/helpers/tests/test_vmdktool.py +++ b/COT/helpers/tests/test_vmdktool.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# test_vmdktool.py - Unit test cases for COT.helpers.vmdktoolsubmodule. +# test_vmdktool.py - Unit test cases for COT.helpers.vmdktool submodule. # # March 2015, Glenn F. Matthews # Copyright (c) 2014-2016 the COT project developers. @@ -22,55 +22,55 @@ import mock from COT.helpers.tests.test_helper import HelperUT -from COT.helpers.vmdktool import VmdkTool +from COT.helpers.vmdktool import VMDKTool +from COT.helpers import helpers +# pylint: disable=protected-access -@mock.patch('COT.helpers.vmdktool.VmdkTool.download_and_expand_tgz', + +@mock.patch('COT.helpers.Helper.download_and_expand_tgz', side_effect=HelperUT.stub_download_and_expand_tgz) -class TestVmdkTool(HelperUT): - """Test cases for VmdkTool helper class.""" +class TestVMDKTool(HelperUT): + """Test cases for VMDKTool helper class.""" def setUp(self): """Test case setup function called automatically prior to each test.""" - self.helper = VmdkTool() - super(TestVmdkTool, self).setUp() + self.helper = VMDKTool() + super(TestVMDKTool, self).setUp() - @mock.patch('COT.helpers.helper.Helper._check_output', - return_value="vmdktool version 1.4") + @mock.patch('COT.helpers.helper.check_output', + return_value="vmdktool version 1.3") def test_get_version(self, *_): """Test .version getter logic.""" - self.assertEqual(StrictVersion("1.4"), self.helper.version) + self.helper._installed = True + self.assertEqual(self.helper.version, StrictVersion("1.3")) - @mock.patch('COT.helpers.helper.Helper._check_output') + @mock.patch('COT.helpers.helper.check_output') @mock.patch('subprocess.check_call') - def test_install_helper_already_present(self, mock_check_call, - mock_check_output, *_): + def test_install_already_present(self, mock_check_call, + mock_check_output, *_): """Do nothing instead of re-installing.""" - self.helper.install_helper() + self.helper._installed = True + self.helper.install() mock_check_output.assert_not_called() mock_check_call.assert_not_called() - self.assertLogged(**self.ALREADY_INSTALLED) @mock.patch('platform.system', return_value='Linux') @mock.patch('os.path.isdir', return_value=False) @mock.patch('os.path.exists', return_value=False) @mock.patch('os.makedirs', side_effect=OSError) @mock.patch('distutils.spawn.find_executable') - @mock.patch('COT.helpers.helper.Helper._check_output', return_value="") + @mock.patch('COT.helpers.helper.check_output', return_value="") @mock.patch('subprocess.check_call') def test_install_helper_apt_get(self, mock_check_call, mock_check_output, - mock_find_executable, *_): """Test installation via 'apt-get'.""" self.enable_apt_install() - mock_find_executable.side_effect = [ - None, # 'vmdktool', - None, # 'make', pre-installation - '/bin/make', # post-installation - ] - self.helper.install_helper() + helpers['dpkg']._installed = True + helpers['make']._installed = False + self.helper.install() self.assertSubprocessCalls( mock_check_output, [ @@ -93,15 +93,15 @@ def test_install_helper_apt_get(self, # Make sure we don't 'apt-get update/install' again unnecessarily mock_check_call.reset_mock() mock_check_output.reset_mock() - mock_find_executable.reset_mock() mock_check_output.return_value = 'install ok installed' - mock_find_executable.side_effect = [ - None, # vmdktool - '/bin/make', - ] + # fakeout! + self.helper._installed = False + helpers['make']._installed = True + os.environ['PREFIX'] = '/opt/local' os.environ['DESTDIR'] = '/home/cot' - self.helper.install_helper() + + self.helper.install() self.assertSubprocessCalls( mock_check_output, [ @@ -130,16 +130,12 @@ def test_install_helper_port(self, *_): @mock.patch('subprocess.check_call') def test_install_helper_yum(self, mock_check_call, - mock_find_executable, *_): """Test installation via 'yum'.""" self.enable_yum_install() - mock_find_executable.side_effect = [ - None, # 'vmdktool', - None, # 'make', pre-installation - '/bin/make', # post-installation - ] - self.helper.install_helper() + self.helper._installed = False + helpers['make']._installed = False + self.helper.install() self.assertSubprocessCalls( mock_check_call, [ @@ -156,8 +152,7 @@ def test_install_helper_yum(self, def test_install_helper_linux_need_make_no_package_manager(self, *_): """Linux installation requires yum or apt-get if 'make' missing.""" self.select_package_manager(None) - with self.assertRaises(NotImplementedError): - self.helper.install_helper() + self.assertRaises(NotImplementedError, self.helper.install) @mock.patch('platform.system', return_value='Linux') @mock.patch('distutils.spawn.find_executable') @@ -167,14 +162,10 @@ def test_install_linux_need_compiler_no_package_manager(self, """Linux installation needs some way to install 'zlib'.""" self.select_package_manager(None) mock_find_exec.side_effect = [None, '/bin/make'] - with self.assertRaises(NotImplementedError): - self.helper.install_helper() - - def test_convert_unsupported(self, *_): - """Negative test - conversion to unsupported format/subformat.""" - with self.assertRaises(NotImplementedError): - self.helper.convert_disk_image(self.blank_vmdk, self.temp_dir, - 'qcow2') - with self.assertRaises(NotImplementedError): - self.helper.convert_disk_image(self.blank_vmdk, self.temp_dir, - 'vmdk', 'monolithicSparse') + self.assertRaises(NotImplementedError, self.helper.install) + + @mock.patch('platform.system', return_value='Darwin') + def test_install_helper_mac_no_package_manager(self, *_): + """Mac installation requires port.""" + self.select_package_manager(None) + self.assertRaises(NotImplementedError, self.helper.install) diff --git a/COT/helpers/tests/test_xorriso.py b/COT/helpers/tests/test_xorriso.py new file mode 100644 index 0000000..3430d0f --- /dev/null +++ b/COT/helpers/tests/test_xorriso.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# +# test_xorriso.py - Unit test cases for XorrISO class. +# +# 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 XorrISO class.""" + +import logging + +from distutils.version import StrictVersion +import mock + +from COT.helpers.tests.test_helper import HelperUT +from COT.helpers.mkisofs import XorrISO + +logger = logging.getLogger(__name__) + +# pylint: disable=protected-access + + +class TestXorrISO(HelperUT): + """Test cases for XorrISO helper class.""" + + def setUp(self): + """Test case setup function called automatically prior to each test.""" + self.helper = XorrISO() + super(TestXorrISO, self).setUp() + + @mock.patch('COT.helpers.helper.check_output', return_value=""" +xorriso 1.3.2 : RockRidge filesystem manipulator, libburnia project. + +xorriso 1.3.2 +ISO 9660 Rock Ridge filesystem manipulator and CD/DVD/BD burn program +Copyright (C) 2013, Thomas Schmitt , libburnia project. +xorriso version : 1.3.2 +Version timestamp : 2013.08.07.110001 +Build timestamp : -none-given- +libisofs in use : 1.3.4 (min. 1.3.2) +libjte in use : 1.0.0 (min. 1.0.0) +libburn in use : 1.3.4 (min. 1.3.4) +libburn OS adapter: internal GNU/Linux SG_IO adapter sg-linux +libisoburn in use : 1.3.2 (min. 1.3.2) +Provided under GNU GPL version 2 or later. +There is NO WARRANTY, to the extent permitted by law. +""") + def test_get_version(self, _): + """Test .version getter logic for xorriso.""" + self.helper._installed = True + self.assertEqual(StrictVersion("1.3.2"), self.helper.version) + + @mock.patch('COT.helpers.helper.check_output') + @mock.patch('subprocess.check_call') + def test_install_already_present(self, mock_check_call, mock_check_output): + """Don't re-install if already installed.""" + self.helper._installed = True + self.helper.install() + mock_check_output.assert_not_called() + mock_check_call.assert_not_called() + + def test_install_helper_apt_get(self): + """Test installation via 'apt-get' of xorriso.""" + self.apt_install_test('xorriso', 'xorriso') diff --git a/COT/helpers/vmdktool.py b/COT/helpers/vmdktool.py index 89d6a0c..b890e23 100644 --- a/COT/helpers/vmdktool.py +++ b/COT/helpers/vmdktool.py @@ -24,52 +24,48 @@ import os.path import platform -from COT.helpers.helper import Helper +from COT.helpers.helper import Helper, helpers, package_managers, check_call logger = logging.getLogger(__name__) -class VmdkTool(Helper): +class VMDKTool(Helper): """Helper provider for ``vmdktool``. http://www.freshports.org/sysutils/vmdktool/ - - **Methods** - - .. autosummary:: - :nosignatures: - - install_helper - convert_disk_image """ def __init__(self): """Initializer.""" - super(VmdkTool, self).__init__( + super(VMDKTool, self).__init__( "vmdktool", + info_uri="http://www.freshports.org/sysutils/vmdktool/", version_args=['-V'], version_regexp="vmdktool version ([0-9.]+)") - def install_helper(self): + @property + def installable(self): + """Whether COT is capable of installing this program on this system.""" + return (package_managers['apt-get'] or + package_managers['port'] or + package_managers['yum']) + + def _install(self): """Install ``vmdktool``.""" - if self.should_not_be_installed_but_is(): - return - logger.info("Installing 'vmdktool'...") - if Helper.port_install('vmdktool'): - pass + if package_managers['port']: + package_managers['port'].install_package('vmdktool') elif platform.system() == 'Linux': # We don't have vmdktool in apt or yum yet, # but we can build it manually: # vmdktool requires make and zlib - if not self.find_executable('make'): - logger.info("vmdktool requires 'make'... installing 'make'") - if not (Helper.apt_install('make') or - Helper.yum_install('make')): - raise NotImplementedError("Not sure how to install 'make'") - assert self.find_executable('make') + helpers['make'].install() + # TODO: check for installed zlib? logger.info("vmdktool requires 'zlib'... installing 'zlib'") - if not (Helper.apt_install('zlib1g-dev') or - Helper.yum_install('zlib-devel')): + if package_managers['apt-get']: + package_managers['apt-get'].install_package('zlib1g-dev') + elif package_managers['yum']: + package_managers['yum'].install_package('zlib-devel') + else: raise NotImplementedError("Not sure how to install 'zlib'") with self.download_and_expand_tgz( 'http://people.freebsd.org/~brian/' @@ -81,9 +77,9 @@ def install_helper(self): # The easiest workaround is to override the CFLAGS to: # 1) add -D_GNU_SOURCE # 2) not treat all warnings as errors - self._check_call(['make', - 'CFLAGS="-D_GNU_SOURCE -g -O -pipe"'], - cwd=new_d) + check_call(['make', + 'CFLAGS="-D_GNU_SOURCE -g -O -pipe"'], + cwd=new_d) destdir = os.getenv('DESTDIR', '') prefix = os.getenv('PREFIX', '/usr/local') args = ['make', 'install', 'PREFIX=' + prefix] @@ -94,56 +90,6 @@ def install_helper(self): logger.info("Compilation complete, installing to " + os.path.join(destdir, prefix)) # Make sure the relevant man and bin directories exist - self.make_install_dir(os.path.join(destdir, prefix, - 'man', 'man8')) - self.make_install_dir(os.path.join(destdir, prefix, 'bin')) - self._check_call(args, retry_with_sudo=True, cwd=new_d) - else: - raise NotImplementedError( - "Unsure how to install vmdktool.\n" - "See http://www.freshports.org/sysutils/vmdktool/") - logger.info("Successfully installed 'vmdktool'") - - def convert_disk_image(self, file_path, output_dir, - new_format, new_subformat=None): - """Convert the given disk image to the requested format/subformat. - - If the disk is already in this format then it is unchanged; - otherwise, will convert to a new disk in the specified output_dir - and return its path. - - Current supported conversions: - - * .vmdk (any format) to .vmdk (streamOptimized) - * .img to .vmdk (streamOptimized) - - :param str file_path: Disk image file to inspect/convert - :param str output_dir: Directory to place converted image into, if - needed - :param str new_format: Desired final format - :param str new_subformat: Desired final subformat - :return: - * :attr:`file_path`, if no conversion was required - * or a file path in :attr:`output_dir` containing the converted image - - :raise NotImplementedError: if the :attr:`new_format` and/or - :attr:`new_subformat` are not supported conversion targets. - """ - file_name = os.path.basename(file_path) - (file_string, _) = os.path.splitext(file_name) - - new_file_path = None - - if new_format == 'vmdk' and new_subformat == 'streamOptimized': - new_file_path = os.path.join(output_dir, file_string + '.vmdk') - logger.info("Invoking vmdktool to convert %s to " - "stream-optimized VMDK %s", file_path, new_file_path) - # Note that vmdktool takes its arguments in unusual order - - # output file comes before input file - self.call_helper(['-z9', '-v', new_file_path, file_path]) - else: - raise NotImplementedError("No support for converting disk image " - "to format %s / subformat %s", - new_format, new_subformat) - - return new_file_path + self.mkdir(os.path.join(destdir, prefix, 'man', 'man8')) + self.mkdir(os.path.join(destdir, prefix, 'bin')) + check_call(args, retry_with_sudo=True, cwd=new_d) diff --git a/COT/helpers/yum.py b/COT/helpers/yum.py new file mode 100644 index 0000000..8684e24 --- /dev/null +++ b/COT/helpers/yum.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# +# yum.py - Wrapper for the 'yum' package manager. +# +# October 2016, Glenn F. Matthews +# Copyright (c) 2015-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. + +"""Wrapper for the 'yum' package manager.""" + +import logging + +from COT.helpers.helper import PackageManager + +logger = logging.getLogger(__name__) + + +class Yum(PackageManager): + """The 'yum' package manager utility.""" + + def __init__(self): + """Initializer.""" + super(Yum, self).__init__("yum") + + def install_package(self, package): + """Install the requested package if needed. + + :param str package: Name of the package to install. + """ + self.call(['--quiet', 'install', package], + capture_output=False, retry_with_sudo=True) diff --git a/COT/inject_config.py b/COT/inject_config.py index 8dba6fc..a002758 100644 --- a/COT/inject_config.py +++ b/COT/inject_config.py @@ -22,7 +22,7 @@ from COT.add_disk import add_disk_worker from COT.data_validation import ValueUnsupportedError, InvalidInputError -from COT.helpers import create_disk_image +from COT.disks import create_disk from COT.submodule import COTSubmodule logger = logging.getLogger(__name__) @@ -175,13 +175,17 @@ def run(self): config_files += self.extra_files # Package the config files into a disk image + # pylint:disable=redefined-variable-type if platform.BOOTSTRAP_DISK_TYPE == 'cdrom': bootstrap_file = os.path.join(vm.working_dir, 'config.iso') - create_disk_image(bootstrap_file, contents=config_files) + disk_image = create_disk(disk_format='iso', + path=bootstrap_file, + files=config_files) elif platform.BOOTSTRAP_DISK_TYPE == 'harddisk': bootstrap_file = os.path.join(vm.working_dir, 'config.img') - create_disk_image(bootstrap_file, file_format='raw', - contents=config_files) + disk_image = create_disk(disk_format='raw', + path=bootstrap_file, + files=config_files) else: raise ValueUnsupportedError("bootstrap disk type", platform.BOOTSTRAP_DISK_TYPE, @@ -191,7 +195,7 @@ def run(self): add_disk_worker( ui=self.UI, vm=vm, - disk_image=bootstrap_file, + disk_image=disk_image, disk_type=platform.BOOTSTRAP_DISK_TYPE, file_id=file_id, controller=cont_type, diff --git a/COT/install_helpers.py b/COT/install_helpers.py index b69b292..7e592c2 100644 --- a/COT/install_helpers.py +++ b/COT/install_helpers.py @@ -27,8 +27,7 @@ from pkg_resources import resource_listdir, resource_filename from COT.submodule import COTGenericSubmodule -from COT.helpers import HelperError, HelperNotFoundError -from COT.helpers import create_install_dir, install_file +from COT.helpers import Helper, HelperError, HelperNotFoundError, helpers logger = logging.getLogger(__name__) @@ -87,7 +86,7 @@ def _install_manpage(src_path, man_dir): f = os.path.basename(src_path) section = os.path.splitext(f)[1][1:] dest = os.path.join(man_dir, "man{0}".format(section)) - create_install_dir(dest) + Helper.mkdir(dest) previously_installed = False dest_path = os.path.join(dest, f) @@ -97,7 +96,7 @@ def _install_manpage(src_path, man_dir): logger.verbose("File %s does not need to be updated", dest_path) return previously_installed, False - install_file(src_path, dest_path) + Helper.cp(src_path, dest_path) return previously_installed, True @@ -143,7 +142,7 @@ def install_helper(self, helper): :return: (result, message) """ - if helper.path: + if helper.installed: return (True, "version {0}, present at {1}" .format(helper.version, str(helper.path))) @@ -151,7 +150,7 @@ def install_helper(self, helper): return (True, "NOT FOUND") else: try: - helper.install_helper() + helper.install() return (True, "successfully installed to {0}, version {1}" .format(str(helper.path), helper.version)) @@ -179,20 +178,22 @@ def manpages_helper(self): def run(self): """Verify all helper tools and install any that are missing.""" - from COT.helpers.fatdisk import FatDisk - from COT.helpers.mkisofs import MkIsoFS - # isoinfo comes with mkisofs so we skip it here - from COT.helpers.ovftool import OVFTool - from COT.helpers.qemu_img import QEMUImg - from COT.helpers.vmdktool import VmdkTool result = True results = {} - for cls in [FatDisk, MkIsoFS, OVFTool, QEMUImg, VmdkTool]: - helper = cls() + for name in ['fatdisk', 'ovftool', 'qemu-img', 'vmdktool']: + helper = helpers[name] rc, results[helper.name] = self.install_helper(helper) if not rc: result = False + # We only need one of these three tools so stop as soon as one succeeds + for name in ['mkisofs', 'genisoimage', 'xorriso']: + isorc, results[name] = self.install_helper(helpers[name]) + if isorc: + break + if not isorc: + result = False + rc, results["COT manpages"] = self.manpages_helper() if not rc: result = False diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index 75b59b6..a8b3237 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -56,8 +56,8 @@ ValueTooHighError, ValueUnsupportedError, canonicalize_nic_subtype, ) from COT.file_reference import FileOnDisk, FileInTAR -from COT.helpers import get_disk_capacity, convert_disk_image from COT.platforms import platform_from_product_class, GenericPlatform +from COT.disks import convert_disk, disk_representation_from_file from COT.ovf.name_helper import name_helper from COT.ovf.hardware import OVFHardware, OVFHardwareDataError @@ -812,7 +812,8 @@ 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: - real_capacity = get_disk_capacity(file_ref.file_path) + dr = disk_representation_from_file(file_ref.file_path) + real_capacity = dr.capacity disk_item = self.find_disk_from_file_id( file_elem.get(self.FILE_ID)) @@ -1748,26 +1749,33 @@ def config_file_to_properties(self, file_path, user_configurable=None): line, user_configurable) - def convert_disk_if_needed(self, file_path, kind): + def convert_disk_if_needed(self, disk_image, kind): """Convert the disk to a more appropriate format if needed. * All hard disk files are converted to stream-optimized VMDK as it is the only format that VMware supports in OVA packages. * CD-ROM iso images are accepted without change. - :param str file_path: Image to inspect and possibly convert - :param str kind: Image type (harddisk/cdrom) + :param disk_image: Image to inspect and possibly convert + :type disk_image: :class:`~COT.disks.Disk` or subclass + :param str kind: Image type (harddisk/cdrom). :return: - * :attr:`file_path`, if no conversion was required - * or a file path in :attr:`output_dir` containing the converted image + * :attr:`disk_image`, if no conversion was required + * or a new :class:`~COT.disks.Disk` instance representing a converted + image that has been created in :attr:`output_dir`. """ if kind != 'harddisk': logger.debug("No disk conversion needed") - return file_path + return disk_image # Convert hard disk to VMDK format, streamOptimized subformat - return convert_disk_image(file_path, self.working_dir, - 'vmdk', 'streamOptimized') + if (disk_image.disk_format == 'vmdk' and + disk_image.disk_subformat == 'streamOptimized'): + logger.debug("No disk conversion needed") + return disk_image + + return convert_disk(disk_image, self.working_dir, + 'vmdk', 'streamOptimized') def search_from_filename(self, filename): """From the given filename, try to find any existing objects. @@ -2166,10 +2174,11 @@ def remove_file(self, file_obj, disk=None, disk_drive=None): .format(self.RES_MAP['cdrom'], self.RES_MAP['harddisk'])) - def add_disk(self, file_path, file_id, disk_type, disk=None): + def add_disk(self, disk_repr, file_id, disk_type, disk=None): """Add a new disk object to the VM or overwrite the provided one. - :param str file_path: Path to disk image file + :param str disk_repr: Disk file representation + :type disk_repr: COT.disks.DiskRepresentation or subclass :param str file_id: Identifier string for the file/disk mapping :param str disk_type: 'harddisk' or 'cdrom' :param disk: Existing disk object to overwrite @@ -2193,6 +2202,7 @@ def add_disk(self, file_path, file_id, disk_type, disk=None): "do not require a Disk") return disk + # Else, adding a hard disk: self.disk_section = self._ensure_section( self.DISK_SECTION, "Virtual disk information", @@ -2207,8 +2217,7 @@ def add_disk(self, file_path, file_id, disk_type, disk=None): disk_id = file_id disk = ET.SubElement(self.disk_section, self.DISK) - capacity = get_disk_capacity(file_path) - self.set_capacity_of_disk(disk, capacity) + self.set_capacity_of_disk(disk, disk_repr.capacity) disk.set(self.DISK_ID, disk_id) disk.set(self.DISK_FILE_REF, file_id) diff --git a/COT/ovf/tests/test_ovf.py b/COT/ovf/tests/test_ovf.py index d411f54..c2b2cb3 100644 --- a/COT/ovf/tests/test_ovf.py +++ b/COT/ovf/tests/test_ovf.py @@ -34,7 +34,7 @@ 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 HelperError +from COT.helpers import helpers, HelperError from COT.vm_context_manager import VMContextManager logger = logging.getLogger(__name__) @@ -457,10 +457,11 @@ def test_invalid_ovf_contents(self): subprocess.check_call(['sed', 's/InstanceID>11108""" .format(cfg_size=self.FILE_SIZE['sample_cfg.txt'], config_size=os.path.getsize(config_iso))) - if ISOINFO.path: + 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(get_disk_file_listing(config_iso), + self.assertEqual(disk_representation_from_file(config_iso).files, ["config.txt"]) else: logger.info("isoinfo not available, not checking disk contents") @@ -172,10 +172,10 @@ def test_inject_config_iso_secondary(self): 8""" .format(cfg_size=self.FILE_SIZE['sample_cfg.txt'], config_size=os.path.getsize(config_iso))) - if ISOINFO.path: + if helpers['isoinfo']: # The sample_cfg.text should be renamed to the platform-specific # file name for secondary bootstrap config - self.assertEqual(get_disk_file_listing(config_iso), + self.assertEqual(disk_representation_from_file(config_iso).files, ["iosxr_config_admin.txt"]) else: logger.info("isoinfo not available, not checking disk contents") @@ -221,7 +221,7 @@ def test_inject_config_vmdk(self): .format(input_size=self.FILE_SIZE['input.vmdk'], config_size=os.path.getsize(config_vmdk))) # TODO - we don't currently have a way to check VMDK file listing - # self.assertEqual(get_disk_file_listing(config_vmdk), + # self.assertEqual(disk_representation_from_file(config_vmdk).files, # ["ios_config.txt"]) def test_inject_config_repeatedly(self): @@ -326,9 +326,9 @@ def test_inject_config_primary_secondary_extra(self): 8""" .format(cfg_size=self.FILE_SIZE['sample_cfg.txt'], config_size=os.path.getsize(config_iso))) - if ISOINFO.path: + if helpers['isoinfo']: self.assertEqual( - get_disk_file_listing(config_iso), + disk_representation_from_file(config_iso).files, [ "iosxr_config.txt", "iosxr_config_admin.txt", diff --git a/COT/tests/test_install_helpers.py b/COT/tests/test_install_helpers.py index 59e8daa..ca0df8e 100644 --- a/COT/tests/test_install_helpers.py +++ b/COT/tests/test_install_helpers.py @@ -25,8 +25,9 @@ from COT.tests.ut import COT_UT from COT.ui_shared import UI from COT.install_helpers import COTInstallHelpers -from COT.helpers import HelperError -from COT.helpers.helper import Helper +from COT.helpers import HelperError, helpers, package_managers +from COT.helpers.apt_get import AptGet +from COT.helpers.port import Port def stub_check_output(arg_list, *_args, **_kwargs): @@ -49,6 +50,8 @@ def stub_dir_exists_but_not_file(path): return os.path.basename(path) != "cot.1" +# pylint: disable=protected-access + class TestCOTInstallHelpers(COT_UT): """Test the COTInstallHelpers class.""" @@ -63,14 +66,27 @@ def setUp(self): self.manpath = os.path.join( os.path.dirname(os.path.dirname(sys.argv[0])), "man") - @mock.patch('COT.helpers.helper.Helper._check_output', + # Fake out installation status + for helper in helpers.values(): + helper._installed = None + helper._path = None + helper._version = None + + def tearDown(self): + """Restore baseline behavior after each test case.""" + for helper in helpers.values(): + helper._installed = None + helper._path = None + helper._version = None + super(TestCOTInstallHelpers, self).tearDown() + + @mock.patch('COT.helpers.helper.check_output', side_effect=stub_check_output) @mock.patch('os.path.exists', return_value=True) @mock.patch('filecmp.cmp', return_value=True) @mock.patch('distutils.spawn.find_executable') def test_verify_only(self, mock_find_executable, *_): """Make sure expected results are seen with --verify-only option.""" - # pylint: disable=protected-access def stub_find_executable(name): """Pretend to find every executable except ovftool.""" if name == 'ovftool': @@ -92,14 +108,14 @@ def stub_find_executable(name): """ self.check_cot_output(expected_output) - @mock.patch('COT.helpers.helper.Helper._check_output', + @mock.patch('COT.helpers.helper.check_output', side_effect=stub_check_output) @mock.patch('os.path.isdir', return_value=True) @mock.patch('os.path.exists', return_value=True) @mock.patch('filecmp.cmp', return_value=True) - @mock.patch('COT.helpers.helper.Helper.apt_install') - @mock.patch('COT.helpers.helper.Helper.yum_install') - @mock.patch('COT.helpers.helper.Helper.port_install') + @mock.patch('COT.helpers.apt_get.AptGet.install_package') + @mock.patch('COT.helpers.yum.Yum.install_package') + @mock.patch('COT.helpers.port.Port.install_package') @mock.patch('distutils.spawn.find_executable') def test_install(self, mock_find_executable, @@ -108,41 +124,44 @@ def test_install(self, mock_apt_install, *_): """Show results when pretending to install helpers.""" - # pylint: disable=protected-access paths = { "fatdisk": "/opt/local/bin/fatdisk", - "mkisofs": None, - "genisoimage": None, - "ovftool": None, - "qemu-img": None, - "vmdktool": None } + for helper_name in helpers: + helpers[helper_name]._installed = False + + helpers['fatdisk']._installed = True + helpers['fatdisk']._path = "/opt/local/bin/fatdisk" + def stub_find_executable(name): - """Get canned paths for various executables.""" + """Pretend to find every executable except ovftool.""" return paths.get(name, None) def stub_install(package): """Fake successful or unsuccessful installation of tools.""" if package == "genisoimage": - paths["genisoimage"] = "/usr/bin/genisoimage" - return True - elif package == "cdrtools": - return False + helpers['genisoimage']._path = "/usr/bin/genisoimage" + helpers['genisoimage']._installed = True + return raise HelperError(1, "not really installing!") mock_find_executable.side_effect = stub_find_executable + package_managers['apt-get']._installed = True mock_apt_install.side_effect = stub_install + package_managers['port']._installed = True mock_port_install.side_effect = stub_install + package_managers['yum']._installed = True mock_yum_install.side_effect = stub_install - Helper._apt_updated = False - Helper._port_updated = False + AptGet._updated = False + Port._updated = False expected_output = """ Results: ------------- COT manpages: already installed, no updates needed fatdisk: version 1.0, present at /opt/local/bin/fatdisk genisoimage: successfully installed to /usr/bin/genisoimage, version 1.1.11 +mkisofs: INSTALLATION FAILED: [Errno 1] not really installing! ovftool: INSTALLATION FAILED: No support for automated installation of ovftool, as VMware requires a site login to download it. See https://www.vmware.com/support/developer/ovf/ @@ -155,7 +174,7 @@ def stub_install(package): # ...but we can set ignore_errors to suppress this behavior self.instance.ignore_errors = True # revert to initial state - paths["genisoimage"] = None + helpers["genisoimage"]._installed = False self.check_cot_output(expected_output) @mock.patch('os.path.exists', return_value=False) @@ -186,7 +205,7 @@ def test_manpages_helper_verify_file_outdated(self, *_): self.assertEqual("NEEDS UPDATE", message) @mock.patch('os.path.exists', return_value=False) - @mock.patch('COT.helpers.helper.Helper._check_call', + @mock.patch('COT.helpers.helper.check_call', side_effect=HelperError) @mock.patch('os.makedirs') def test_manpages_helper_create_dir_fail(self, mock_makedirs, *_): @@ -204,7 +223,7 @@ def test_manpages_helper_create_dir_fail(self, mock_makedirs, *_): @mock.patch('os.path.exists', return_value=True) @mock.patch('os.path.isdir', return_value=True) @mock.patch('filecmp.cmp', return_value=False) - @mock.patch('COT.helpers.helper.Helper._check_call', + @mock.patch('COT.helpers.helper.check_call', side_effect=HelperError) @mock.patch('shutil.copy') def test_manpages_helper_create_file_fail(self, mock_copy, *_): diff --git a/COT/tests/ut.py b/COT/tests/ut.py index 491153d..427c197 100644 --- a/COT/tests/ut.py +++ b/COT/tests/ut.py @@ -49,6 +49,8 @@ def emit(self, record): from pkg_resources import resource_filename import mock +from COT.helpers import helpers, HelperError + try: import unittest2 as unittest except ImportError: @@ -137,10 +139,6 @@ def assertNoLogsOver(self, max_level, info=''): # noqa: N802 class COT_UT(unittest.TestCase): # noqa: N801 """Subclass of unittest.TestCase adding some additional behaviors.""" - from COT.helpers.ovftool import OVFTool - - OVFTOOL = OVFTool() - FILE_SIZE = {} for filename in [ 'input.iso', @@ -379,12 +377,11 @@ def validate_with_ovftool(self, filename=None): """Use OVFtool to validate the given OVF/OVA file.""" if filename is None: filename = self.temp_file - if (self.OVFTOOL.path and self.validate_output_with_ovftool and - os.path.exists(filename)): - # Ask OVFtool to validate that the output file is sane - from COT.helpers import HelperError + if (self.validate_output_with_ovftool and + os.path.exists(filename) and + helpers['ovftool']): try: - self.OVFTOOL.validate_ovf(filename) + helpers['ovftool'].call(['--schemaValidate', filename]) except HelperError as e: self.fail("OVF not valid according to ovftool:\n{0}" .format(e.strerror)) diff --git a/COT/ui_shared.py b/COT/ui_shared.py index 4ec1a92..87e497c 100644 --- a/COT/ui_shared.py +++ b/COT/ui_shared.py @@ -43,8 +43,8 @@ def __init__(self, force=False): self.default_confirm_response = True """Knob for API testing, sets the default response to confirm().""" self._terminal_width = 80 - import COT.helpers.helper - COT.helpers.helper.confirm = self.confirm + from COT.helpers import Helper + Helper.UI = self @property def terminal_width(self): diff --git a/COT/vm_description.py b/COT/vm_description.py index 3907780..d07ad48 100644 --- a/COT/vm_description.py +++ b/COT/vm_description.py @@ -244,18 +244,20 @@ def version_long(self, value): # API methods needed for add-disk def convert_disk_if_needed(self, # pylint: disable=no-self-use - file_path, + disk_image, kind): # pylint: disable=unused-argument """Convert the disk to a more appropriate format if needed. - :param str file_path: Image to inspect and possibly convert + :param disk_image: Image to inspect and possibly convert + :type disk_image: :class:`~COT.disks.Disk` or subclass :param str kind: Image type (harddisk/cdrom). :return: - * :attr:`file_path`, if no conversion was required - * or a file path in :attr:`output_dir` containing the converted image + * :attr:`disk_image`, if no conversion was required + * or a new :class:`~COT.disks.Disk` instance representing a converted + image that has been created in :attr:`output_dir`. """ # Some VMs may not need this, so default to do nothing, not error - return file_path + return disk_image def search_from_filename(self, filename): """From the given filename, try to find any existing objects. @@ -389,10 +391,11 @@ def remove_file(self, file_obj, disk=None, disk_drive=None): """ raise NotImplementedError("remove_file not implemented") - def add_disk(self, file_path, file_id, disk_type, disk=None): + def add_disk(self, disk_repr, file_id, disk_type, disk=None): """Add a new disk object to the VM or overwrite the provided one. - :param str file_path: Path to disk image file + :param str disk_repr: Disk file representation + :type disk_repr: COT.disks.DiskRepresentation or subclass :param str file_id: Identifier string for the file/disk mapping :param str disk_type: 'harddisk' or 'cdrom' :param disk: Existing disk object to overwrite diff --git a/docs/COT.disks.iso.rst b/docs/COT.disks.iso.rst new file mode 100644 index 0000000..b2d7578 --- /dev/null +++ b/docs/COT.disks.iso.rst @@ -0,0 +1,4 @@ +``COT.disks.iso`` module +======================== + +.. automodule:: COT.disks.iso diff --git a/docs/COT.disks.qcow2.rst b/docs/COT.disks.qcow2.rst new file mode 100644 index 0000000..359651f --- /dev/null +++ b/docs/COT.disks.qcow2.rst @@ -0,0 +1,4 @@ +``COT.disks.qcow2`` module +========================== + +.. automodule:: COT.disks.qcow2 diff --git a/docs/COT.disks.raw.rst b/docs/COT.disks.raw.rst new file mode 100644 index 0000000..543073a --- /dev/null +++ b/docs/COT.disks.raw.rst @@ -0,0 +1,4 @@ +``COT.disks.raw`` module +======================== + +.. automodule:: COT.disks.raw diff --git a/docs/COT.disks.rst b/docs/COT.disks.rst new file mode 100644 index 0000000..4cbc7ea --- /dev/null +++ b/docs/COT.disks.rst @@ -0,0 +1,5 @@ +``COT.disks`` package reference +=============================== + +.. automodule:: COT.disks + :no-members: diff --git a/docs/COT.disks.vmdk.rst b/docs/COT.disks.vmdk.rst new file mode 100644 index 0000000..6cb95f5 --- /dev/null +++ b/docs/COT.disks.vmdk.rst @@ -0,0 +1,4 @@ +``COT.disks.vmdk`` module +========================= + +.. automodule:: COT.disks.vmdk diff --git a/docs/COT.helpers.api.rst b/docs/COT.helpers.api.rst deleted file mode 100644 index 6702bb8..0000000 --- a/docs/COT.helpers.api.rst +++ /dev/null @@ -1,5 +0,0 @@ -``COT.helpers.api`` module -=========================== - -.. automodule:: COT.helpers.api - diff --git a/docs/COT.helpers.apt_get.rst b/docs/COT.helpers.apt_get.rst new file mode 100644 index 0000000..2f4d35d --- /dev/null +++ b/docs/COT.helpers.apt_get.rst @@ -0,0 +1,4 @@ +``COT.helpers.apt_get`` module +============================== + +.. automodule:: COT.helpers.apt_get diff --git a/docs/COT.helpers.gcc.rst b/docs/COT.helpers.gcc.rst new file mode 100644 index 0000000..6beff85 --- /dev/null +++ b/docs/COT.helpers.gcc.rst @@ -0,0 +1,4 @@ +``COT.helpers.gcc`` module +========================== + +.. automodule:: COT.helpers.gcc diff --git a/docs/COT.helpers.make.rst b/docs/COT.helpers.make.rst new file mode 100644 index 0000000..f1604a5 --- /dev/null +++ b/docs/COT.helpers.make.rst @@ -0,0 +1,4 @@ +``COT.helpers.make`` module +=========================== + +.. automodule:: COT.helpers.make diff --git a/docs/COT.helpers.port.rst b/docs/COT.helpers.port.rst new file mode 100644 index 0000000..b13d359 --- /dev/null +++ b/docs/COT.helpers.port.rst @@ -0,0 +1,4 @@ +``COT.helpers.port`` module +=========================== + +.. automodule:: COT.helpers.port diff --git a/docs/COT.helpers.yum.rst b/docs/COT.helpers.yum.rst new file mode 100644 index 0000000..7328f12 --- /dev/null +++ b/docs/COT.helpers.yum.rst @@ -0,0 +1,4 @@ +``COT.helpers.yum`` module +========================== + +.. automodule:: COT.helpers.yum diff --git a/docs/index.rst b/docs/index.rst index 9e275cd..287efa3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,6 +12,7 @@ Common OVF Tool (COT) contributing thanks COT + COT.disks COT.helpers COT.ovf COT.platforms diff --git a/setup.py b/setup.py index 3ce4b16..88852f3 100644 --- a/setup.py +++ b/setup.py @@ -122,7 +122,7 @@ def with_project_on_sys_path(self, func): # Package contents cmdclass=cmdclass, - packages=['COT', 'COT.helpers', 'COT.ovf', 'COT.platforms'], + packages=['COT', 'COT.disks', 'COT.helpers', 'COT.ovf', 'COT.platforms'], package_data={ 'COT': ['docs/man/*'], }, From b2e5bbc3fbba384c2aa7b69b4cb6a45cd2f85412 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 17 Oct 2016 11:17:46 -0400 Subject: [PATCH 23/59] Doc fixup --- CHANGELOG.rst | 11 +++++++---- COT/disks/__init__.py | 13 ++++++++----- COT/ovf/ovf.py | 8 +++++--- COT/vm_description.py | 8 +++++--- docs/COT.disks.disk.rst | 4 ++++ docs/COT.disks.rst | 1 - 6 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 docs/COT.disks.disk.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7289036..8d1b099 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,7 +9,6 @@ This project adheres to `Semantic Versioning`_. **Added** - ``cot inject-config --extra-files`` parameter (`#53`_). -- :func:`COT.helpers.get_disk_file_listing`, currently used only in UT scripts. - Helper class for ``isoinfo`` (a companion to ``mkisofs``). **Changed** @@ -17,14 +16,18 @@ This project adheres to `Semantic Versioning`_. - Refactored the monolithic ``COT/platforms.py`` file into a proper submodule. - :func:`~COT.helpers.mkisofs.MkIsoFs.create_iso` now adds Rock Ridge extensions by default. +- Refactored :mod:`COT.helpers` into two modules - :mod:`COT.helpers` + (now just for handling helper programs such as ``apt-get`` and ``mkisofs``) + and :mod:`COT.disks` (which uses the helpers to handle ISO/VMDK/QCOW2/RAW + image files). **Removed** -- :func:`get_checksum` is no longer part of the ``COT.helpers`` public API. +- :func:`get_checksum` is no longer part of the :mod:`COT.helpers` API. (It's now the method :func:`~COT.data_validation.file_checksum` in ``COT.data_validation``, where it really belonged from the start). -- :func:`download_and_expand` is no longer part of the ``COT.helpers`` public - API. (It's now the static method +- :func:`download_and_expand` is no longer part of the :mod:`COT.helpers` + public API. (It's now the static method :func:`~COT.helpers.helper.Helper.download_and_expand_tgz` on class :class:`~COT.helpers.helper.Helper`.) diff --git a/COT/disks/__init__.py b/COT/disks/__init__.py index cb42838..503eb60 100644 --- a/COT/disks/__init__.py +++ b/COT/disks/__init__.py @@ -24,6 +24,7 @@ convert_disk create_disk disk_representation_from_file + ~COT.disks.disk.DiskRepresentation Disk modules ------------ @@ -31,6 +32,7 @@ .. autosummary:: :toctree: + COT.disks.disk COT.disks.iso COT.disks.qcow2 COT.disks.raw @@ -57,11 +59,11 @@ def convert_disk(disk_image, new_directory, new_format, new_subformat=None): """Convert a disk representation into a new format. :param disk_image: Existing disk image as input. - :type disk_image: :class:`~COT.disks.DiskRepresentation` or subclass. + :type disk_image: :class:`~COT.disks.disk.DiskRepresentation` or subclass. :param str new_directory: Directory to create new image under :param str new_format: Format to convert to. :param str new_subformat: (optional) Sub-format to convert to. - :return: new instance of :class:`~COT.disks.DiskRepresentation` subclass. + :return: instance of :class:`~COT.disks.disk.DiskRepresentation` subclass. """ if new_format not in _class_for_format: raise NotImplementedError("No support for converting to type '{0}'" @@ -75,9 +77,10 @@ def create_disk(disk_format, *args, **kwargs): """Create a disk of the requested format. :param str disk_format: Disk format such as 'iso' or 'vmdk'. - For the other parameters, see :class:`~COT.disks.DiskRepresentation`. - :return: new instance of :class:`~COT.disks.DiskRepresentation` subclass. + For the other parameters, see :class:`~COT.disks.disk.DiskRepresentation`. + + :return: instance of :class:`~COT.disks.disk.DiskRepresentation` subclass. """ if disk_format in _class_for_format: return _class_for_format[disk_format](*args, **kwargs) @@ -90,7 +93,7 @@ def disk_representation_from_file(file_path): :param str file_path: Path of existing file to represent. - :return: new instance of :class:`~COT.disks.DiskRepresentation` subclass. + :return: instance of :class:`~COT.disks.disk.DiskRepresentation` subclass. """ if not os.path.exists(file_path): raise IOError(2, "No such file or directory: {0}".format(file_path)) diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index a8b3237..5a29ee6 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -1757,12 +1757,14 @@ def convert_disk_if_needed(self, disk_image, kind): * CD-ROM iso images are accepted without change. :param disk_image: Image to inspect and possibly convert - :type disk_image: :class:`~COT.disks.Disk` or subclass + :type disk_image: instance of :class:`~COT.disks.DiskRepresentation` + or subclass :param str kind: Image type (harddisk/cdrom). :return: * :attr:`disk_image`, if no conversion was required - * or a new :class:`~COT.disks.Disk` instance representing a converted - image that has been created in :attr:`output_dir`. + * or a new :class:`~COT.disks.DiskRepresentation` instance + representing a converted image that has been created in + :attr:`output_dir`. """ if kind != 'harddisk': logger.debug("No disk conversion needed") diff --git a/COT/vm_description.py b/COT/vm_description.py index d07ad48..d6ff46d 100644 --- a/COT/vm_description.py +++ b/COT/vm_description.py @@ -249,12 +249,14 @@ def convert_disk_if_needed(self, # pylint: disable=no-self-use """Convert the disk to a more appropriate format if needed. :param disk_image: Image to inspect and possibly convert - :type disk_image: :class:`~COT.disks.Disk` or subclass + :type disk_image: instance of :class:`~COT.disks.DiskRepresentation` + or subclass :param str kind: Image type (harddisk/cdrom). :return: * :attr:`disk_image`, if no conversion was required - * or a new :class:`~COT.disks.Disk` instance representing a converted - image that has been created in :attr:`output_dir`. + * or a new :class:`~COT.disks.DiskRepresentation` instance + representing a converted image that has been created in + :attr:`output_dir`. """ # Some VMs may not need this, so default to do nothing, not error return disk_image diff --git a/docs/COT.disks.disk.rst b/docs/COT.disks.disk.rst new file mode 100644 index 0000000..f9b446b --- /dev/null +++ b/docs/COT.disks.disk.rst @@ -0,0 +1,4 @@ +``COT.disks.disk`` module +========================= + +.. automodule:: COT.disks.disk diff --git a/docs/COT.disks.rst b/docs/COT.disks.rst index 4cbc7ea..288ebc2 100644 --- a/docs/COT.disks.rst +++ b/docs/COT.disks.rst @@ -2,4 +2,3 @@ =============================== .. automodule:: COT.disks - :no-members: From 01fcdd619b5e49ab31aeba440537cc72316b66e0 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 17 Oct 2016 20:26:55 -0400 Subject: [PATCH 24/59] Add helper_select() API --- COT/disks/iso.py | 12 ++++------- COT/disks/tests/test_iso.py | 26 ++++++++++++++++++++++- COT/disks/vmdk.py | 41 +++++++++++++++++-------------------- COT/helpers/__init__.py | 5 ++++- COT/helpers/fatdisk.py | 11 +++++----- COT/helpers/helper.py | 38 ++++++++++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 37 deletions(-) diff --git a/COT/disks/iso.py b/COT/disks/iso.py index 2cb3a6d..3f344b5 100644 --- a/COT/disks/iso.py +++ b/COT/disks/iso.py @@ -16,7 +16,7 @@ import re from COT.disks.disk import DiskRepresentation -from COT.helpers import helpers, HelperError +from COT.helpers import helpers, HelperError, helper_select class ISO(DiskRepresentation): @@ -83,14 +83,10 @@ def _create_file(self): if self._disk_subformat == 'rockridge': args.append('-r') args += self.files - # TODO require_any_of - if helpers['mkisofs']: - helpers['mkisofs'].call(args) - elif helpers['genisoimage']: - helpers['genisoimage'].call(args) - elif helpers['xorriso']: + helper = helper_select(['mkisofs', 'genisoimage', 'xorriso']) + if helper.name == "xorriso": args = ['-as', 'mkisofs'] + args - helpers['xorriso'].call(args) + helper.call(args) self._files = None diff --git a/COT/disks/tests/test_iso.py b/COT/disks/tests/test_iso.py index fd2f423..f9e6c98 100644 --- a/COT/disks/tests/test_iso.py +++ b/COT/disks/tests/test_iso.py @@ -22,7 +22,9 @@ from COT.tests.ut import COT_UT from COT.disks import ISO -from COT.helpers import helpers, HelperError +from COT.helpers import ( + helpers, package_managers, HelperError, HelperNotFoundError, +) logger = logging.getLogger(__name__) @@ -32,6 +34,15 @@ class TestISO(COT_UT): """Test cases for ISO class.""" + def tearDown(self): + """Test case cleanup function called automatically after each test.""" + for name in ['mkisofs', 'genisoimage', 'xorriso', 'isoinfo']: + helper = helpers[name] + helper._installed = None + helper._path = None + helper._version = None + super(TestISO, self).tearDown() + 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"), @@ -94,6 +105,19 @@ def test_create_with_xorriso(self, mock_call): ['-as', 'mkisofs', '-output', 'foo.iso', '-full-iso9660-filenames', '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) + def test_create_no_helpers_available(self): + """Creation of ISO should fail if no helpers are install[ed|able].""" + helpers['mkisofs']._installed = False + helpers['genisoimage']._installed = False + helpers['xorriso']._installed = False + package_managers['apt-get']._installed = False + package_managers['port']._installed = False + package_managers['yum']._installed = False + self.assertRaises(HelperNotFoundError, + ISO, + path='foo.iso', + files=[self.input_ovf]) + @mock.patch("COT.helpers.mkisofs.MkISOFS.call") def test_create_with_mkisofs_non_rockridge(self, mock_call): """Creation of a non-Rock-Ridge ISO with mkisofs (default).""" diff --git a/COT/disks/vmdk.py b/COT/disks/vmdk.py index a859c14..ad96e44 100644 --- a/COT/disks/vmdk.py +++ b/COT/disks/vmdk.py @@ -16,10 +16,8 @@ import os import re -from distutils.version import StrictVersion - from COT.disks.disk import DiskRepresentation -from COT.helpers import helpers, HelperNotFoundError +from COT.helpers import helpers, helper_select logger = logging.getLogger(__name__) @@ -62,33 +60,32 @@ def from_other_image(cls, input_image, output_dir, output_subformat=None): (file_prefix, _) = os.path.splitext(file_name) output_path = os.path.join(output_dir, file_prefix + ".vmdk") if output_subformat == "streamOptimized": - # TODO - if (helpers['qemu-img'] and - helpers['qemu-img'].version >= StrictVersion("2.1.0")): - helpers['qemu-img'].call(['convert', - '-O', 'vmdk', - '-o', 'subformat=streamOptimized', - input_image.path, - output_path]) - elif helpers['vmdktool']: + helper = helper_select([('qemu-img', '2.1.0'), 'vmdktool']) + if helper.name == 'qemu-img': + helper.call(['convert', + '-O', 'vmdk', + '-o', 'subformat=streamOptimized', + input_image.path, + output_path]) + elif helper.name == 'vmdktool': if input_image.disk_format != 'raw': # vmdktool needs a raw image as input from COT.disks import RAW - temp_image = RAW.from_other_image(input_image, output_dir) - output_image = cls.from_other_image(temp_image, - output_dir, - output_subformat) - os.remove(temp_image.path) + try: + temp_image = RAW.from_other_image(input_image, + output_dir) + output_image = cls.from_other_image(temp_image, + output_dir, + output_subformat) + finally: + os.remove(temp_image.path) return output_image # Note that vmdktool takes its arguments in unusual order - # output file comes before input file - helpers['vmdktool'].call(['-z9', - '-v', output_path, - input_image.path]) - else: - raise HelperNotFoundError("No helper program available.") + helper.call(['-z9', '-v', output_path, input_image.path]) else: + # TODO: support at least monolithicSparse! raise NotImplementedError("No support for subformat '%s'", output_subformat) return cls(output_path) diff --git a/COT/helpers/__init__.py b/COT/helpers/__init__.py index d8b72a6..16ed11e 100644 --- a/COT/helpers/__init__.py +++ b/COT/helpers/__init__.py @@ -21,6 +21,7 @@ helpers package_managers + helper_select Exceptions ---------- @@ -52,7 +53,7 @@ from .helper import ( Helper, PackageManager, helpers, package_managers, - HelperError, HelperNotFoundError, + HelperError, HelperNotFoundError, helper_select, ) from .apt_get import AptGet # noqa from .fatdisk import FatDisk # noqa @@ -72,6 +73,7 @@ # pylint:disable=no-member +# Populate helpers and package_managers for cls in Helper.__subclasses__(): if cls is PackageManager: continue @@ -89,4 +91,5 @@ 'HelperNotFoundError', 'helpers', 'package_managers', + 'helper_select', ) diff --git a/COT/helpers/fatdisk.py b/COT/helpers/fatdisk.py index c3e9ced..b431216 100644 --- a/COT/helpers/fatdisk.py +++ b/COT/helpers/fatdisk.py @@ -24,7 +24,9 @@ import os.path import platform -from COT.helpers.helper import Helper, helpers, package_managers, check_call +from COT.helpers.helper import ( + Helper, helpers, package_managers, check_call, helper_select, +) logger = logging.getLogger(__name__) @@ -56,10 +58,9 @@ def _install(self): # Fatdisk installation requires make helpers['make'].install() - # Fatdisk requires clang or gcc or g++ - # TODO - if not (helpers['clang'] or helpers['gcc'] or helpers['g++']): - helpers['gcc'].install() + # Fatdisk build requires clang or gcc or g++, + # but COT doesn't care which one we have. + helper_select(['clang', 'gcc', 'g++']) with self.download_and_expand_tgz( 'https://github.com/goblinhack/' diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index ae27429..9e794b6 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -526,3 +526,41 @@ def check_output(args, require_success=True, retry_with_sudo=False, **kwargs): logger.info("...done") logger.verbose("%s output:\n%s", cmd, stdout) return stdout + + +def helper_select(choices): + """Select the first helper that is available from the given list. + + :param list choices: List of helpers, in order from most preferred to + least preferred. Each choice in this list can be a string (the helper + name, such as "mkisofs") or a tuple of (name, minimum version) such as + ("qemu-img", "2.1.0"). + :return: The selected helper class instance. + """ + for choice in choices: + if isinstance(choice, str): + # Helper name only, no version constraints + name = choice + min_version = None + else: + # Tuple of (name, version) + (name, vers) = choice + min_version = StrictVersion(vers) + if helpers[name]: + if min_version is None or helpers[name].version >= min_version: + return helpers[name] + + # OK, nothing yet installed. So what can we install? + for choice in choices: + if isinstance(choice, str): + name = choice + min_version = None + else: + (name, vers) = choice + min_version = StrictVersion(vers) + if helpers[name].installable: + helpers[name].install() + if min_version is None or helpers[name].version >= min_version: + return helpers[name] + + raise HelperNotFoundError("No helper available or installable!") From 72e613cec3218c50cf277787ee2c17b9b222378d Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 18 Oct 2016 15:46:35 -0400 Subject: [PATCH 25/59] Improve test coverage --- COT/helpers/fatdisk.py | 47 +++++++------- COT/helpers/gcc.py | 2 +- COT/helpers/helper.py | 37 +++++++---- COT/helpers/isoinfo.py | 2 +- COT/helpers/make.py | 2 +- COT/helpers/mkisofs.py | 6 +- COT/helpers/ovftool.py | 17 ++--- COT/helpers/qemu_img.py | 2 +- COT/helpers/tests/test_fatdisk.py | 2 + COT/helpers/tests/test_helper.py | 100 ++++++++++++++++++++++++++++- COT/helpers/tests/test_vmdktool.py | 2 + COT/helpers/vmdktool.py | 87 +++++++++++++------------ 12 files changed, 209 insertions(+), 97 deletions(-) diff --git a/COT/helpers/fatdisk.py b/COT/helpers/fatdisk.py index b431216..0adee33 100644 --- a/COT/helpers/fatdisk.py +++ b/COT/helpers/fatdisk.py @@ -54,27 +54,30 @@ def _install(self): """Install ``fatdisk``.""" if package_managers['port']: package_managers['port'].install_package('fatdisk') - elif platform.system() == 'Linux': - # Fatdisk installation requires make - helpers['make'].install() + return + elif platform.system() != 'Linux': + self.unsure_how_to_install() - # Fatdisk build requires clang or gcc or g++, - # but COT doesn't care which one we have. - helper_select(['clang', 'gcc', 'g++']) + # Fatdisk installation requires make + helpers['make'].install() - with self.download_and_expand_tgz( - 'https://github.com/goblinhack/' - 'fatdisk/archive/v1.0.0-beta.tar.gz') as d: - new_d = os.path.join(d, 'fatdisk-1.0.0-beta') - logger.info("Compiling 'fatdisk'") - check_call(['./RUNME'], cwd=new_d) - destdir = os.getenv('DESTDIR', '') - prefix = os.getenv('PREFIX', '/usr/local') - # os.path.join doesn't like absolute paths in the middle - if destdir != '': - prefix = prefix.lstrip(os.sep) - destination = os.path.join(destdir, prefix, 'bin') - logger.info("Compilation complete, installing to " + - destination) - self.mkdir(destination) - self.cp(os.path.join(new_d, 'fatdisk'), destination) + # Fatdisk build requires clang or gcc or g++, + # but COT doesn't care which one we have. + helper_select(['clang', 'gcc', 'g++']) + + with self.download_and_expand_tgz( + 'https://github.com/goblinhack/' + 'fatdisk/archive/v1.0.0-beta.tar.gz') as d: + new_d = os.path.join(d, 'fatdisk-1.0.0-beta') + logger.info("Compiling 'fatdisk'") + check_call(['./RUNME'], cwd=new_d) + destdir = os.getenv('DESTDIR', '') + prefix = os.getenv('PREFIX', '/usr/local') + # os.path.join doesn't like absolute paths in the middle + if destdir != '': + prefix = prefix.lstrip(os.sep) + destination = os.path.join(destdir, prefix, 'bin') + logger.info("Compilation complete, installing to " + + destination) + self.mkdir(destination) + self.cp(os.path.join(new_d, 'fatdisk'), destination) diff --git a/COT/helpers/gcc.py b/COT/helpers/gcc.py index 8810981..f73c3e3 100644 --- a/COT/helpers/gcc.py +++ b/COT/helpers/gcc.py @@ -22,7 +22,7 @@ class GCC(Helper): """Helper provider for ``gcc`` command.""" - _provider_packages = { + _provider_package = { 'apt-get': 'gcc', 'yum': 'gcc', } diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 9e794b6..450d504 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -163,6 +163,7 @@ class Helper(object): call install + unsure_how_to_install """ def __init__(self, name, @@ -194,7 +195,8 @@ def __bool__(self): # For Python 2.x compatibility: __nonzero__ = __bool__ - _provider_packages = {} + _provider_package = {} + """Mapping of package_manager to package name.""" UI = None """User interface (if any) available to helpers.""" @@ -232,7 +234,7 @@ def installed(self): @property def installable(self): """Whether COT is capable of installing this program on this system.""" - for pm_name in self._provider_packages: + for pm_name in self._provider_package: if package_managers[pm_name]: return True return False @@ -293,28 +295,35 @@ def install(self): if self.installed: return if not self.installable: - msg = "Unsure how to install {0}.".format(self.name) - if self.info_uri: - msg += "\nRefer to {0} for information".format(self.info_uri) - raise NotImplementedError(msg) + self.unsure_how_to_install() logger.info("Installing '%s'...", self.name) # Call the subclass implementation self._install() # Make sure it actually performed as promised - assert self.path, "after installing, path is {0}".format(self.path) + if not self.path: + raise HelperNotFoundError( + 1, + "Installation did not raise an exception, but afterward, " + "unable to locate {0}!".format(self.name)) logger.info("Successfully installed '%s'", self.name) + def unsure_how_to_install(self): + """Raise a NotImplementedError about missing install logic.""" + msg = "Unsure how to install {0}.".format(self.name) + if self.info_uri: + msg += "\nRefer to {0} for information".format(self.info_uri) + raise NotImplementedError(msg) + def _install(self): """Subclass-specific implementation of installation logic.""" # Default implementation - for pm_name, packages in self._provider_packages.items(): - if not package_managers[pm_name]: - continue - if isinstance(packages, str): - packages = [packages] - for pkg in packages: - package_managers[pm_name].install_package(pkg) + for pm_name, package in self._provider_package.items(): + if package_managers[pm_name]: + package_managers[pm_name].install_package(package) + return + # We shouldn't get here under normal call flow and logic. + self.unsure_how_to_install() @staticmethod @contextlib.contextmanager diff --git a/COT/helpers/isoinfo.py b/COT/helpers/isoinfo.py index 75c5149..638299f 100644 --- a/COT/helpers/isoinfo.py +++ b/COT/helpers/isoinfo.py @@ -28,7 +28,7 @@ class ISOInfo(Helper): http://cdrecord.org/ """ - _provider_packages = { + _provider_package = { 'apt-get': 'genisoimage', 'port': 'cdrtools', } diff --git a/COT/helpers/make.py b/COT/helpers/make.py index ba374e0..3e78b6e 100644 --- a/COT/helpers/make.py +++ b/COT/helpers/make.py @@ -22,7 +22,7 @@ class Make(Helper): """Helper provider for ``make`` command.""" - _provider_packages = { + _provider_package = { 'apt-get': 'make', 'yum': 'make', } diff --git a/COT/helpers/mkisofs.py b/COT/helpers/mkisofs.py index 9d59109..63fb0d7 100644 --- a/COT/helpers/mkisofs.py +++ b/COT/helpers/mkisofs.py @@ -38,7 +38,7 @@ class MkISOFS(Helper): http://cdrecord.org/ """ - _provider_packages = { + _provider_package = { 'port': 'cdrtools', } @@ -51,7 +51,7 @@ def __init__(self): class GenISOImage(Helper): """Helper provider for ``genisoimage``, a fork of mkisofs.""" - _provider_packages = { + _provider_package = { 'apt-get': 'genisoimage', 'yum': 'genisoimage', } @@ -69,7 +69,7 @@ class XorrISO(Helper): https://www.gnu.org/software/xorriso/ """ - _provider_packages = { + _provider_package = { 'apt-get': 'xorriso', } diff --git a/COT/helpers/ovftool.py b/COT/helpers/ovftool.py index bbdb568..2c36fab 100644 --- a/COT/helpers/ovftool.py +++ b/COT/helpers/ovftool.py @@ -40,17 +40,14 @@ def installable(self): """COT can't install ovftool because of VMware site restrictions.""" return False - def install(self): - """Install the helper program. - - :raise: :exc:`NotImplementedError` if not ``installable`` + def unsure_how_to_install(self): + """Raise a NotImplementedError about missing install logic. We override the default install implementation to raise a more detailed error message for ovftool. """ - if not self.installed: - raise NotImplementedError( - "No support for automated installation of ovftool, as VMware " - "requires a site login to download it. See " - "https://www.vmware.com/support/developer/ovf/" - ) + raise NotImplementedError( + "No support for automated installation of ovftool, as VMware " + "requires a site login to download it. See " + "https://www.vmware.com/support/developer/ovf/" + ) diff --git a/COT/helpers/qemu_img.py b/COT/helpers/qemu_img.py index a6e0fe9..e85410c 100644 --- a/COT/helpers/qemu_img.py +++ b/COT/helpers/qemu_img.py @@ -25,7 +25,7 @@ class QEMUImg(Helper): """Helper provider for ``qemu-img`` (http://www.qemu.org).""" - _provider_packages = { + _provider_package = { 'apt-get': 'qemu-utils', 'port': 'qemu', 'yum': 'qemu-img', diff --git a/COT/helpers/tests/test_fatdisk.py b/COT/helpers/tests/test_fatdisk.py index 29ec0cc..0e22c58 100644 --- a/COT/helpers/tests/test_fatdisk.py +++ b/COT/helpers/tests/test_fatdisk.py @@ -194,6 +194,8 @@ def test_install_linux_need_compiler_no_package_manager(self, self.helper.install() @mock.patch('platform.system', return_value='Darwin') + @mock.patch('COT.helpers.fatdisk.FatDisk.installable', + new_callable=mock.PropertyMock, return_value=True) def test_install_helper_mac_no_package_manager(self, *_): """Mac installation requires port.""" self.select_package_manager(None) diff --git a/COT/helpers/tests/test_helper.py b/COT/helpers/tests/test_helper.py index 562cd22..e291e99 100644 --- a/COT/helpers/tests/test_helper.py +++ b/COT/helpers/tests/test_helper.py @@ -20,6 +20,7 @@ import os import logging import subprocess +from distutils.version import StrictVersion import requests import mock @@ -28,9 +29,9 @@ from COT.tests.ut import COT_UT from COT.helpers.helper import TemporaryDirectory, check_call, check_output from COT.helpers import ( - Helper, + Helper, PackageManager, HelperError, HelperNotFoundError, - helpers, package_managers, + helpers, package_managers, helper_select ) from COT.helpers.port import Port from COT.helpers.apt_get import AptGet @@ -297,6 +298,19 @@ def test_install_already_present(self, mock_install): self.helper.install() mock_install.assert_not_called() + @mock.patch('COT.helpers.Helper.installable', + new_callable=mock.PropertyMock, return_value=True) + def test_install_no_package_managers(self, *_): + """If installable lies, default _install should fail cleanly.""" + self.assertRaises(NotImplementedError, self.helper.install) + + @mock.patch('COT.helpers.Helper._install') + @mock.patch('COT.helpers.Helper.installable', + new_callable=mock.PropertyMock, return_value=True) + def test_install_bad_implementation(self, *_): + """If custom _install() doesn't do its job, install() catches it.""" + self.assertRaises(HelperNotFoundError, self.helper.install) + def test_call_install(self): """call will call install, which raises an error.""" self.assertRaises(NotImplementedError, @@ -336,6 +350,20 @@ def test_download_and_expand_tgz(self): self.assertFalse(os.path.exists(directory)) +class PackageManagerGenericTest(HelperUT): + """Unit test for abstract PackageManager class.""" + + def setUp(self): + """Test case setup function called automatically prior to each test.""" + self.helper = PackageManager("generic") + super(PackageManagerGenericTest, self).setUp() + + def test_install_package_abstract(self): + """The install_package API is abstract.""" + self.assertRaises(NotImplementedError, + self.helper.install_package, "COT") + + @mock.patch('COT.helpers.helper.check_call') @mock.patch('os.makedirs') @mock.patch('os.path.exists', return_value=False) @@ -419,3 +447,71 @@ def test_need_sudo(self, mock_copy, mock_check_call): self.assertTrue(Helper.cp('/foo', '/bar')) mock_copy.assert_called_with('/foo', '/bar') mock_check_call.assert_called_with(['sudo', 'cp', '/foo', '/bar']) + + +class TestHelperSelect(COT_UT): + """Test cases for helper_select() API.""" + + def setUp(self): + """Fake out helper availability.""" + super(TestHelperSelect, self).setUp() + helpers['qemu-img']._installed = True + helpers['qemu-img']._version = StrictVersion("2.1.0") + helpers['vmdktool']._installed = True + helpers['vmdktool']._version = StrictVersion("1.4") + helpers['fatdisk']._installed = False + + def tearDown(self): + """Test case cleanup function called automatically after each test.""" + for helper in helpers.values(): + helper._installed = None + helper._path = None + helper._version = None + super(TestHelperSelect, self).tearDown() + + def test_select_name_only(self): + """Select a helper from a list of names only.""" + helper = helper_select(['fatdisk', 'vmdktool', 'qemu-img']) + self.assertEqual(helper, helpers['vmdktool']) + + def test_select_name_version(self): + """Select a helper from a list of names and versions.""" + helper = helper_select([('fatdisk', '1.4'), # not installed + ('vmdktool', '2.0'), # version too low + ('qemu-img', '2.1.0'), # just right + ]) + self.assertEqual(helper, helpers['qemu-img']) + + @mock.patch('COT.helpers.fatdisk.FatDisk.installable', + new_callable=mock.PropertyMock, return_value=False) + @mock.patch('COT.helpers.vmdktool.VMDKTool.installable', + new_callable=mock.PropertyMock, return_value=True) + @mock.patch('COT.helpers.vmdktool.VMDKTool.install') + def test_select_install_name_only(self, mock_install, *_): + """Select and install a helper from a list of names only.""" + helpers['vmdktool']._installed = False + helpers['qemu-img']._installed = False + helper = helper_select(['fatdisk', 'vmdktool', 'qemu-img']) + self.assertEqual(helper, helpers['vmdktool']) + mock_install.assert_called_once_with() + + @mock.patch('COT.helpers.fatdisk.FatDisk.installable', + new_callable=mock.PropertyMock, return_value=False) + @mock.patch('COT.helpers.vmdktool.VMDKTool.installable', + new_callable=mock.PropertyMock, return_value=True) + @mock.patch('COT.helpers.qemu_img.QEMUImg.installable', + new_callable=mock.PropertyMock, return_value=True) + @mock.patch('COT.helpers.qemu_img.QEMUImg.install') + @mock.patch('COT.helpers.vmdktool.VMDKTool.install') + def test_select_install_name_version(self, + mock_install_v, mock_install_q, *_): + """Select and install a helper from a list of names and versions.""" + helpers['vmdktool']._installed = False + helpers['qemu-img']._installed = False + helper = helper_select([('fatdisk', '1.4'), # not installable + ('vmdktool', '2.0'), # version too low + ('qemu-img', '2.1.0'), # just right + ]) + self.assertEqual(helper, helpers['qemu-img']) + mock_install_v.assert_called_once_with() + mock_install_q.assert_called_once_with() diff --git a/COT/helpers/tests/test_vmdktool.py b/COT/helpers/tests/test_vmdktool.py index f7d74e6..044b679 100644 --- a/COT/helpers/tests/test_vmdktool.py +++ b/COT/helpers/tests/test_vmdktool.py @@ -165,6 +165,8 @@ def test_install_linux_need_compiler_no_package_manager(self, self.assertRaises(NotImplementedError, self.helper.install) @mock.patch('platform.system', return_value='Darwin') + @mock.patch('COT.helpers.vmdktool.VMDKTool.installable', + new_callable=mock.PropertyMock, return_value=True) def test_install_helper_mac_no_package_manager(self, *_): """Mac installation requires port.""" self.select_package_manager(None) diff --git a/COT/helpers/vmdktool.py b/COT/helpers/vmdktool.py index b890e23..f65e0bb 100644 --- a/COT/helpers/vmdktool.py +++ b/COT/helpers/vmdktool.py @@ -46,50 +46,53 @@ def __init__(self): @property def installable(self): """Whether COT is capable of installing this program on this system.""" - return (package_managers['apt-get'] or - package_managers['port'] or - package_managers['yum']) + return bool(package_managers['apt-get'] or + package_managers['port'] or + package_managers['yum']) def _install(self): """Install ``vmdktool``.""" if package_managers['port']: package_managers['port'].install_package('vmdktool') - elif platform.system() == 'Linux': - # We don't have vmdktool in apt or yum yet, - # but we can build it manually: - # vmdktool requires make and zlib - helpers['make'].install() - # TODO: check for installed zlib? - logger.info("vmdktool requires 'zlib'... installing 'zlib'") - if package_managers['apt-get']: - package_managers['apt-get'].install_package('zlib1g-dev') - elif package_managers['yum']: - package_managers['yum'].install_package('zlib-devel') - else: - raise NotImplementedError("Not sure how to install 'zlib'") - with self.download_and_expand_tgz( - 'http://people.freebsd.org/~brian/' - 'vmdktool/vmdktool-1.4.tar.gz') as d: - new_d = os.path.join(d, "vmdktool-1.4") - logger.info("Compiling 'vmdktool'") - # vmdktool is originally a BSD tool so it has some build - # assumptions that aren't necessarily correct under Linux. - # The easiest workaround is to override the CFLAGS to: - # 1) add -D_GNU_SOURCE - # 2) not treat all warnings as errors - check_call(['make', - 'CFLAGS="-D_GNU_SOURCE -g -O -pipe"'], - cwd=new_d) - destdir = os.getenv('DESTDIR', '') - prefix = os.getenv('PREFIX', '/usr/local') - args = ['make', 'install', 'PREFIX=' + prefix] - if destdir != '': - args.append('DESTDIR=' + destdir) - # os.path.join doesn't like absolute paths in the middle - prefix = prefix.lstrip(os.sep) - logger.info("Compilation complete, installing to " + - os.path.join(destdir, prefix)) - # Make sure the relevant man and bin directories exist - self.mkdir(os.path.join(destdir, prefix, 'man', 'man8')) - self.mkdir(os.path.join(destdir, prefix, 'bin')) - check_call(args, retry_with_sudo=True, cwd=new_d) + return + elif platform.system() != 'Linux': + self.unsure_how_to_install() + + # We don't have vmdktool in apt or yum yet, + # but we can build it manually: + # vmdktool requires make and zlib + helpers['make'].install() + # TODO: check for installed zlib? + logger.info("vmdktool requires 'zlib'... installing 'zlib'") + if package_managers['apt-get']: + package_managers['apt-get'].install_package('zlib1g-dev') + elif package_managers['yum']: + package_managers['yum'].install_package('zlib-devel') + else: + raise NotImplementedError("Not sure how to install 'zlib'") + with self.download_and_expand_tgz( + 'http://people.freebsd.org/~brian/vmdktool/vmdktool-1.4.tar.gz' + ) as d: + new_d = os.path.join(d, "vmdktool-1.4") + logger.info("Compiling 'vmdktool'") + # vmdktool is originally a BSD tool so it has some build + # assumptions that aren't necessarily correct under Linux. + # The easiest workaround is to override the CFLAGS to: + # 1) add -D_GNU_SOURCE + # 2) not treat all warnings as errors + check_call(['make', + 'CFLAGS="-D_GNU_SOURCE -g -O -pipe"'], + cwd=new_d) + destdir = os.getenv('DESTDIR', '') + prefix = os.getenv('PREFIX', '/usr/local') + args = ['make', 'install', 'PREFIX=' + prefix] + if destdir != '': + args.append('DESTDIR=' + destdir) + # os.path.join doesn't like absolute paths in the middle + prefix = prefix.lstrip(os.sep) + logger.info("Compilation complete, installing to " + + os.path.join(destdir, prefix)) + # Make sure the relevant man and bin directories exist + self.mkdir(os.path.join(destdir, prefix, 'man', 'man8')) + self.mkdir(os.path.join(destdir, prefix, 'bin')) + check_call(args, retry_with_sudo=True, cwd=new_d) From f021b4f57fe13d83f55f9151da126210e11fdae0 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 20 Oct 2016 09:41:45 -0400 Subject: [PATCH 26/59] API simplification - merge package_managers into helpers --- COT/disks/tests/test_iso.py | 8 ++++---- COT/helpers/__init__.py | 9 ++++----- COT/helpers/fatdisk.py | 18 ++++++++---------- COT/helpers/helper.py | 18 +++++++++--------- COT/helpers/tests/test_helper.py | 6 +++--- COT/helpers/vmdktool.py | 18 ++++++++---------- COT/tests/test_install_helpers.py | 8 ++++---- 7 files changed, 40 insertions(+), 45 deletions(-) diff --git a/COT/disks/tests/test_iso.py b/COT/disks/tests/test_iso.py index f9e6c98..20d35c3 100644 --- a/COT/disks/tests/test_iso.py +++ b/COT/disks/tests/test_iso.py @@ -23,7 +23,7 @@ from COT.tests.ut import COT_UT from COT.disks import ISO from COT.helpers import ( - helpers, package_managers, HelperError, HelperNotFoundError, + helpers, HelperError, HelperNotFoundError, ) logger = logging.getLogger(__name__) @@ -110,9 +110,9 @@ def test_create_no_helpers_available(self): helpers['mkisofs']._installed = False helpers['genisoimage']._installed = False helpers['xorriso']._installed = False - package_managers['apt-get']._installed = False - package_managers['port']._installed = False - package_managers['yum']._installed = False + helpers['apt-get']._installed = False + helpers['port']._installed = False + helpers['yum']._installed = False self.assertRaises(HelperNotFoundError, ISO, path='foo.iso', diff --git a/COT/helpers/__init__.py b/COT/helpers/__init__.py index 16ed11e..aa4a5f6 100644 --- a/COT/helpers/__init__.py +++ b/COT/helpers/__init__.py @@ -20,7 +20,6 @@ :nosignatures: helpers - package_managers helper_select Exceptions @@ -52,7 +51,7 @@ """ from .helper import ( - Helper, PackageManager, helpers, package_managers, + Helper, PackageManager, helpers, HelperError, HelperNotFoundError, helper_select, ) from .apt_get import AptGet # noqa @@ -73,9 +72,10 @@ # pylint:disable=no-member -# Populate helpers and package_managers +# Populate helpers dictionary for cls in Helper.__subclasses__(): if cls is PackageManager: + # Don't record the abstract class! continue ins = cls() helpers[ins.name] = ins @@ -83,13 +83,12 @@ for cls in PackageManager.__subclasses__(): ins = cls() - package_managers[ins.name] = ins + helpers[ins.name] = ins __all__ = ( 'HelperError', 'HelperNotFoundError', 'helpers', - 'package_managers', 'helper_select', ) diff --git a/COT/helpers/fatdisk.py b/COT/helpers/fatdisk.py index 0adee33..465a6c3 100644 --- a/COT/helpers/fatdisk.py +++ b/COT/helpers/fatdisk.py @@ -24,9 +24,7 @@ import os.path import platform -from COT.helpers.helper import ( - Helper, helpers, package_managers, check_call, helper_select, -) +from COT.helpers.helper import Helper, helpers, check_call, helper_select logger = logging.getLogger(__name__) @@ -44,16 +42,16 @@ def __init__(self): @property def installable(self): """Whether COT is capable of installing this program on this system.""" - return (package_managers['port'] or - (platform.system() == 'Linux' and - (helpers['make'] or helpers['make'].installable) and - (helpers['clang'] or helpers['gcc'] or - helpers['g++'] or helpers['gcc'].installable))) + return bool(helpers['port'] or + (platform.system() == 'Linux' and + (helpers['make'] or helpers['make'].installable) and + (helpers['clang'] or helpers['gcc'] or + helpers['g++'] or helpers['gcc'].installable))) def _install(self): """Install ``fatdisk``.""" - if package_managers['port']: - package_managers['port'].install_package('fatdisk') + if helpers['port']: + helpers['port'].install_package('fatdisk') return elif platform.system() != 'Linux': self.unsure_how_to_install() diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 450d504..d81d483 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -35,7 +35,6 @@ :nosignatures: helpers - package_managers **Functions** @@ -196,7 +195,7 @@ def __bool__(self): __nonzero__ = __bool__ _provider_package = {} - """Mapping of package_manager to package name.""" + """Mapping of package manager name to package name to install with it.""" UI = None """User interface (if any) available to helpers.""" @@ -235,7 +234,7 @@ def installed(self): def installable(self): """Whether COT is capable of installing this program on this system.""" for pm_name in self._provider_package: - if package_managers[pm_name]: + if helpers[pm_name]: return True return False @@ -319,8 +318,8 @@ def _install(self): """Subclass-specific implementation of installation logic.""" # Default implementation for pm_name, package in self._provider_package.items(): - if package_managers[pm_name]: - package_managers[pm_name].install_package(package) + if helpers[pm_name]: + helpers[pm_name].install_package(package) return # We shouldn't get here under normal call flow and logic. self.unsure_how_to_install() @@ -430,10 +429,6 @@ def install_package(self, package): raise NotImplementedError("install_package not implemented!") -package_managers = HelperDict(PackageManager) -"""Dictionary of concrete PackageManager subclasses, populated at load time.""" - - def check_call(args, require_success=True, retry_with_sudo=False, **kwargs): """Wrapper for :func:`subprocess.check_call`. @@ -540,6 +535,11 @@ def check_output(args, require_success=True, retry_with_sudo=False, **kwargs): def helper_select(choices): """Select the first helper that is available from the given list. + If no helper in the list is currently installed, will install the + first installable helper from the list. + + :raise HelperNotFoundError: if no valid helper is available or installable. + :param list choices: List of helpers, in order from most preferred to least preferred. Each choice in this list can be a string (the helper name, such as "mkisofs") or a tuple of (name, minimum version) such as diff --git a/COT/helpers/tests/test_helper.py b/COT/helpers/tests/test_helper.py index e291e99..e8c0d87 100644 --- a/COT/helpers/tests/test_helper.py +++ b/COT/helpers/tests/test_helper.py @@ -31,7 +31,7 @@ from COT.helpers import ( Helper, PackageManager, HelperError, HelperNotFoundError, - helpers, package_managers, helper_select + helpers, helper_select ) from COT.helpers.port import Port from COT.helpers.apt_get import AptGet @@ -71,8 +71,8 @@ def set_helper_version(self, ver): @staticmethod def select_package_manager(name): """Select the specified installer program for Helper to use.""" - for pm_name in package_managers: - package_managers[pm_name]._installed = (pm_name == name) + for pm_name in ['apt-get', 'port', 'yum']: + helpers[pm_name]._installed = (pm_name == name) def enable_apt_install(self): """Set flags and values to force an apt-get update and apt install.""" diff --git a/COT/helpers/vmdktool.py b/COT/helpers/vmdktool.py index f65e0bb..e1d560e 100644 --- a/COT/helpers/vmdktool.py +++ b/COT/helpers/vmdktool.py @@ -24,7 +24,7 @@ import os.path import platform -from COT.helpers.helper import Helper, helpers, package_managers, check_call +from COT.helpers.helper import Helper, helpers, check_call logger = logging.getLogger(__name__) @@ -46,14 +46,12 @@ def __init__(self): @property def installable(self): """Whether COT is capable of installing this program on this system.""" - return bool(package_managers['apt-get'] or - package_managers['port'] or - package_managers['yum']) + return bool(helpers['apt-get'] or helpers['port'] or helpers['yum']) def _install(self): """Install ``vmdktool``.""" - if package_managers['port']: - package_managers['port'].install_package('vmdktool') + if helpers['port']: + helpers['port'].install_package('vmdktool') return elif platform.system() != 'Linux': self.unsure_how_to_install() @@ -64,10 +62,10 @@ def _install(self): helpers['make'].install() # TODO: check for installed zlib? logger.info("vmdktool requires 'zlib'... installing 'zlib'") - if package_managers['apt-get']: - package_managers['apt-get'].install_package('zlib1g-dev') - elif package_managers['yum']: - package_managers['yum'].install_package('zlib-devel') + if helpers['apt-get']: + helpers['apt-get'].install_package('zlib1g-dev') + elif helpers['yum']: + helpers['yum'].install_package('zlib-devel') else: raise NotImplementedError("Not sure how to install 'zlib'") with self.download_and_expand_tgz( diff --git a/COT/tests/test_install_helpers.py b/COT/tests/test_install_helpers.py index ca0df8e..235fe72 100644 --- a/COT/tests/test_install_helpers.py +++ b/COT/tests/test_install_helpers.py @@ -25,7 +25,7 @@ from COT.tests.ut import COT_UT from COT.ui_shared import UI from COT.install_helpers import COTInstallHelpers -from COT.helpers import HelperError, helpers, package_managers +from COT.helpers import HelperError, helpers from COT.helpers.apt_get import AptGet from COT.helpers.port import Port @@ -147,11 +147,11 @@ def stub_install(package): raise HelperError(1, "not really installing!") mock_find_executable.side_effect = stub_find_executable - package_managers['apt-get']._installed = True + helpers['apt-get']._installed = True mock_apt_install.side_effect = stub_install - package_managers['port']._installed = True + helpers['port']._installed = True mock_port_install.side_effect = stub_install - package_managers['yum']._installed = True + helpers['yum']._installed = True mock_yum_install.side_effect = stub_install AptGet._updated = False Port._updated = False From 9fdfa713a30267252b5b20f0035404eb19ae3fab Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 20 Oct 2016 17:09:22 -0400 Subject: [PATCH 27/59] More user thanks --- docs/thanks.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/thanks.rst b/docs/thanks.rst index 7649de0..5ef465f 100644 --- a/docs/thanks.rst +++ b/docs/thanks.rst @@ -5,15 +5,20 @@ We would like to thank: * For evangelization, user feedback and bug reports: + * Sean Adams * Arun Arunkumar * Mark Coverdill * Myles Dear + * Chandu Gutti + * Jeff Haag * Jeff Loughridge * Jonathan Muslow * Rick Ogg + * Keerthi Rawat * David Rosenfeld * Rafal Skorka * Perumal Venkatesh + * John Withington * For initial design review and comments: From f977d0dd1baac7d78bcc4530a29f30e92ac8c66d Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 24 Oct 2016 10:05:47 -0400 Subject: [PATCH 28/59] More minor cleanup --- COT/add_disk.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/COT/add_disk.py b/COT/add_disk.py index 4fe5a3f..99d0582 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -251,9 +251,9 @@ def create_subparser(self): p.set_defaults(instance=self) -def guess_disk_type_from_extension(disk_file): +def guess_disk_type_from_extension(disk_file_name): """Guess the disk type (harddisk/cdrom) from the disk file name.""" - disk_extension = os.path.splitext(disk_file)[1] + disk_extension = os.path.splitext(disk_file_name)[1] ext_type_map = { '.iso': 'cdrom', '.vmdk': 'harddisk', @@ -268,7 +268,7 @@ def guess_disk_type_from_extension(disk_file): "Unable to guess disk type for file '{0}' from its extension " "'{1}'.\nKnown extensions are {2}\n" "Please specify '--type harddisk' or '--type cdrom'." - .format(disk_file, disk_extension, ext_type_map.keys())) + .format(disk_file_name, disk_extension, ext_type_map.keys())) return disk_type @@ -477,20 +477,18 @@ def add_disk_worker(vm, :param str diskname: Name for disk device :param str description: Description of disk device """ - disk_path = disk_image.path if disk_type is None: - disk_type = guess_disk_type_from_extension(disk_path) + disk_type = guess_disk_type_from_extension(disk_image.path) logger.warning("New disk type not specified, guessing it should " "be '%s' based on file extension", disk_type) # Convert the disk to a new format if needed... disk_image = vm.convert_disk_if_needed(disk_image, disk_type) - disk_path = disk_image.path - disk_file = os.path.basename(disk_path) + disk_filename = os.path.basename(disk_image.path) (file_obj, disk, ctrl_item, disk_item) = \ - search_for_elements(vm, disk_file, file_id, controller, address) + search_for_elements(vm, disk_filename, file_id, controller, address) if controller is None: controller = guess_controller_type(vm, ctrl_item, disk_type) @@ -504,8 +502,8 @@ def add_disk_worker(vm, validate_elements(vm, file_obj, disk, disk_item, ctrl_item, file_id, controller) - confirm_elements(vm, ui, file_obj, disk_path, disk, disk_item, disk_type, - controller, ctrl_item, subtype) + confirm_elements(vm, ui, file_obj, disk_image.path, disk, disk_item, + disk_type, controller, ctrl_item, subtype) # OK - let's add things! if file_id is None and file_obj is not None: @@ -513,10 +511,10 @@ def add_disk_worker(vm, if file_id is None and disk is not None: file_id = vm.get_file_ref_from_disk(disk) if file_id is None: - file_id = disk_file + file_id = disk_filename # First, the File - file_obj = vm.add_file(disk_path, file_id, file_obj, disk) + file_obj = vm.add_file(disk_image.path, file_id, file_obj, disk) # Next, the Disk disk = vm.add_disk(disk_image, file_id, disk_type, disk) From 20dbd0b21cafd3e104619cb508f3bfb927449f9e Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 24 Oct 2016 12:25:23 -0400 Subject: [PATCH 29/59] More cleanup and simplification --- .pylintrc | 2 +- COT/add_disk.py | 76 ++++++++++++++++---------------- COT/inject_config.py | 6 +-- COT/ovf/hardware.py | 2 +- COT/ovf/item.py | 14 ++++++ COT/ovf/ovf.py | 68 ++++++++++------------------ COT/tests/test_add_disk.py | 6 +-- COT/tests/test_vm_description.py | 4 -- COT/tests/ut.py | 6 +-- COT/vm_description.py | 28 +++--------- 10 files changed, 92 insertions(+), 120 deletions(-) diff --git a/.pylintrc b/.pylintrc index 74264cb..1d4a714 100644 --- a/.pylintrc +++ b/.pylintrc @@ -57,7 +57,7 @@ max-attributes=29 max-locals=29 # default: max-public-methods=20 # current worst offender: OVF -max-public-methods=75 +max-public-methods=73 # default: max-returns=6 # default: max-statements=50 diff --git a/COT/add_disk.py b/COT/add_disk.py index 99d0582..213eef7 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -24,7 +24,7 @@ add_disk_worker confirm_elements guess_controller_type - guess_disk_type_from_extension + guess_drive_type_from_extension search_for_elements validate_elements validate_controller_address @@ -83,7 +83,7 @@ class COTAddDisk(COTSubmodule): Attributes: :attr:`disk_image`, - :attr:`type`, + :attr:`drive_type`, :attr:`file_id`, :attr:`controller`, :attr:`subtype`, @@ -96,8 +96,8 @@ def __init__(self, ui): """Instantiate this submodule with the given UI.""" super(COTAddDisk, self).__init__(ui) self._disk_image = None - self.disk_type = None - """Disk type ('harddisk' or 'cdrom').""" + self.drive_type = None + """Disk drive type ('harddisk' or 'cdrom').""" self.subtype = None """Controller subtype, such as "virtio".""" self.file_id = None @@ -173,7 +173,7 @@ def run(self): add_disk_worker(self.vm, ui=self.UI, disk_image=self.disk_image, - disk_type=self.disk_type, + drive_type=self.drive_type, subtype=self.subtype, file_id=self.file_id, controller=self.controller, @@ -215,10 +215,10 @@ def create_subparser(self): help="""Disk image file ID string within the OVF """ """package (default: use disk image filename)""") group.add_argument('-t', '--type', - dest='disk_type', + dest='drive_type', choices=['harddisk', 'cdrom'], - help="""Disk type (default: files ending in """ - """.vmdk/.raw/.qcow2/.img will use harddisk """ + help="""Disk drive type (default: files ending """ + """in .vmdk/.raw/.qcow2/.img will use harddisk """ """and files ending in .iso will use cdrom)""") group = p.add_argument_group("controller-related options") @@ -226,7 +226,7 @@ def create_subparser(self): group.add_argument('-c', '--controller', choices=['ide', 'scsi'], help="""Disk controller type (default: """ - """determined by disk type and platform)""") + """determined by disk drive type and platform)""") group.add_argument('-a', '--address', type=device_address, help="""Address of the disk, such as "1:0". """ """Requires that --controller be explicitly set. """ @@ -251,8 +251,8 @@ def create_subparser(self): p.set_defaults(instance=self) -def guess_disk_type_from_extension(disk_file_name): - """Guess the disk type (harddisk/cdrom) from the disk file name.""" +def guess_drive_type_from_extension(disk_file_name): + """Guess the disk drive type (harddisk/cdrom) from the disk file name.""" disk_extension = os.path.splitext(disk_file_name)[1] ext_type_map = { '.iso': 'cdrom', @@ -262,14 +262,14 @@ def guess_disk_type_from_extension(disk_file_name): '.img': 'harddisk', } try: - disk_type = ext_type_map[disk_extension] + drive_type = ext_type_map[disk_extension] except KeyError: raise InvalidInputError( - "Unable to guess disk type for file '{0}' from its extension " - "'{1}'.\nKnown extensions are {2}\n" + "Unable to guess disk drive type for file '{0}' from its " + "extension '{1}'.\nKnown extensions are {2}\n" "Please specify '--type harddisk' or '--type cdrom'." .format(disk_file_name, disk_extension, ext_type_map.keys())) - return disk_type + return drive_type def search_for_elements(vm, disk_file, file_id, controller, address): @@ -328,19 +328,19 @@ def search_for_elements(vm, disk_file, file_id, controller, address): return file_obj, disk_obj, ctrl_item, disk_item -def guess_controller_type(vm, ctrl_item, disk_type): +def guess_controller_type(vm, ctrl_item, drive_type): """If a controller type wasn't specified, try to guess from context.""" if ctrl_item is None: # If the user didn't tell us which controller type they wanted, # and we didn't find a controller item based on existing file/disk, # then we need to guess which type of controller we need, - # based on the platform and the disk type. - ctrl_type = vm.platform.controller_type_for_device(disk_type) + # based on the platform and the disk drive type. + ctrl_type = vm.platform.controller_type_for_device(drive_type) logger.warning("Guessing controller type should be %s " - "based on disk type %s and platform %s", - ctrl_type, disk_type, vm.platform.__name__) + "based on disk drive type %s and platform %s", + ctrl_type, drive_type, vm.platform.__name__) else: - ctrl_type = vm.get_type_from_device(ctrl_item) + ctrl_type = ctrl_item.hardware_type if ctrl_type != 'ide' and ctrl_type != 'scsi': raise ValueUnsupportedError("controller ResourceType", ctrl_type, @@ -383,7 +383,7 @@ def validate_elements(vm, file_obj, disk_obj, disk_item, ctrl_item, if ctrl_item is not None: match_or_die("controller type", - vm.get_type_from_device(ctrl_item), + ctrl_item.hardware_type, "--controller", ctrl_type) # Whew! Everything looks sane! @@ -391,7 +391,7 @@ def validate_elements(vm, file_obj, disk_obj, disk_item, ctrl_item, def confirm_elements(vm, ui, file_obj, disk_image, disk_obj, disk_item, - disk_type, controller, ctrl_item, subtype): + drive_type, controller, ctrl_item, subtype): """Get user confirmation of any risky or unusual operations.""" # TODO: more refactoring! if file_obj is not None: @@ -403,22 +403,22 @@ def confirm_elements(vm, ui, file_obj, disk_image, disk_obj, disk_item, if file_obj is None and (disk_obj is not None or disk_item is not None): ui.confirm_or_die( "Add disk file to existing (but empty) {0} drive?" - .format(disk_type)) + .format(drive_type)) if disk_obj is not None: logger.warning("Overwriting existing Disk in OVF") if disk_item is not None: ui.confirm_or_die("Existing disk Item is a {0}. Change it to a {1}?" - .format(vm.get_type_from_device(disk_item), - disk_type)) + .format(disk_item.hardware_type, + drive_type)) # We'll overwrite the existing disk Item instead of deleting # and recreating it, in order to preserve things like Description logger.warning("Overwriting existing disk Item in OVF") if ctrl_item is not None: if subtype is not None: - curr_subtype = vm.get_subtype_from_device(ctrl_item) + curr_subtype = ctrl_item.hardware_subtype if curr_subtype is not None and curr_subtype != subtype: ui.confirm_or_die("Change {0} controller subtype from " "'{1}' to '{2}'?".format(controller, @@ -433,7 +433,7 @@ def confirm_elements(vm, ui, file_obj, disk_image, disk_obj, disk_item, def add_disk_worker(vm, ui, disk_image, - disk_type=None, + drive_type=None, file_id=None, controller=None, subtype=None, @@ -456,7 +456,7 @@ def add_disk_worker(vm, :type disk_image: instance of :class:`~COT.disks.DiskRepresentation` or subclass. - :param str disk_type: Disk type: ``'cdrom'`` or ``'harddisk'``. + :param str drive_type: Disk drive type: ``'cdrom'`` or ``'harddisk'``. If not specified, will be derived automatically from the disk_image file name extension. @@ -477,13 +477,13 @@ def add_disk_worker(vm, :param str diskname: Name for disk device :param str description: Description of disk device """ - if disk_type is None: - disk_type = guess_disk_type_from_extension(disk_image.path) - logger.warning("New disk type not specified, guessing it should " - "be '%s' based on file extension", disk_type) + if drive_type is None: + drive_type = guess_drive_type_from_extension(disk_image.path) + logger.warning("New disk drive type not specified, guessing it should " + "be '%s' based on file extension", drive_type) # Convert the disk to a new format if needed... - disk_image = vm.convert_disk_if_needed(disk_image, disk_type) + disk_image = vm.convert_disk_if_needed(disk_image, drive_type) disk_filename = os.path.basename(disk_image.path) @@ -491,7 +491,7 @@ def add_disk_worker(vm, search_for_elements(vm, disk_filename, file_id, controller, address) if controller is None: - controller = guess_controller_type(vm, ctrl_item, disk_type) + controller = guess_controller_type(vm, ctrl_item, drive_type) if ctrl_item is None and address is None: # We didn't find a specific controller from the user info, @@ -503,7 +503,7 @@ def add_disk_worker(vm, file_id, controller) confirm_elements(vm, ui, file_obj, disk_image.path, disk, disk_item, - disk_type, controller, ctrl_item, subtype) + drive_type, controller, ctrl_item, subtype) # OK - let's add things! if file_id is None and file_obj is not None: @@ -517,7 +517,7 @@ def add_disk_worker(vm, file_obj = vm.add_file(disk_image.path, file_id, file_obj, disk) # Next, the Disk - disk = vm.add_disk(disk_image, file_id, disk_type, disk) + disk = vm.add_disk(disk_image, file_id, drive_type, disk) # Next, the controller (if needed) if address is not None: @@ -538,5 +538,5 @@ def add_disk_worker(vm, ctrl_addr, ctrl_item) # Finally, the disk Item - vm.add_disk_device(disk_type, disk_addr, diskname, + vm.add_disk_device(drive_type, disk_addr, diskname, description, disk, file_obj, ctrl_item, disk_item) diff --git a/COT/inject_config.py b/COT/inject_config.py index a002758..8f19a5b 100644 --- a/COT/inject_config.py +++ b/COT/inject_config.py @@ -139,7 +139,7 @@ def run(self): elif platform.BOOTSTRAP_DISK_TYPE == 'harddisk': (f, _, _, drive_device) = vm.search_from_filename('config.vmdk') else: - raise ValueUnsupportedError("bootstrap disk type", + raise ValueUnsupportedError("bootstrap disk drive type", platform.BOOTSTRAP_DISK_TYPE, "'cdrom' or 'harddisk'") if f is not None: @@ -187,7 +187,7 @@ def run(self): path=bootstrap_file, files=config_files) else: - raise ValueUnsupportedError("bootstrap disk type", + raise ValueUnsupportedError("bootstrap disk drive type", platform.BOOTSTRAP_DISK_TYPE, "'cdrom' or 'harddisk'") @@ -196,7 +196,7 @@ def run(self): ui=self.UI, vm=vm, disk_image=disk_image, - disk_type=platform.BOOTSTRAP_DISK_TYPE, + drive_type=platform.BOOTSTRAP_DISK_TYPE, file_id=file_id, controller=cont_type, address=drive_address, diff --git a/COT/ovf/hardware.py b/COT/ovf/hardware.py index 1b08f14..b08d494 100644 --- a/COT/ovf/hardware.py +++ b/COT/ovf/hardware.py @@ -333,7 +333,7 @@ def _update_cloned_item(self, new_item, new_item_profiles, item_count): Helper method for :meth:`set_item_count_per_profile`. """ - resource_type = self.ovf.get_type_from_device(new_item) + resource_type = new_item.hardware_type address = new_item.get(self.ovf.ADDRESS) if address: raise NotImplementedError("Don't know how to ensure a unique " diff --git a/COT/ovf/item.py b/COT/ovf/item.py index 6cbec27..1884eec 100644 --- a/COT/ovf/item.py +++ b/COT/ovf/item.py @@ -118,6 +118,20 @@ def property_names(self): """List of names of all properties known to this OVFItem.""" return list(self.properties.keys()) + @property + def hardware_type(self): + """Device hardware type such as 'ide' or 'memory'.""" + value = self.get_value(self.RESOURCE_TYPE) + for key in self.RES_MAP: + if value == self.RES_MAP[key]: + return key + return "unknown ({0})".format(value) + + @property + def hardware_subtype(self): + """Device hardware subtype such as 'virtio' or 'lsilogic'.""" + return self.get_value(self.RESOURCE_SUB_TYPE) + def property_values(self, name): """Get list of values known for a given property name.""" return list(self.properties[name].keys()) diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index 5a29ee6..d83a96d 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -1265,11 +1265,10 @@ def device_info_str(self, device_item): ctrl_type = "(?)" ctrl_addr = "?" else: - ctrl_type = self.get_type_from_device( - controller_item).upper() + ctrl_type = controller_item.hardware_type.upper() ctrl_addr = controller_item.get_value(self.ADDRESS) return "{0} @ {1} {2}:{3}".format( - self.get_type_from_device(device_item), + device_item.hardware_type, ctrl_type, ctrl_addr, device_item.get_value(self.ADDRESS_ON_PARENT)) @@ -2003,27 +2002,6 @@ def get_file_ref_from_disk(self, disk): """ return disk.get(self.DISK_FILE_REF) - def get_type_from_device(self, device): - """Get the type of the given device. - - :param OVFItem device: Device object to query - :return: string such as 'ide' or 'memory' - """ - device_type = device.get_value(self.RESOURCE_TYPE) - for key in self.RES_MAP: - if device_type == self.RES_MAP[key]: - return key - return "unknown ({0})".format(device_type) - - def get_subtype_from_device(self, device): - """Get the sub-type of the given opaque device object. - - :param OVFItem device: Device object to query - :return: ``None``, or string such as 'virtio' or 'lsilogic', or - list of strings - """ - return device.get_value(self.RESOURCE_SUB_TYPE) - def get_common_subtype(self, device_type): """Get the sub-type common to all devices of the given type. @@ -2176,19 +2154,19 @@ def remove_file(self, file_obj, disk=None, disk_drive=None): .format(self.RES_MAP['cdrom'], self.RES_MAP['harddisk'])) - def add_disk(self, disk_repr, file_id, disk_type, disk=None): + def add_disk(self, disk_repr, file_id, drive_type, disk=None): """Add a new disk object to the VM or overwrite the provided one. :param str disk_repr: Disk file representation :type disk_repr: COT.disks.DiskRepresentation or subclass :param str file_id: Identifier string for the file/disk mapping - :param str disk_type: 'harddisk' or 'cdrom' + :param str drive_type: 'harddisk' or 'cdrom' :param disk: Existing disk object to overwrite :type disk: xml.etree.ElementTree.Element :return: New or updated disk object """ - if disk_type != 'harddisk': + if drive_type != 'harddisk': if disk is not None: logger.warning("CD-ROMs do not require a Disk element. " "Existing element will be deleted.") @@ -2268,7 +2246,7 @@ def add_controller_device(self, device_type, subtype, address, ctrl_item.set_property(self.RESOURCE_SUB_TYPE, subtype) return ctrl_item - def _create_new_disk_device(self, disk_type, address, name, ctrl_item): + def _create_new_disk_device(self, drive_type, address, name, ctrl_item): """Helper for :meth:`add_disk_device`, in the case of no prior Item.""" ctrl_instance = ctrl_item.get_value(self.INSTANCE_ID) if address is None: @@ -2284,7 +2262,7 @@ def _create_new_disk_device(self, disk_type, address, name, ctrl_item): logger.warning("New disk address on parent not specified, " "guessing it should be %s", address) - ctrl_type = self.get_type_from_device(ctrl_item) + ctrl_type = ctrl_item.hardware_type # Make sure the address is valid! if ctrl_type == "scsi" and int(address) > 15: raise ValueTooHighError("disk address on SCSI controller", @@ -2294,26 +2272,26 @@ def _create_new_disk_device(self, disk_type, address, name, ctrl_item): address, 1) if name is None: - if disk_type == 'cdrom': + if drive_type == 'cdrom': name = "CD-ROM Drive" - elif disk_type == 'harddisk': + elif drive_type == 'harddisk': name = "Hard Disk Drive" else: # Should never get here! - raise ValueUnsupportedError("disk type", disk_type, + raise ValueUnsupportedError("disk drive type", drive_type, "'cdrom' or 'harddisk'") - (_, disk_item) = self.hardware.new_item(disk_type) + (_, disk_item) = self.hardware.new_item(drive_type) disk_item.set_property(self.ADDRESS_ON_PARENT, address) disk_item.set_property(self.PARENT, ctrl_instance) return disk_item, name - def add_disk_device(self, disk_type, address, name, description, + def add_disk_device(self, drive_type, address, name, description, disk, file_obj, ctrl_item, disk_item=None): """Create a new disk hardware device or overwrite an existing one. - :param str disk_type: ``'harddisk'`` or ``'cdrom'`` + :param str drive_type: ``'harddisk'`` or ``'cdrom'`` :param str address: Address on controller, such as "1:0" (optional) :param str name: Device name string (optional) :param str description: Description string (optional) @@ -2330,13 +2308,13 @@ def add_disk_device(self, disk_type, address, name, description, if disk_item is None: logger.info("Disk Item not found, adding new Item") disk_item, name = self._create_new_disk_device( - disk_type, address, name, ctrl_item) + drive_type, address, name, ctrl_item) else: logger.debug("Updating existing disk Item") # Make these changes to the disk Item regardless of new/existing - disk_item.set_property(self.RESOURCE_TYPE, self.RES_MAP[disk_type]) - if disk_type == 'harddisk': + disk_item.set_property(self.RESOURCE_TYPE, self.RES_MAP[drive_type]) + if drive_type == 'harddisk': # Link to the Disk we created disk_item.set_property(self.HOST_RESOURCE, (self.HOST_RSRC_DISK_REF + @@ -2620,18 +2598,18 @@ def find_disk_from_file_id(self, file_id): return self.find_child(self.disk_section, self.DISK, attrib={self.DISK_FILE_REF: file_id}) - def find_empty_drive(self, disk_type): + def find_empty_drive(self, drive_type): """Find a disk device that exists but contains no data. - :param str disk_type: Either 'cdrom' or 'harddisk' + :param str drive_type: Either 'cdrom' or 'harddisk' :return: Hardware device object, or None. """ - if disk_type == 'cdrom': + if drive_type == 'cdrom': # Find a drive that has no HostResource property return self.hardware.find_item( - resource_type=disk_type, + resource_type=drive_type, properties={self.HOST_RESOURCE: None}) - elif disk_type == 'harddisk': + elif drive_type == 'harddisk': # All harddisk items must have a HostResource, so we need a # different way to indicate an empty drive. By convention, # we do this with a small placeholder disk (one with a Disk entry @@ -2649,7 +2627,7 @@ def find_empty_drive(self, disk_type): return None else: raise ValueUnsupportedError("drive type", - disk_type, + drive_type, "'cdrom' or 'harddisk'") def find_device_location(self, device): @@ -2661,7 +2639,7 @@ def find_device_location(self, device): controller = self.find_parent_from_item(device) if controller is None: raise LookupError("No parent controller for device?") - return (self.get_type_from_device(controller), + return (controller.hardware_type, (controller.get_value(self.ADDRESS) + ':' + device.get_value(self.ADDRESS_ON_PARENT))) diff --git a/COT/tests/test_add_disk.py b/COT/tests/test_add_disk.py index a6c3d74..be2cb9d 100644 --- a/COT/tests/test_add_disk.py +++ b/COT/tests/test_add_disk.py @@ -376,7 +376,7 @@ def test_overwrite_harddisk_with_cdrom(self): """Replace a hard disk with a cd-rom.""" self.instance.package = self.v09_ovf self.instance.disk_image = self.input_iso - self.instance.disk_type = 'cdrom' + self.instance.drive_type = 'cdrom' self.instance.controller = 'scsi' self.instance.address = "0:0" self.instance.run() @@ -420,7 +420,7 @@ def test_overwrite_cdrom_with_harddisk(self): """Replace a cd-rom with a hard disk.""" self.instance.package = self.input_ovf self.instance.disk_image = self.blank_vmdk - self.instance.disk_type = 'harddisk' + self.instance.drive_type = 'harddisk' self.instance.controller = 'ide' self.instance.address = "1:0" self.instance.run() @@ -597,7 +597,7 @@ def test_add_cdrom_to_existing_controller(self): """Add a CDROM drive to an existing controller.""" self.instance.package = self.input_ovf self.instance.disk_image = self.blank_vmdk - self.instance.disk_type = "cdrom" + self.instance.drive_type = "cdrom" self.instance.controller = "scsi" self.instance.address = "0:1" self.instance.run() diff --git a/COT/tests/test_vm_description.py b/COT/tests/test_vm_description.py index bda77c2..eeafcd7 100644 --- a/COT/tests/test_vm_description.py +++ b/COT/tests/test_vm_description.py @@ -80,10 +80,6 @@ def test_abstract_disk_file_apis(self): ins.get_file_ref_from_disk, None) self.assertRaises(NotImplementedError, ins.get_id_from_disk, None) - self.assertRaises(NotImplementedError, - ins.get_type_from_device, None) - self.assertRaises(NotImplementedError, - ins.get_subtype_from_device, None) self.assertRaises(NotImplementedError, ins.get_common_subtype, None) self.assertRaises(NotImplementedError, diff --git a/COT/tests/ut.py b/COT/tests/ut.py index 427c197..a8773b5 100644 --- a/COT/tests/ut.py +++ b/COT/tests/ut.py @@ -163,17 +163,17 @@ class COT_UT(unittest.TestCase): # noqa: N801 # Standard WARNING logger messages we may expect at various points: TYPE_NOT_SPECIFIED_GUESS_HARDDISK = { 'levelname': 'WARNING', - 'msg': "disk type not specified.*guessing.*based on file extension", + 'msg': "drive type not specified.*guessing.*based on file extension", 'args': ('harddisk', ), } TYPE_NOT_SPECIFIED_GUESS_CDROM = { 'levelname': 'WARNING', - 'msg': "disk type not specified.*guessing.*based on file extension", + 'msg': "drive type not specified.*guessing.*based on file extension", 'args': ('cdrom', ), } CONTROLLER_NOT_SPECIFIED_GUESS_IDE = { 'levelname': 'WARNING', - 'msg': "Guessing controller type.*based on disk type", + 'msg': "Guessing controller type.*based on disk drive type", 'args': ('ide', r'.*', r'.*'), } UNRECOGNIZED_PRODUCT_CLASS = { diff --git a/COT/vm_description.py b/COT/vm_description.py index d6ff46d..2143c5e 100644 --- a/COT/vm_description.py +++ b/COT/vm_description.py @@ -330,22 +330,6 @@ def get_id_from_disk(self, disk): """ raise NotImplementedError("get_id_from_disk not implemented") - def get_type_from_device(self, device): - """Get the type of the given opaque device object. - - :param device: Device object to query - :return: string such as 'ide' or 'memory' - """ - raise NotImplementedError("get_type_from_device not implemented") - - def get_subtype_from_device(self, device): - """Get the sub-type of the given opaque device object. - - :param device: Device object to query - :return: ``None``, or string such as 'virtio' or 'lsilogic' - """ - raise NotImplementedError("get_subtype_from_device not implemented") - def get_common_subtype(self, device_type): """Get the sub-type common to all devices of the given type. @@ -393,13 +377,13 @@ def remove_file(self, file_obj, disk=None, disk_drive=None): """ raise NotImplementedError("remove_file not implemented") - def add_disk(self, disk_repr, file_id, disk_type, disk=None): + def add_disk(self, disk_repr, file_id, drive_type, disk=None): """Add a new disk object to the VM or overwrite the provided one. :param str disk_repr: Disk file representation :type disk_repr: COT.disks.DiskRepresentation or subclass :param str file_id: Identifier string for the file/disk mapping - :param str disk_type: 'harddisk' or 'cdrom' + :param str drive_type: 'harddisk' or 'cdrom' :param disk: Existing disk object to overwrite :return: New or updated disk object @@ -419,11 +403,11 @@ def add_controller_device(self, device_type, subtype, address, """ raise NotImplementedError("add_controller_device not implemented") - def add_disk_device(self, disk_type, address, name, description, + def add_disk_device(self, drive_type, address, name, description, disk, file_obj, ctrl_item, disk_item=None): """Add a new disk device to the VM or update the provided one. - :param str disk_type: ``'harddisk'`` or ``'cdrom'`` + :param str drive_type: ``'harddisk'`` or ``'cdrom'`` :param str address: Address on controller, such as "1:0" (optional) :param str name: Device name string (optional) :param str description: Description string (optional) @@ -717,10 +701,10 @@ def profile_info_string(self, width=79, verbosity_option=None): raise NotImplementedError("profile_info_string not implemented") # API methods needed for inject-config - def find_empty_drive(self, disk_type): + def find_empty_drive(self, drive_type): """Find a disk device that exists but contains no data. - :param str disk_type: Disk type, such as 'cdrom' or 'harddisk' + :param str drive_type: Disk drive type, such as 'cdrom' or 'harddisk' :return: Hardware device object, or None. """ raise NotImplementedError("find_empty_drive not implemented") From 2f285a963c488f235393e76f0238f2279e4803be Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 24 Oct 2016 12:25:33 -0400 Subject: [PATCH 30/59] Add glossary --- CHANGELOG.rst | 1 + docs/glossary.rst | 66 +++++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 3 files changed, 68 insertions(+) create mode 100644 docs/glossary.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8d1b099..45dbc7c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,7 @@ This project adheres to `Semantic Versioning`_. - ``cot inject-config --extra-files`` parameter (`#53`_). - Helper class for ``isoinfo`` (a companion to ``mkisofs``). +- Added glossary of terms to COT documentation. **Changed** diff --git a/docs/glossary.rst b/docs/glossary.rst new file mode 100644 index 0000000..197812b --- /dev/null +++ b/docs/glossary.rst @@ -0,0 +1,66 @@ +Glossary +======== + +.. default-role:: term + +.. glossary:: + + COT + Common `OVF` Tool + + controller + hardware controller + A virtual hardware controller for hardware such as a `disk device`. + In addition to its primary type (IDE, SCSI, etc.), a controller may also + have a subtype, such as ``virtio`` or ``lsilogic``. + In an `OVF` package, a controller is represented by an XML ``Item`` + element in the ``VirtualHardwareSection`` of the `OVF descriptor`. + Typically each `disk device` must be associated with a controller. + + disk description + disk element + disk reference + A description of a virtual disk included in a virtual machine. + In an `OVF descriptor`, this is an XML ``Disk`` element in the + ``DiskSection``. + This disk description may be associated with a `file reference` and/or + `disk file`, or it may be a placeholder for a blank disk not yet created. + Typically a disk description must be associated with a `disk drive` in + order to actually be accessible by the guest OS. + + disk device + disk drive + disk item + A `hardware item` describing a virtual CD-ROM, DVD-ROM, or hard disk drive. + In an `OVF` package, this is an XML ``Item`` element in the + ``VirtualHardwareSection`` of the `OVF descriptor`. + This item may reference a `disk reference` or a `file reference` to map + a filesystem to this drive. + Typically a disk drive must be associated to a `hardware controller`. + + disk file + disk image + A file such as a .vmdk, .iso, or .qcow2. May or may not be associated with + a `disk drive`. + + file element + file reference + A reference to a file, such as a `disk file` or any other file type, + to be included in a virtual machine. + In an `OVF descriptor`, this is an XML ``File`` element in the + ``References`` section. + + hardware element + hardware item + Generic term for any discrete piece of virtual machine hardware, including + but not limited to the CPU(s), memory, `disk drive`, `hardware controller`, + network port, etc. + + OVF + `Open Virtualization Format`_, an open standard. + + OVF descriptor + An XML file, based on the `OVF` specification, which describes a + virtual machine. + +.. _`Open Virtualization Format`: http://dmtf.org/standards/ovf diff --git a/docs/index.rst b/docs/index.rst index 287efa3..d30634c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,6 +8,7 @@ Common OVF Tool (COT) introduction installation usage + glossary changelog contributing thanks From 2a96d3c1397e52f0e46aa9364398a4a47c15fdcc Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 25 Oct 2016 15:39:01 -0400 Subject: [PATCH 31/59] Fix failures seen in Travis CI environment --- COT/disks/raw.py | 12 +++++++++++- COT/disks/tests/test_api.py | 7 +++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/COT/disks/raw.py b/COT/disks/raw.py index 1f2a627..158c4ae 100644 --- a/COT/disks/raw.py +++ b/COT/disks/raw.py @@ -17,7 +17,7 @@ import re from COT.disks.disk import DiskRepresentation -from COT.helpers import helpers +from COT.helpers import helpers, helper_select logger = logging.getLogger(__name__) @@ -99,6 +99,16 @@ def from_other_image(cls, input_image, output_dir, output_subformat=None): file_name = os.path.basename(input_image.path) file_prefix, _ = os.path.splitext(file_name) output_path = os.path.join(output_dir, file_prefix + ".img") + if (input_image.disk_format == 'vmdk' and + input_image.disk_subformat == 'streamOptimized'): + helper = helper_select([('qemu-img', '2.1.0'), 'vmdktool']) + # Special case: qemu-img < 2.1.0 can't handle streamOptimized VMDKs + if helper.name == 'vmdktool': + # Note that vmdktool takes its arguments in unusual order - + # output file comes before input file + helper.call(['-s', output_path, input_image.path]) + return cls(output_path) + helpers['qemu-img'].call(['convert', '-O', 'raw', input_image.path, diff --git a/COT/disks/tests/test_api.py b/COT/disks/tests/test_api.py index b3717c7..d93474a 100644 --- a/COT/disks/tests/test_api.py +++ b/COT/disks/tests/test_api.py @@ -59,10 +59,13 @@ def test_disk_representation_from_file_vmdk(self): self.assertEqual(dr.disk_subformat, "streamOptimized") def test_disk_representation_from_file_iso(self): - """Test if disk_representation_from_file() works for vmdk images.""" + """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") - self.assertEqual(dr.disk_subformat, "") + # 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.""" From 0e47d78f1132b8baa267200427ee269d4f4ff6b1 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 25 Oct 2016 16:02:08 -0400 Subject: [PATCH 32/59] Fix pylint redefined-variable-type under Python 3 --- COT/disks/raw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/COT/disks/raw.py b/COT/disks/raw.py index 158c4ae..2ffc192 100644 --- a/COT/disks/raw.py +++ b/COT/disks/raw.py @@ -67,7 +67,7 @@ def _create_file(self): capacity_val += os.path.getsize(content_file) # Round capacity to the next larger multiple of 8 MB # just to be safe... - capacity_val = 8 * ((capacity_val / 1024 / 1024 / 8) + 1) + capacity_val = int(8 * ((capacity_val / 1024 / 1024 / 8) + 1)) capacity_str = "{0}M".format(capacity_val) self._capacity = capacity_str logger.verbose( From c16f13d31040bb2fe57d63e0c5e7f083a99dcf5b Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 25 Oct 2016 17:02:53 -0400 Subject: [PATCH 33/59] Improve COT.disks code coverage --- COT/disks/iso.py | 5 ++-- COT/disks/tests/test_iso.py | 43 +++++++++++++++++++++++++++++------ COT/disks/tests/test_qcow2.py | 13 ++++++++++- COT/disks/tests/test_vmdk.py | 2 +- 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/COT/disks/iso.py b/COT/disks/iso.py index 3f344b5..19986e5 100644 --- a/COT/disks/iso.py +++ b/COT/disks/iso.py @@ -47,9 +47,8 @@ def files(self): """The list of files contained in this ISO.""" if self._files is None: if helpers['isoinfo']: # TODO - args = ["-i", self.path, "-f"] - if self.disk_subformat == "rockridge": - args.append("-R") + # It's safe to specify -R even for non-rockridge ISOs + args = ["-i", self.path, "-f", "-R"] # At this time we don't support Joliet extensions output = helpers['isoinfo'].call(args) result = [] diff --git a/COT/disks/tests/test_iso.py b/COT/disks/tests/test_iso.py index 20d35c3..912b483 100644 --- a/COT/disks/tests/test_iso.py +++ b/COT/disks/tests/test_iso.py @@ -48,12 +48,24 @@ def test_create_with_files(self): iso = ISO(path=os.path.join(self.temp_dir, "out.iso"), files=[self.input_ovf]) if helpers['isoinfo']: - self.assertEqual(iso.files, - [os.path.basename(self.input_ovf)]) # Our default create format is rockridge self.assertEqual(iso.disk_subformat, "rockridge") + self.assertEqual(iso.files, + [os.path.basename(self.input_ovf)]) else: - logger.info("isoinfo not available, not checking disk contents") + helpers['isoinfo']._installed = True + + with mock.patch.object(helpers['isoinfo'], "call", + return_value="Rock Ridge extensions found"): + self.assertEqual(iso.disk_subformat, "rockridge") + + with mock.patch.object(helpers['isoinfo'], "call", + return_value=""" +Setting input-charset to 'UTF-8' from locale. +/{0} +""".format(os.path.basename(self.input_ovf))): + self.assertEqual(iso.files, + [os.path.basename(self.input_ovf)]) def test_create_with_files_non_rockridge(self): """Creation of a non-rock-ridge ISO with specific file contents.""" @@ -61,12 +73,22 @@ def test_create_with_files_non_rockridge(self): files=[self.input_ovf], disk_subformat="") if helpers['isoinfo']: + self.assertEqual(iso.disk_subformat, "") self.assertEqual(iso.files, [os.path.basename(self.input_ovf)]) - # Our default create format is rockridge - self.assertEqual(iso.disk_subformat, "") else: - logger.info("isoinfo not available, not checking disk contents") + helpers['isoinfo']._installed = True + with mock.patch.object(helpers['isoinfo'], "call", + return_value="No SUSP/Rock Ridge present"): + self.assertEqual(iso.disk_subformat, "") + + with mock.patch.object(helpers['isoinfo'], "call", + return_value=""" +Setting input-charset to 'UTF-8' from locale. +/{0};1 +""".format(os.path.basename(self.input_ovf).upper())): + self.assertEqual(iso.files, + [os.path.basename(self.input_ovf)]) def test_create_without_files(self): """Can't create an empty ISO.""" @@ -137,7 +159,14 @@ def test_file_is_this_type_isoinfo(self): if helpers['isoinfo']: self.assertTrue(ISO.file_is_this_type(self.input_iso)) self.assertFalse(ISO.file_is_this_type(self.blank_vmdk)) - # TODO - check call at least. + else: + # Fake it til you make it + helpers['isoinfo']._installed = True + with mock.patch.object(helpers['isoinfo'], "call"): + self.assertTrue(ISO.file_is_this_type(self.input_iso)) + with mock.patch.object(helpers['isoinfo'], "call", + side_effect=HelperError): + self.assertFalse(ISO.file_is_this_type(self.blank_vmdk)) def test_file_is_this_type_noisoinfo(self): """The file_is_this_type API should work if isoinfo isn't available.""" diff --git a/COT/disks/tests/test_qcow2.py b/COT/disks/tests/test_qcow2.py index c96bb87..12700ef 100644 --- a/COT/disks/tests/test_qcow2.py +++ b/COT/disks/tests/test_qcow2.py @@ -20,7 +20,8 @@ import os from COT.tests.ut import COT_UT -from COT.disks import QCOW2 +from COT.disks import QCOW2, disk_representation_from_file +from COT.helpers import helpers logger = logging.getLogger(__name__) @@ -34,3 +35,13 @@ def test_init_with_files_unsupported(self): QCOW2, path=os.path.join(self.temp_dir, "out.qcow2"), files=[self.input_ovf]) + + def test_from_other_image(self): + """Test conversion of various formats to qcow2.""" + temp_disk = os.path.join(self.temp_dir, "foo.raw") + helpers['qemu-img'].call(['create', '-f', 'raw', temp_disk, "16M"]) + old = disk_representation_from_file(temp_disk) + qcow2 = QCOW2.from_other_image(old, self.temp_dir) + + self.assertEqual(qcow2.disk_format, 'qcow2') + self.assertEqual(qcow2.disk_subformat, None) diff --git a/COT/disks/tests/test_vmdk.py b/COT/disks/tests/test_vmdk.py index f88c6b0..2b21cb0 100644 --- a/COT/disks/tests/test_vmdk.py +++ b/COT/disks/tests/test_vmdk.py @@ -33,7 +33,7 @@ class TestVMDK(COT_UT): """Test cases for VMDK class.""" def other_format_to_vmdk_stream_optimized_test(self, disk_format): - """Test conversion of raw to vmdk streamOptimized.""" + """Test conversion of various formats to vmdk streamOptimized.""" temp_disk = os.path.join(self.temp_dir, "foo.{0}".format(disk_format)) helpers['qemu-img'].call(['create', '-f', disk_format, temp_disk, "16M"]) From 60e940e967751971e87f80362172f8b83cbbe1eb Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 26 Oct 2016 09:18:22 -0400 Subject: [PATCH 34/59] More COT.disks test coverage --- COT/disks/iso.py | 1 + COT/disks/tests/test_api.py | 12 +++++++ COT/disks/tests/test_disk_representation.py | 4 +++ COT/disks/tests/test_iso.py | 11 +++++++ COT/disks/tests/test_vmdk.py | 35 +++++++++++++++------ 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/COT/disks/iso.py b/COT/disks/iso.py index 19986e5..d914ad0 100644 --- a/COT/disks/iso.py +++ b/COT/disks/iso.py @@ -87,6 +87,7 @@ def _create_file(self): args = ['-as', 'mkisofs'] + args helper.call(args) + self._disk_subformat = None self._files = None @classmethod diff --git a/COT/disks/tests/test_api.py b/COT/disks/tests/test_api.py index d93474a..e546bbe 100644 --- a/COT/disks/tests/test_api.py +++ b/COT/disks/tests/test_api.py @@ -80,3 +80,15 @@ def test_disk_representation_from_file_errors(self): 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 16bf846..9e3cbf5 100644 --- a/COT/disks/tests/test_disk_representation.py +++ b/COT/disks/tests/test_disk_representation.py @@ -53,6 +53,10 @@ def test_file_is_this_type_missing_file(self): self.assertRaises(HelperError, DiskRepresentation.file_is_this_type, "/foo/bar") + def test_create_file_path_mandatory(self): + """Can't create a file without specifying a path.""" + self.assertRaises(ValueError, DiskRepresentation, path=None) + def test_create_file_already_extant(self): """Can't call create_file if the file already exists.""" self.assertRaises(RuntimeError, diff --git a/COT/disks/tests/test_iso.py b/COT/disks/tests/test_iso.py index 912b483..5610b63 100644 --- a/COT/disks/tests/test_iso.py +++ b/COT/disks/tests/test_iso.py @@ -43,6 +43,17 @@ def tearDown(self): helper._version = None super(TestISO, self).tearDown() + def test_representation(self): + """Representing an existing ISO.""" + iso = ISO(self.input_iso) + self.assertEqual(iso.path, self.input_iso) + self.assertEqual(iso.disk_format, 'iso') + self.assertEqual(iso.capacity, str(self.FILE_SIZE['input.iso'])) + if helpers['isoinfo']: + self.assertEqual(iso.disk_subformat, "") + self.assertEqual(iso.files, + ['iosxr_config.txt', 'iosxr_config_admin.txt']) + 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"), diff --git a/COT/disks/tests/test_vmdk.py b/COT/disks/tests/test_vmdk.py index 2b21cb0..8e0114e 100644 --- a/COT/disks/tests/test_vmdk.py +++ b/COT/disks/tests/test_vmdk.py @@ -32,34 +32,41 @@ class TestVMDK(COT_UT): """Test cases for VMDK class.""" - def other_format_to_vmdk_stream_optimized_test(self, disk_format): - """Test conversion of various formats to vmdk streamOptimized.""" + def other_format_to_vmdk_test(self, disk_format, + output_subformat="streamOptimized"): + """Test conversion of various formats to vmdk.""" 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) - vmdk = VMDK.from_other_image(old, self.temp_dir, 'streamOptimized') + vmdk = VMDK.from_other_image(old, self.temp_dir, output_subformat) self.assertEqual(vmdk.disk_format, 'vmdk') - self.assertEqual(vmdk.disk_subformat, 'streamOptimized') + self.assertEqual(vmdk.disk_subformat, output_subformat) @mock.patch('COT.helpers.qemu_img.QEMUImg.version', new_callable=mock.PropertyMock, return_value=StrictVersion("1.0.0")) def test_disk_conversion_old_qemu(self, _): """Test disk conversion flows with older qemu-img version.""" - self.other_format_to_vmdk_stream_optimized_test('raw') - self.other_format_to_vmdk_stream_optimized_test('qcow2') - self.other_format_to_vmdk_stream_optimized_test('vmdk') + self.other_format_to_vmdk_test('raw') + self.other_format_to_vmdk_test('qcow2') + self.other_format_to_vmdk_test('vmdk') @mock.patch('COT.helpers.qemu_img.QEMUImg.version', new_callable=mock.PropertyMock, return_value=StrictVersion("2.1.0")) def test_disk_conversion_new_qemu(self, _): """Test disk conversion flows with newer qemu-img version.""" - self.other_format_to_vmdk_stream_optimized_test('raw') - self.other_format_to_vmdk_stream_optimized_test('qcow2') - self.other_format_to_vmdk_stream_optimized_test('vmdk') + self.other_format_to_vmdk_test('raw') + self.other_format_to_vmdk_test('qcow2') + self.other_format_to_vmdk_test('vmdk') + + def test_disk_conversion_unsupported_subformat(self): + """Not all VMDK subformats are currently implemented.""" + self.assertRaises(NotImplementedError, + self.other_format_to_vmdk_test, + 'qcow2', output_subformat="twoGbMaxExtentFlat") def test_capacity(self): """Check capacity of several VMDK files.""" @@ -76,6 +83,7 @@ def test_create_default(self): self.assertEqual(vmdk.path, os.path.join(self.temp_dir, "foo.vmdk")) self.assertEqual(vmdk.disk_format, "vmdk") self.assertEqual(vmdk.disk_subformat, "monolithicSparse") + self.assertEqual(vmdk.disk_subformat, "monolithicSparse") def test_create_stream_optimized(self): """Explicit subformat specification.""" @@ -85,3 +93,10 @@ def test_create_stream_optimized(self): self.assertEqual(vmdk.path, os.path.join(self.temp_dir, "foo.vmdk")) self.assertEqual(vmdk.disk_format, "vmdk") self.assertEqual(vmdk.disk_subformat, "streamOptimized") + self.assertEqual(vmdk.disk_subformat, "streamOptimized") + + 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"), + files=[self.input_iso]) From d127f7924f6d443c7dfa89f94b55e7b185325ffa Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 26 Oct 2016 09:44:10 -0400 Subject: [PATCH 35/59] Add negative test for non-FAT disk --- COT/disks/tests/test_raw.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/COT/disks/tests/test_raw.py b/COT/disks/tests/test_raw.py index a7a9e01..36380de 100644 --- a/COT/disks/tests/test_raw.py +++ b/COT/disks/tests/test_raw.py @@ -21,6 +21,7 @@ from COT.tests.ut import COT_UT from COT.disks import RAW, disk_representation_from_file +from COT.helpers import HelperError logger = logging.getLogger(__name__) @@ -28,6 +29,12 @@ class TestRAW(COT_UT): """Test cases for RAW disk image representation.""" + def test_representation_invalid(self): + """Representation of a file that isn't really a raw disk.""" + fake_raw = RAW(self.input_iso) + with self.assertRaises(HelperError): + assert fake_raw.files + def test_convert_from_vmdk(self): """Test conversion of a RAW image from a VMDK.""" old = disk_representation_from_file(self.blank_vmdk) From d427f615b4c4286b8dfeb66c269b1dec4bd25f73 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 26 Oct 2016 09:59:15 -0400 Subject: [PATCH 36/59] Add test_inject_extra_directory to test directory descent --- COT/tests/test_inject_config.py | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/COT/tests/test_inject_config.py b/COT/tests/test_inject_config.py index a34fd7f..c50f374 100644 --- a/COT/tests/test_inject_config.py +++ b/COT/tests/test_inject_config.py @@ -19,6 +19,8 @@ import logging import os.path import re +import shutil +import tempfile from pkg_resources import resource_filename from COT.tests.ut import COT_UT @@ -290,6 +292,38 @@ def test_find_parent_fail_no_parent(self): self.assertLogged(levelname="ERROR", msg="Item has no .*Parent element") + def test_inject_extra_directory(self): + """Test injection of extras from an entire directory.""" + self.instance.package = self.input_ovf + extra_dir = tempfile.mkdtemp(prefix="cot_ic_ut") + try: + shutil.copy(self.input_ovf, extra_dir) + shutil.copy(self.minimal_ovf, extra_dir) + subdir = os.path.join(extra_dir, "subdirectory") + os.makedirs(subdir) + shutil.copy(self.invalid_ovf, subdir) + + self.instance.extra_files = [extra_dir] + self.instance.run() + self.assertLogged(**self.OVERWRITING_DISK_ITEM) + self.instance.finished() + + config_iso = os.path.join(self.temp_dir, 'config.iso') + if helpers['isoinfo']: + self.assertEqual( + disk_representation_from_file(config_iso).files, + [ + 'input.ovf', + 'minimal.ovf', + 'subdirectory', + 'subdirectory/invalid.ovf', + ] + ) + else: + logger.info("isoinfo not present, not checking disk contents") + finally: + shutil.rmtree(extra_dir) + def test_inject_config_primary_secondary_extra(self): """Test injection of primary and secondary files and extras.""" self.instance.package = self.input_ovf From 1f2555b12a0a7ea9e347ddaa4df9887ac3381652 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 27 Oct 2016 08:49:00 -0400 Subject: [PATCH 37/59] OVF module length reduced slightly --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 1d4a714..28626b5 100644 --- a/.pylintrc +++ b/.pylintrc @@ -65,7 +65,7 @@ max-public-methods=73 # default: max-module-lines: 1000 # current worst offender: OVF -max-module-lines=2750 +max-module-lines=2700 [LOGGING] From b41a362faa571f6bd48b45b0863fca3bb813279a Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 27 Oct 2016 08:50:05 -0400 Subject: [PATCH 38/59] Improve code coverage --- COT/ovf/tests/test_ovf.py | 6 ++++++ COT/tests/test_inject_config.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/COT/ovf/tests/test_ovf.py b/COT/ovf/tests/test_ovf.py index c2b2cb3..a8df7e2 100644 --- a/COT/ovf/tests/test_ovf.py +++ b/COT/ovf/tests/test_ovf.py @@ -494,3 +494,9 @@ def test_configuration_profiles(self): "1CPU-1GB-1NIC", "2CPU-2GB-1NIC"]) self.assertEqual(ovf.default_config_profile, "4CPU-4GB-3NIC") + + def test_find_empty_drive_unsupported(self): + """Negative test for find_empty_drive().""" + with VMContextManager(self.input_ovf, None) as ovf: + self.assertRaises(ValueUnsupportedError, + ovf.find_empty_drive, 'floppy') diff --git a/COT/tests/test_inject_config.py b/COT/tests/test_inject_config.py index c50f374..c9ed9b0 100644 --- a/COT/tests/test_inject_config.py +++ b/COT/tests/test_inject_config.py @@ -21,12 +21,14 @@ import re import shutil import tempfile + +import mock from pkg_resources import resource_filename from COT.tests.ut import COT_UT from COT.ui_shared import UI from COT.inject_config import COTInjectConfig -from COT.data_validation import InvalidInputError +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 @@ -226,6 +228,31 @@ def test_inject_config_vmdk(self): # self.assertEqual(disk_representation_from_file(config_vmdk).files, # ["ios_config.txt"]) + def test_inject_config_unsupported_format_existing(self): + """Only 'harddisk' and 'cdrom' config drives are supported.""" + self.instance.package = self.input_ovf + self.instance.config_file = self.config_file + # Failure during initial lookup of existing drive + # pylint: disable=protected-access + with mock.patch.object(self.instance.vm._platform, + 'BOOTSTRAP_DISK_TYPE', + new_callable=mock.PropertyMock, + return_value='floppy'): + self.assertRaises(ValueUnsupportedError, self.instance.run) + + def test_inject_config_unsupported_format_new_disk(self): + """Only 'harddisk' and 'cdrom' config drives are supported.""" + self.instance.package = self.input_ovf + self.instance.config_file = self.config_file + # Drive lookup passes, but failure to create new disk + # pylint: disable=protected-access + with mock.patch.object(self.instance.vm._platform, + 'BOOTSTRAP_DISK_TYPE', + new_callable=mock.PropertyMock, + side_effect=('cdrom', 'cdrom', + 'floppy', 'floppy', 'floppy')): + self.assertRaises(ValueUnsupportedError, self.instance.run) + def test_inject_config_repeatedly(self): """inject-config repeatedly.""" # Add initial config file From 33557e754a200a20fd78aff01f6768c7fa039d65 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 27 Oct 2016 12:40:30 -0400 Subject: [PATCH 39/59] Change VMDK default subformat to streamOptimized and streamline conversion logic --- COT/disks/tests/test_vmdk.py | 22 ++++++++++++++------ COT/disks/vmdk.py | 40 +++++++++++++++++++----------------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/COT/disks/tests/test_vmdk.py b/COT/disks/tests/test_vmdk.py index 8e0114e..18db62c 100644 --- a/COT/disks/tests/test_vmdk.py +++ b/COT/disks/tests/test_vmdk.py @@ -24,7 +24,7 @@ from COT.tests.ut import COT_UT from COT.disks import VMDK, disk_representation_from_file -from COT.helpers import helpers +from COT.helpers import helpers, HelperError logger = logging.getLogger(__name__) @@ -63,10 +63,10 @@ def test_disk_conversion_new_qemu(self, _): self.other_format_to_vmdk_test('vmdk') def test_disk_conversion_unsupported_subformat(self): - """Not all VMDK subformats are currently implemented.""" - self.assertRaises(NotImplementedError, + """qemu-img will fail if subformat is invalid.""" + self.assertRaises(HelperError, self.other_format_to_vmdk_test, - 'qcow2', output_subformat="twoGbMaxExtentFlat") + 'qcow2', output_subformat="foobar") def test_capacity(self): """Check capacity of several VMDK files.""" @@ -82,8 +82,8 @@ def test_create_default(self): capacity="16M") self.assertEqual(vmdk.path, os.path.join(self.temp_dir, "foo.vmdk")) self.assertEqual(vmdk.disk_format, "vmdk") - self.assertEqual(vmdk.disk_subformat, "monolithicSparse") - self.assertEqual(vmdk.disk_subformat, "monolithicSparse") + self.assertEqual(vmdk.disk_subformat, "streamOptimized") + self.assertEqual(vmdk.disk_subformat, "streamOptimized") def test_create_stream_optimized(self): """Explicit subformat specification.""" @@ -95,6 +95,16 @@ def test_create_stream_optimized(self): self.assertEqual(vmdk.disk_subformat, "streamOptimized") self.assertEqual(vmdk.disk_subformat, "streamOptimized") + 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") + self.assertEqual(vmdk.path, os.path.join(self.temp_dir, "foo.vmdk")) + self.assertEqual(vmdk.disk_format, "vmdk") + self.assertEqual(vmdk.disk_subformat, "monolithicSparse") + self.assertEqual(vmdk.disk_subformat, "monolithicSparse") + def test_create_files_unsupported(self): """No support for creating a VMDK with a filesystem.""" self.assertRaises(NotImplementedError, diff --git a/COT/disks/vmdk.py b/COT/disks/vmdk.py index ad96e44..0f0d2b0 100644 --- a/COT/disks/vmdk.py +++ b/COT/disks/vmdk.py @@ -48,46 +48,48 @@ def disk_subformat(self): return self._disk_subformat @classmethod - def from_other_image(cls, input_image, output_dir, output_subformat=None): + def from_other_image(cls, input_image, output_dir, + output_subformat="streamOptimized"): """Convert the other disk image into an image of this type. :param DiskRepresentation input_image: Existing image representation. :param str output_dir: Output directory to store the new image in. - :param str output_subformat: Any relevant subformat information. - :rtype: instance of DiskRepresentation or subclass + :param str output_subformat: VMDK subformat string. + Defaults to "streamOptimized" if unset. + :rtype: :class:`~COT.disks.vmdk.VMDK` """ file_name = os.path.basename(input_image.path) (file_prefix, _) = os.path.splitext(file_name) output_path = os.path.join(output_dir, file_prefix + ".vmdk") if output_subformat == "streamOptimized": + # Special case - qemu-img prior to 2.1.0 can't do streamOptimized helper = helper_select([('qemu-img', '2.1.0'), 'vmdktool']) - if helper.name == 'qemu-img': - helper.call(['convert', - '-O', 'vmdk', - '-o', 'subformat=streamOptimized', - input_image.path, - output_path]) - elif helper.name == 'vmdktool': + if helper.name == 'vmdktool': if input_image.disk_format != 'raw': # vmdktool needs a raw image as input from COT.disks import RAW try: temp_image = RAW.from_other_image(input_image, output_dir) - output_image = cls.from_other_image(temp_image, - output_dir, - output_subformat) + return cls.from_other_image(temp_image, + output_dir, + output_subformat) finally: os.remove(temp_image.path) - return output_image # Note that vmdktool takes its arguments in unusual order - # output file comes before input file helper.call(['-z9', '-v', output_path, input_image.path]) - else: - # TODO: support at least monolithicSparse! - raise NotImplementedError("No support for subformat '%s'", - output_subformat) + return cls(output_path) + + # else, fall through to default: + + helpers['qemu-img'].call([ + 'convert', + '-O', 'vmdk', + '-o', 'subformat={0}'.format(output_subformat), + input_image.path, + output_path]) return cls(output_path) def _create_file(self): @@ -96,7 +98,7 @@ def _create_file(self): 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 = "monolithicSparse" + self._disk_subformat = "streamOptimized" helpers['qemu-img'].call(['create', '-f', self.disk_format, '-o', 'subformat=' + self._disk_subformat, From 1cb287a01d0f23321400124a3416e80e9ada5839 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 27 Oct 2016 12:39:54 -0400 Subject: [PATCH 40/59] Fix qemu-img version check (1.2 for conversion *from* streamOptimized, 2.1 for conversion *to* streamOptimized) and add tests. --- COT/disks/qcow2.py | 19 ++++++++++- COT/disks/raw.py | 4 +-- COT/disks/tests/test_qcow2.py | 62 +++++++++++++++++++++++++++++++---- COT/disks/tests/test_raw.py | 34 ++++++++++++++++++- 4 files changed, 108 insertions(+), 11 deletions(-) diff --git a/COT/disks/qcow2.py b/COT/disks/qcow2.py index e12c38b..78d0110 100644 --- a/COT/disks/qcow2.py +++ b/COT/disks/qcow2.py @@ -15,7 +15,7 @@ import os from COT.disks.disk import DiskRepresentation -from COT.helpers import helpers +from COT.helpers import helpers, helper_select class QCOW2(DiskRepresentation): @@ -35,6 +35,23 @@ def from_other_image(cls, input_image, output_dir, output_subformat=None): file_name = os.path.basename(input_image.path) (file_prefix, _) = os.path.splitext(file_name) output_path = os.path.join(output_dir, file_prefix + ".qcow2") + if (input_image.disk_format == 'vmdk' and + input_image.disk_subformat == 'streamOptimized'): + helper = helper_select([('qemu-img', '1.2.0'), 'vmdktool']) + # Special case: qemu-img < 1.2.0 can't read streamOptimized VMDKs + if helper.name == 'vmdktool': + # vmdktool can convert streamOptimized VMDK to raw + # Convert vmdk to raw, then raw to qcow2 + # Note that vmdktool takes its arguments in unusual order - + # output file comes before input file + from COT.disks import RAW + try: + temp_image = RAW.from_other_image(input_image, output_dir) + return cls.from_other_image(temp_image, output_dir, + output_subformat) + finally: + os.remove(temp_image.path) + helpers['qemu-img'].call(['convert', '-O', 'qcow2', input_image.path, diff --git a/COT/disks/raw.py b/COT/disks/raw.py index 2ffc192..e76e74b 100644 --- a/COT/disks/raw.py +++ b/COT/disks/raw.py @@ -101,8 +101,8 @@ def from_other_image(cls, input_image, output_dir, output_subformat=None): output_path = os.path.join(output_dir, file_prefix + ".img") if (input_image.disk_format == 'vmdk' and input_image.disk_subformat == 'streamOptimized'): - helper = helper_select([('qemu-img', '2.1.0'), 'vmdktool']) - # Special case: qemu-img < 2.1.0 can't handle streamOptimized VMDKs + helper = helper_select([('qemu-img', '1.2.0'), 'vmdktool']) + # Special case: qemu-img < 1.2.0 can't read streamOptimized VMDKs if helper.name == 'vmdktool': # Note that vmdktool takes its arguments in unusual order - # output file comes before input file diff --git a/COT/disks/tests/test_qcow2.py b/COT/disks/tests/test_qcow2.py index 12700ef..ea8c659 100644 --- a/COT/disks/tests/test_qcow2.py +++ b/COT/disks/tests/test_qcow2.py @@ -19,8 +19,11 @@ import logging import os +from distutils.version import StrictVersion +import mock + from COT.tests.ut import COT_UT -from COT.disks import QCOW2, disk_representation_from_file +from COT.disks import QCOW2, VMDK, RAW from COT.helpers import helpers logger = logging.getLogger(__name__) @@ -29,6 +32,12 @@ class TestQCOW2(COT_UT): """Test cases for QCOW2 class.""" + def setUp(self): + """Pre-testcase setup.""" + super(TestQCOW2, self).setUp() + self.temp_disk = os.path.join(self.temp_dir, "blank.img") + helpers['qemu-img'].call(['create', '-f', 'raw', self.temp_disk, "8M"]) + def test_init_with_files_unsupported(self): """Creation of a QCOW2 with specific file contents is not supported.""" self.assertRaises(NotImplementedError, @@ -36,12 +45,51 @@ def test_init_with_files_unsupported(self): path=os.path.join(self.temp_dir, "out.qcow2"), files=[self.input_ovf]) - def test_from_other_image(self): - """Test conversion of various formats to qcow2.""" - temp_disk = os.path.join(self.temp_dir, "foo.raw") - helpers['qemu-img'].call(['create', '-f', 'raw', temp_disk, "16M"]) - old = disk_representation_from_file(temp_disk) - qcow2 = QCOW2.from_other_image(old, self.temp_dir) + def test_from_other_image_raw(self): + """Test conversion of raw format to qcow2.""" + qcow2 = QCOW2.from_other_image(RAW(self.temp_disk), self.temp_dir) + + self.assertEqual(qcow2.disk_format, 'qcow2') + self.assertEqual(qcow2.disk_subformat, None) + + def test_from_other_image_vmdk(self): + """Test conversion of streamOptimized vmdk format to qcow2.""" + qcow2 = QCOW2.from_other_image(VMDK(self.blank_vmdk), self.temp_dir) self.assertEqual(qcow2.disk_format, 'qcow2') self.assertEqual(qcow2.disk_subformat, None) + + @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('COT.disks.raw.RAW.from_other_image') + @mock.patch('COT.helpers.qemu_img.QEMUImg.call') + def test_convert_from_vmdk_old_qemu(self, + mock_qemuimg, + mock_raw, + *_): + """Test conversion from streamOptimized VMDK with old QEMU.""" + mock_raw.return_value = RAW(self.temp_disk) + + QCOW2.from_other_image(VMDK(self.blank_vmdk), self.temp_dir) + + mock_qemuimg.assert_called_with([ + 'convert', '-O', 'qcow2', self.temp_disk, + os.path.join(self.temp_dir, 'blank.qcow2') + ]) + + @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('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, *_): + """Test conversion from streamOptimized VMDK with new QEMU.""" + QCOW2.from_other_image(VMDK(self.blank_vmdk), self.temp_dir) + + mock_vmdktool.assert_not_called() + mock_qemuimg.assert_called_with([ + 'convert', '-O', 'qcow2', self.blank_vmdk, + os.path.join(self.temp_dir, "blank.qcow2")]) diff --git a/COT/disks/tests/test_raw.py b/COT/disks/tests/test_raw.py index 36380de..2904889 100644 --- a/COT/disks/tests/test_raw.py +++ b/COT/disks/tests/test_raw.py @@ -19,8 +19,11 @@ import logging import os +from distutils.version import StrictVersion +import mock + from COT.tests.ut import COT_UT -from COT.disks import RAW, disk_representation_from_file +from COT.disks import RAW, VMDK, disk_representation_from_file from COT.helpers import HelperError logger = logging.getLogger(__name__) @@ -43,6 +46,35 @@ def test_convert_from_vmdk(self): self.assertEqual(raw.disk_format, 'raw') self.assertEqual(raw.disk_subformat, None) + @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('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, *_): + """Test conversion from streamOptimized VMDK with old QEMU.""" + RAW.from_other_image(VMDK(self.blank_vmdk), self.temp_dir) + + mock_vmdktool.assert_called_with([ + '-s', os.path.join(self.temp_dir, "blank.img"), self.blank_vmdk]) + mock_qemuimg.assert_not_called() + + @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('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, *_): + """Test conversion from streamOptimized VMDK with new QEMU.""" + RAW.from_other_image(VMDK(self.blank_vmdk), self.temp_dir) + + mock_vmdktool.assert_not_called() + mock_qemuimg.assert_called_with([ + 'convert', '-O', 'raw', self.blank_vmdk, + os.path.join(self.temp_dir, "blank.img")]) + 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"), From 8b3b8751ef2c79128cf5f4af9d0fe0a4663bd276 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 15 Jul 2016 16:40:44 -0400 Subject: [PATCH 41/59] Change all docstrings in COT.ovf to Google style for readability --- .pylintrc | 4 +- COT/ovf/hardware.py | 182 +++++---- COT/ovf/item.py | 214 ++++++----- COT/ovf/name_helper.py | 54 ++- COT/ovf/ovf.py | 815 +++++++++++++++++++++++++---------------- docs/conf.py | 5 + 6 files changed, 783 insertions(+), 491 deletions(-) diff --git a/.pylintrc b/.pylintrc index d1821ff..aa69e81 100644 --- a/.pylintrc +++ b/.pylintrc @@ -75,8 +75,8 @@ accept-no-return-type-doc=yes [FORMAT] # default: max-module-lines: 1000 -# current worst offender: OVF -max-module-lines=2900 +# current worst offender: COT/ovf/ovf.py +max-module-lines=3000 [LOGGING] diff --git a/COT/ovf/hardware.py b/COT/ovf/hardware.py index ec06098..b39b7b7 100644 --- a/COT/ovf/hardware.py +++ b/COT/ovf/hardware.py @@ -53,9 +53,11 @@ class OVFHardware(object): def __init__(self, ovf): """Construct an OVFHardware object describing all Items in the OVF. - :param ovf: OVF instance to extract hardware information from. - :type ovf: :class:`~COT.ovf.ovf.OVF` - :raises OVFHardwareDataError: if any data errors are seen + Args: + ovf (OVF): OVF instance to extract hardware information from. + + Raises: + OVFHardwareDataError: if any data errors are seen """ self.ovf = ovf self.item_dict = {} @@ -143,8 +145,8 @@ def update_xml(self): def find_unused_instance_id(self): """Find the first available ``InstanceID`` number. - :return: Instance ID not yet in use. - :rtype: string + Returns: + str: An instance ID that is not yet in use. """ i = 1 while str(i) in self.item_dict.keys(): @@ -153,11 +155,15 @@ def find_unused_instance_id(self): return str(i) def new_item(self, resource_type, profile_list=None): - """Create a new :class:`OVFItem` of the given type. + """Create a new :class:`~COT.ovf.item.OVFItem` of the given type. + + Args: + resource_type (str): String such as 'cpu' or 'harddisk' - used as + a key to :data:`~COT.ovf.name_helper.OVFNameHelper1.RES_MAP` + profile_list (list): Profiles the new item should belong to - :param str resource_type: - :param list profile_list: Profiles the new item should belong to - :return: ``(instance, ovfitem)`` + Returns: + tuple: ``(instance_id, ovfitem)`` """ instance = self.find_unused_instance_id() ovfitem = OVFItem(self.ovf) @@ -178,8 +184,8 @@ def new_item(self, resource_type, profile_list=None): def delete_item(self, item): """Delete the given Item from the hardware. - :param item: Item to delete - :type item: :class:`~COT.ovf.item.OVFItem` + Args: + item (OVFItem): Item to delete """ instance = item.get_value(self.ovf.INSTANCE_ID) if self.item_dict[instance] == item: @@ -189,9 +195,12 @@ def delete_item(self, item): def clone_item(self, parent_item, profile_list): """Clone an :class:`OVFItem` to create a new instance. - :param OVFItem parent_item: Instance to clone from - :param list profile_list: List of profiles to clone into - :return: ``(instance, ovfitem)`` + Args: + parent_item (OVFItem): Instance to clone from + profile_list (list): List of profiles to clone into + + Returns: + tuple: ``(instance_id, ovfitem)`` """ instance = self.find_unused_instance_id() ovfitem = copy.deepcopy(parent_item) @@ -205,13 +214,14 @@ def clone_item(self, parent_item, profile_list): def item_match(self, item, resource_type, properties, profile_list): """Check whether the given item matches the given filters. - :param item: Item to validate - :type item: :class:`~COT.ovf.item.OVFItem` - :param str resource_type: Resource type string like 'scsi' or 'serial' - :param properties: Property values to match - :type properties: dict[property, value] - :param list profile_list: List of profiles to filter on - :return: True if the item matches all filters, False if not. + Args: + item (OVFItem): Item to validate + resource_type (str): Resource type string like 'scsi' or 'serial' + properties (dict): Properties and their values to match + profile_list (list): List of profiles to filter on + + Returns: + bool: True if the item matches all filters, False if not. """ if resource_type and (self.ovf.RES_MAP[resource_type] != item.get_value(self.ovf.RESOURCE_TYPE)): @@ -229,11 +239,13 @@ def find_all_items(self, resource_type=None, properties=None, profile_list=None): """Find all items matching the given type, properties, and profiles. - :param str resource_type: Resource type string like 'scsi' or 'serial' - :param properties: Property values to match - :type properties: dict[property, value] - :param list profile_list: List of profiles to filter on - :return: list of :class:`OVFItem` instances + Args: + resource_type (str): Resource type string like 'scsi' or 'serial' + properties (dict): Properties and their values to match + profile_list (list): List of profiles to filter on + + Returns: + list: Matching :class:`~COT.ovf.item.OVFItem` instances """ items = [self.item_dict[instance] for instance in natural_sort(self.item_dict)] @@ -249,12 +261,16 @@ def find_all_items(self, resource_type=None, properties=None, def find_item(self, resource_type=None, properties=None, profile=None): """Find the only :class:`OVFItem` of the given :attr:`resource_type`. - :param str resource_type: Resource type string like 'scsi' or 'serial' - :param properties: Property values to match - :type properties: dict[property, value] - :param str profile: Single profile ID to search within - :return: Matching :class:`OVFItem` instance, or None - :raises LookupError: if more than one such Item exists. + Args: + resource_type (str): Resource type string like 'scsi' or 'serial' + properties (dict): Properties and their values to match + profile (str): Single profile ID to search within + + Returns: + OVFItem: Matching instance, or None + + Raises: + LookupError: if more than one such Item exists. """ matches = self.find_all_items(resource_type, properties, [profile]) if len(matches) > 1: @@ -268,9 +284,12 @@ def find_item(self, resource_type=None, properties=None, profile=None): def get_item_count(self, resource_type, profile): """Wrapper for :meth:`get_item_count_per_profile`. - :param str resource_type: - :param str profile: Single profile identifier string to look up. - :return: Number of items of this type in this profile. + Args: + resource_type (str): Resource type string like 'scsi' or 'serial' + profile (str): Single profile identifier string to look up. + + Returns: + int: Number of items of this type in this profile. """ return (self.get_item_count_per_profile(resource_type, [profile]) [profile]) @@ -281,11 +300,14 @@ def get_item_count_per_profile(self, resource_type, profile_list): Items present under "no profile" will be counted against the total for each profile. - :param str resource_type: - :param list profile_list: List of profiles to filter on - (default: apply across all profiles) - :return: Dict mapping profile strings to the number of items under - each profile. + Args: + resource_type (str): Resource type string like 'scsi' or 'serial' + profile_list (list): List of profiles to filter on + (default: apply across all profiles) + + Returns: + dict: mapping profile strings to the number of items under + each profile. """ count_dict = {} if not profile_list: @@ -308,11 +330,14 @@ def update_existing_item_count_per_profile(self, resource_type, Helper method for :meth:`set_item_count_per_profile`. - :param str resource_type: 'cpu', 'harddisk', etc. - :param int count: Desired number of items - :param list profile_list: List of profiles to filter on - (default: apply across all profiles) - :return: (count_dict, items_to_add, last_item) + Args: + resource_type (str): 'cpu', 'harddisk', etc. + count (int): Desired number of items + profile_list (list): List of profiles to filter on + (default: apply across all profiles) + + Returns: + tuple: (count_dict, items_to_add, last_item) """ count_dict = self.get_item_count_per_profile(resource_type, profile_list) @@ -354,17 +379,21 @@ def _update_cloned_item(self, new_item, new_item_profiles, item_count): Helper method for :meth:`set_item_count_per_profile`. - :param new_item: Newly cloned Item - :type new_item: :class:`~COT.ovf.item.OVFItem` - :param list new_item_profiles: Profiles new_item should belong to - :param int item_count: How many Items of this type (including this - item) now exist. Used with - :meth:`COT.platform.GenericPlatform.guess_nic_name` - :return: Updated :param:`new_item` - :raises NotImplementedError: No support yet for updating ``Address`` - :raises NotImplementedError: If updating ``AddressOnParent`` but the - prior value varies across config profiles. - :raises NotImplementedError: if ``AddressOnParent`` is not an integer. + Args: + new_item (OVFItem): Newly cloned Item + new_item_profiles (list): Profiles new_item should belong to + item_count (int): How many Items of this type (including this + item) now exist. Used with + :meth:`COT.platform.GenericPlatform.guess_nic_name` + + Returns: + OVFItem: Updated :param:`new_item` + + Raises: + NotImplementedError: No support yet for updating ``Address`` + NotImplementedError: If updating ``AddressOnParent`` but the + prior value varies across config profiles. + NotImplementedError: if ``AddressOnParent`` is not an integer. """ resource_type = new_item.hardware_type address = new_item.get(self.ovf.ADDRESS) @@ -416,10 +445,11 @@ def set_item_count_per_profile(self, resource_type, count, profile_list): If the new count is less than the current count under this profile, then the highest-numbered instances will be removed preferentially. - :param str resource_type: 'cpu', 'harddisk', etc. - :param int count: Desired number of items - :param list profile_list: List of profiles to filter on - (default: apply across all profiles) + Args: + resource_type (str): 'cpu', 'harddisk', etc. + count (int): Desired number of items + profile_list (list): List of profiles to filter on + (default: apply across all profiles) """ if not profile_list: # Set the profile list for all profiles, including the default @@ -460,13 +490,14 @@ def set_value_for_all_items(self, resource_type, prop_name, new_value, :attr:`create_new` is set to ``True``; otherwise will log a warning and do nothing. - :param str resource_type: Resource type such as 'cpu' or 'harddisk' - :param str prop_name: Property name to update - :param str new_value: New value to set the property to - :param list profile_list: List of profiles to filter on - (default: apply across all profiles) - :param boolean create_new: Whether to create a new entry if no items - of this :attr:`resource_type` presently exist. + Args: + resource_type (str): Resource type such as 'cpu' or 'harddisk' + prop_name (str): Property name to update + new_value (str): New value to set the property to + profile_list (list): List of profiles to filter on + (default: apply across all profiles) + create_new (bool): Whether to create a new entry if no items + of this :attr:`resource_type` presently exist. """ ovfitem_list = self.find_all_items(resource_type) if not ovfitem_list: @@ -488,14 +519,15 @@ def set_item_values_per_profile(self, resource_type, prop_name, value_list, profile_list, default=None): """Set value(s) for a property of multiple items of a type. - :param str resource_type: Device type such as 'harddisk' or 'cpu' - :param str prop_name: Property name to update - :param list value_list: List of values to set (one value per item - of the given :attr:`resource_type`) - :param list profile_list: List of profiles to filter on - (default: apply across all profiles) - :param str default: If there are more matching items than entries in - :attr:`value_list`, set extra items to this value + Args: + resource_type (str): Device type such as 'harddisk' or 'cpu' + prop_name (str): Property name to update + value_list (list): List of values to set (one value per item + of the given :attr:`resource_type`) + profile_list (list): List of profiles to filter on + (default: apply across all profiles) + default (str): If there are more matching items than entries in + :attr:`value_list`, set extra items to this value """ if profile_list is None: profile_list = self.ovf.config_profiles + [None] diff --git a/COT/ovf/item.py b/COT/ovf/item.py index c0b5aeb..123c69d 100644 --- a/COT/ovf/item.py +++ b/COT/ovf/item.py @@ -59,8 +59,11 @@ def list_union(*lists): >>> list_union(['bar', 'foo'], ['foo'], ['bar']) ['bar', 'foo'] - :param list lists: List of lists to unify. - :return: List of all distinct values across the given lists. + Args: + lists (list): List of lists to unify. + + Returns: + list: All distinct values across the given lists. """ result = [] for l in lists: @@ -92,9 +95,9 @@ class OVFItem(object): def __init__(self, ovf, item=None): """Create a new OVFItem with contents based on the given Item element. - :param OVF ovf: OVF instance that owns the Item (optional) - :param item: 'Item' element (optional) - :type item: :class:`xml.etree.ElementTree.Element` + Args: + ovf (OVF): OVF instance that owns the Item (optional) + item (xml.etree.ElementTree.Element): 'Item' element (optional) """ self.ovf = ovf if ovf is not None: @@ -121,10 +124,15 @@ def __str__(self): def __getattr__(self, name): """Transparently pass attribute lookups off to OVF/OVFNameHelper. - :param str name: Attribute name. - :return: Value looked up from OVFNameHelper. - :raises AttributeError: Magic methods (``__foo``) will not be passed - through but will raise an AttributeError as usual. + Args: + name (str): Attribute name. + + Returns: + Value looked up from OVFNameHelper. + + Raises: + AttributeError: Magic methods (``__foo``) will not be passed + through but will raise an AttributeError as usual. """ # Don't pass 'special' attributes through to the helper if re.match(r"^__", name): @@ -155,27 +163,35 @@ def hardware_subtype(self): def property_values(self, name): """Get list of values known for a given property name. - :param str name: Property name. - :return: List of values + Args: + name (str): Property name. + + Returns: + list: List of values """ return list(self.properties[name].keys()) def property_profiles(self, name, value): """Get set of profiles associated with a property name and value. - :param str name: Property name. - :param value: Property value of interest. - :type value: str, tuple - :return: Set of profile strings + Args: + name (str): Property name. + value (object): Property value of interest. + + Returns: + set: Profile strings associated with this name/value. """ return self.properties[name][value] def all_profiles(self, name, default=None): """Superset of all profiles for which this name has a value. - :param str name: Property name. - :param object default: Default value to return if there are no matches - :return: Set of profile strings, or the given `default` if no matches. + Args: + name (str): Property name. + default (object): Default value to return if there are no matches + + Returns: + Set of profile strings, or the given `default` if no matches. """ value_dict = self.properties.get(name, None) if not value_dict: @@ -185,12 +201,14 @@ def all_profiles(self, name, default=None): def add_item(self, item): """Add the given ``Item`` element to this OVFItem. - :param item: XML ``Item`` element - :type item: :class:`xml.etree.ElementTree.Element` - :raises ValueUnsupportedError: if the ``item`` is not a recognized - Item variant. - :raises OVFItemDataError: if the new Item conflicts with existing data - already in the OVFItem. + Args: + item (xml.etree.ElementTree.Element): XML ``Item`` element + + Raises: + ValueUnsupportedError: if the ``item`` is not a recognized + Item variant. + OVFItemDataError: if the new Item conflicts with existing data + already in the OVFItem. """ logger.debug("Adding new %s", item.tag) self.NS = self.name_helper.namespace_for_item_tag(item.tag) @@ -261,10 +279,13 @@ def value_add_wildcards(self, name, value, profiles): placeholder that we can regenerate at output time. That way, if the VirtualQuantity or ResourceSubType changes, these can change too. - :param str name: Property name - :param str value: Value to add wildcards to. - :param list profiles: Profiles to which this (name, value) applies. - :return: The updated value string with wildcards added. + Args: + name (str): Property name + value (str): Value to add wildcards to. + profiles (list): Profiles to which this (name, value) applies. + + Returns: + str: The updated value string with wildcards added. .. seealso:: :meth:`value_replace_wildcards` @@ -292,10 +313,13 @@ def value_add_wildcards(self, name, value, profiles): def value_replace_wildcards(self, name, value, profiles): """Replace wildcards with actual values. - :param str name: Property name - :param str value: Value to replace wildcards from. - :param list profiles: Profiles to which this (name, value) applies. - :return: The updated value string, with wildcards replaced. + Args: + name (str): Property name + value (str): Value to replace wildcards from. + profiles (list): Profiles to which this (name, value) applies. + + Returns: + The updated value string, with wildcards replaced. .. seealso:: :meth:`value_add_wildcards` @@ -324,9 +348,10 @@ def value_replace_wildcards(self, name, value, profiles): def _set_new_property(self, name, value, profiles): """Helper for :meth:`set_property`. Create a new property entry. - :param str name: Property name - :param str value: Value to store for this property. - :param list profiles: Profiles to which this (name, value) applies. + Args: + name (str): Property name + value (str): Value to store for this property. + profiles (list): Profiles to which this (name, value) applies. """ if not value: return @@ -340,12 +365,15 @@ def _set_new_property(self, name, value, profiles): def _set_existing_property(self, name, value, profiles, overwrite): """Helper for :meth:`set_property`. Update an existing property. - :param str name: Property name - :param str value: Value to store for this property. - :param list profiles: Profiles to which this (name, value) applies. - :param bool overwrite: Whether to permit overwriting existing values. - :raises OVFItemDataError: If ``overwrite`` is False and the value is - already set for one or more of the requested ``profiles``. + Args: + name (str): Property name + value (str): Value to store for this property. + profiles (list): Profiles to which this (name, value) applies. + overwrite (bool): Whether to permit overwriting existing values. + + Raises: + OVFItemDataError: If ``overwrite`` is False and the value is + already set for one or more of the requested ``profiles``. """ for (known_value, profile_set) in list(self.properties[name].items()): if not overwrite and profile_set.intersection(profiles): @@ -388,16 +416,17 @@ def _set_existing_property(self, name, value, profiles, overwrite): def set_property(self, name, value, profiles=None, overwrite=True): """Store the value and profiles associated with it for the given name. - :param str name: Property name - :param str value: Value associated with :attr:`name` - :param profiles: If ``None``, set for all profiles currently - known to this item, else set only for the given list of profiles. - :type profiles: list[str] - :param boolean overwrite: Whether to permit overwriting of existing - value set in this item. - - :raises OVFItemDataError: if a value is already defined and would be - overwritten, unless :attr:`overwrite` is ``True`` + Args: + name (str): Property name + value (str): Value associated with :attr:`name` + profiles (list): If ``None``, set for all profiles currently known + to this item, else set only for the given list of profiles. + overwrite (bool): Whether to permit overwriting of existing + value set in this item. + + Raises: + OVFItemDataError: if a value is already defined and would be + overwritten, unless :attr:`overwrite` is ``True`` """ # A ResourceSubType in the XML can be a single value or a # space-separated list of values. Internally, we'll store it as a @@ -442,11 +471,14 @@ def set_property(self, name, value, profiles=None, overwrite=True): def add_profile(self, new_profile, from_item=None): """Add a new profile to this item. - :param str new_profile: Profile name to add - :param OVFItem from_item: Item to inherit properties from. If unset, - this defaults to ``self``. - :raises RuntimeError: If unable to determine what value to inherit for - a particular property. + Args: + new_profile (str): Profile name to add + from_item (OVFItem): Item to inherit properties from. If unset, + this defaults to ``self``. + + Raises: + RuntimeError: If unable to determine what value to inherit for + a particular property. """ if self.has_profile(new_profile): logger.error("Profile %s already exists under %s!", @@ -482,11 +514,12 @@ def add_profile(self, new_profile, from_item=None): def remove_profile(self, profile, split_default=True): """Remove all trace of the given profile from this item. - :param str profile: Profile name to remove - :param bool split_default: If False, do not split out 'default' - profile items to specifically exclude this profile. Used when the - profile being removed will no longer exist anywhere and so - 'default' will continue to exclude this profile. + Args: + profile (str): Profile name to remove + split_default (bool): If False, do not split out 'default' + profile items to specifically exclude this profile. Used when + the profile being removed will no longer exist anywhere and + so 'default' will continue to exclude this profile. """ if not self.has_profile(profile): logger.error("Requested deletion of profile '%s' but it is " @@ -522,8 +555,11 @@ def remove_profile(self, profile, split_default=True): def get(self, tag): """Get the dict associated with the given XML tag, if any. - :param str tag: XML tag to look up - :return: Dictionary of values associated with this tag (TODO?) + Args: + tag (str): XML tag to look up + + Returns: + dict: Dictionary of values associated with this tag (TODO?) """ return self.properties.get(tag, None) @@ -536,10 +572,12 @@ def _get_value(self, tag, profiles=None): If the tag does not exist under these profiles, or the tag values differ across the profiles, returns ``None``. - :param str tag: Tag to retrieve value for - :param profiles: set of profile names, or None - :type profiles: set of strings - :return: Value, default value, or ``None``, unsanitized. + Args: + tag (str): Tag to retrieve value for + profiles (set): set of profile names, or None + + Returns: + str,tuple: Value, default value, or ``None``, unsanitized. """ if profiles is not None: profiles = set(profiles) @@ -571,12 +609,16 @@ def get_value(self, tag, profiles=None): If the tag does not exist under these profiles, or the tag values differ across the profiles, returns ``None``. - :param str tag: Tag to retrieve value for - :param profiles: set of profile names, or None - :type profiles: set of strings - :return: Value string or list, or ``None`` - :raises OVFItemDataError: if :meth:`value_replace_wildcards` failed to - remove any wildcards from the internally stored value. + Args: + tag (str): Tag to retrieve value for + profiles (set): set of profile names, or None + + Returns: + Value string or list, or ``None`` + + Raises: + OVFItemDataError: if :meth:`value_replace_wildcards` failed to + remove any wildcards from the internally stored value. """ val = self._get_value(tag, profiles) val = self.value_replace_wildcards(tag, val, profiles) @@ -591,8 +633,11 @@ def get_value(self, tag, profiles=None): def get_all_values(self, tag): """Get the list of all value strings for the given tag. - :param str tag: Tag to retrieve value for - :return: List of value strings. + Args: + tag (str): Tag to retrieve value for + + Returns: + list: List of value strings. """ if tag == self.RESOURCE_SUB_TYPE: # ResourceSubType values may themselves be tuples @@ -605,8 +650,9 @@ def validate(self): Also clean up any oddities (like a property value assigned to 'all profiles' and also redundantly to a specific profile). - :raises RuntimeError: if validation fails and COT doesn't know - how to automatically repair the error(s) identified. + Raises: + RuntimeError: if validation fails and COT doesn't know + how to automatically repair the error(s) identified. """ # An OVFItem must describe only one InstanceID # All Items with a given InstanceID must have the same ResourceType @@ -636,8 +682,11 @@ def validate(self): def has_profile(self, profile): """Check if this Item exists under the given profile. - :param str profile: Profile name - :return: True if the item exists in this profile, False if not. + Args: + profile (str): Profile name + + Returns: + bool: True if the item exists in this profile, False if not. """ profiles = self.all_profiles(self.INSTANCE_ID) if profiles is None: @@ -651,7 +700,8 @@ def has_profile(self, profile): def get_nonintersecting_set_list(self): """Identify the minimal non-intersecting set of profiles. - :return: List of profile-set strings. + Returns: + list: List of profile-set strings. """ set_list = [] for name in self.property_names: @@ -694,8 +744,8 @@ def get_nonintersecting_set_list(self): def generate_items(self): """Get a list of Item XML elements derived from this object's data. - :return: Generated list of XML Item elements - :rtype: list[xml.etree.ElementTree.Element] + Returns: + list: Generated list of XML Item elements """ set_string_list = self.get_nonintersecting_set_list() diff --git a/COT/ovf/name_helper.py b/COT/ovf/name_helper.py index df4f55d..d25b592 100644 --- a/COT/ovf/name_helper.py +++ b/COT/ovf/name_helper.py @@ -38,9 +38,11 @@ def name_helper(version): """Generate an instance of the correct OVFNameHelper variant class. - :param float version: OVF specification version to use, such as 0.9, - 1.0, or 2.0 - :return: Instance of OVFNameHelper[012] as appropriate. + Args: + version (float): OVF specification version to use, such as 0.9, + 1.0, or 2.0 + Returns: + Instance of OVFNameHelper[012] as appropriate. """ if version < 1.0: return OVFNameHelper0() @@ -56,8 +58,9 @@ class _Tag(object): def __init__(self, namespace_name, tag): """Store namespace name and tag. - :param str namespace_name: XML namespace name - :param str tag: XML tag + Args: + namespace_name (str): XML namespace name + tag (str): XML tag """ self.namespace_name = namespace_name.upper() self.tag = tag @@ -87,6 +90,7 @@ class OVFNameHelper1(object): # Older OVF versions have ethernet and storage items # in the same RASD namespace as other hardware, but 2.x has separate ) + """Shorthand for XML namespace URIs usually seen in a version 1.x OVF.""" # Non-standard namespaces (such as VMWare's # 'http://www.vmware.com/schema/ovf') should not be added to the NSM @@ -114,6 +118,12 @@ class OVFNameHelper1(object): 'parallel': '22', 'usb': '23', } + """Mapping of human-readable strings to ResourceType values. + + See + http://schemas.dmtf.org/wbem/cim-html/2/CIM_ResourceAllocationSettingData.html + for more details. + """ # noqa: E501 # Cached strings, built on the fly _cache = {} @@ -258,9 +268,12 @@ class OVFNameHelper1(object): def __getattr__(self, name): """Transparently pass attribute lookups to _raw and _cache. - :param str name: Attribute name to look up. - :return: Value looked up from :attr:`_raw` and/or :attr:`_cache`. - :raises AttributeError: if the given ``name`` is not found. + Args: + name (str): Attribute name to look up. + Returns: + Value looked up from :attr:`_raw` and/or :attr:`_cache`. + Raises: + AttributeError: if the given ``name`` is not found. """ if name in self._item_children: return self._item_children[name] @@ -318,8 +331,10 @@ def __init__(self): def namespace_for_item_tag(self, tag): """Get the XML namespace for the given item tag. - :param str tag: Un-namespaced XML tag. - :return: XML namespace string, or None. + Args: + tag (str): Un-namespaced XML tag. + Returns: + str: XML namespace string, or None. """ if tag == self.ITEM: return self.RASD @@ -332,8 +347,10 @@ def namespace_for_item_tag(self, tag): def namespace_for_resource_type(self, resource_type): """Get the XML namespace for the given ResourceType. - :param str resource_type: ResourceType value string. - :return: XML namespace string, or None. + Args: + resource_type (str): ResourceType value string. + Returns: + str: XML namespace string, or None. """ if resource_type == self.RES_MAP['ethernet']: return self.EPASD @@ -346,9 +363,12 @@ def namespace_for_resource_type(self, resource_type): def item_tag_for_namespace(self, ns): """Get the Item tag for the given XML namespace. - :param str ns: XML namespace - :return: 'Item', 'StorageItem', or 'EthernetPortItem' as appropriate. - :raises ValueUnsupportedError: if the namespace is unrecognized + Args: + ns (str): XML namespace + Returns: + str: 'Item', 'StorageItem', or 'EthernetPortItem' as appropriate. + Raises: + ValueUnsupportedError: if the namespace is unrecognized """ if ns == self.RASD: return self.ITEM @@ -373,6 +393,8 @@ class OVFNameHelper0(OVFNameHelper1): OVFNameHelper1.NSM, ovf="http://www.vmware.com/schema/ovf/1/envelope", ) + """Shorthand for XML namespace URIs usually seen in a version 0.x OVF.""" + _cache = dict(OVFNameHelper1._cache) _raw = dict( OVFNameHelper1._raw, @@ -464,6 +486,8 @@ class OVFNameHelper2(OVFNameHelper1): sasd=(CIM_URI + "/cim-schema/2/CIM_StorageAllocationSettingData.xsd"), ) + """Shorthand for XML namespace URIs usually seen in a version 2.x OVF.""" + _cache = dict(OVFNameHelper1._cache) _raw = dict( OVFNameHelper1._raw, diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index f39c1d9..d7f8449 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -14,7 +14,6 @@ # of COT, including this file, may be copied, modified, propagated, or # distributed except according to the terms contained in the LICENSE.txt file. -# TODO update me """Module for handling OVF and OVA virtual machine description files. **Functions** @@ -78,12 +77,13 @@ def byte_count(base_val, multiplier): >>> byte_count("512", "MegaBytes") 536870912 - :param str base_val: Base value string (value of ``ovf:capacity``, etc.) - :param str multiplier: Multiplier string (value of - ``ovf:capacityAllocationUnits``, etc.) + Args: + base_val (str): Base value string (value of ``ovf:capacity``, etc.) + multiplier (str): Multiplier string (value of + ``ovf:capacityAllocationUnits``, etc.) - :return: Number of bytes - :rtype: int + Returns: + int: Number of bytes """ if not multiplier: return int(base_val) @@ -121,8 +121,11 @@ def factor_bytes(byte_value): >>> factor_bytes(134217729) ('134217729', 'byte') - :param int byte_value: Number of bytes - :return: ``(base_val, multiplier)`` + Args: + byte_value (int): Number of bytes + + Returns: + ``(base_val, multiplier)`` """ shift = 0 byte_value = int(byte_value) @@ -161,10 +164,13 @@ def byte_string(byte_value, base_shift=0): >>> byte_string(2560) '2.5 KiB' - :param float byte_value: Value - :param int base_shift: Base value of byte_value - (0 = bytes, 1 = KiB, 2 = MiB, etc.) - :return: Pretty-printed byte string such as "1.00 GiB" + Args: + byte_value (float): Value + base_shift (int): Base value of byte_value + (0 = bytes, 1 = KiB, 2 = MiB, etc.) + + Returns: + Pretty-printed byte string such as "1.00 GiB" """ tags = ["B", "KiB", "MiB", "GiB", "TiB"] byte_value = float(byte_value) @@ -211,9 +217,14 @@ def detect_type_from_name(filename): Does not check file contents, as the given filename may not yet exist. - :param str filename: File name/path - :return: '.ovf' or '.ova' - :raises ValueUnsupportedError: if filename doesn't match ovf/ova + Args: + filename (str): File name/path + + Returns: + '.ovf' or '.ova' + + Raises: + ValueUnsupportedError: if filename doesn't match ovf/ova """ # We don't care about any directory path filename = os.path.basename(filename) @@ -244,8 +255,11 @@ def _ovf_descriptor_from_name(self, input_file): 2. The file may be an OVA, in which case we need to untar it and return the path to the extracted OVF descriptor. - :param str input_file: Path to an OVF descriptor or OVA file. - :return: OVF descriptor path + Args: + input_file (str): Path to an OVF descriptor or OVA file. + + Returns: + OVF descriptor path """ extension = self.detect_type_from_name(input_file) if extension == '.ova': @@ -259,17 +273,22 @@ def _ovf_descriptor_from_name(self, input_file): def __init__(self, input_file, output_file): """Open the specified OVF and read its XML into memory. - :param str input_file: Data file to read in. - :param str output_file: File name to write to. If this VM is read-only, - (there will never be an output file) this value should be ``None``; - if the output filename is not yet known, use ``""`` and subsequently - set :attr:`output_file` when it is determined. - :raises VMInitError: if the OVF descriptor cannot be located - :raises VMInitError: if an XML parsing error occurs - :raises VMInitError: if the XML is not actually an OVF descriptor - :raises VMInitError: if the OVF hardware validation fails - :raises Exception: will call :meth:`destroy` to clean up before - reraising any exception encountered. + Args: + input_file (str): Data file to read in. + output_file (str): File name to write to. If this VM is read-only, + (there will never be an output file) this value should be + ``None``; if the output filename is not yet known, use + ``""`` and subsequently set :attr:`output_file` when it is + determined. + + Raises: + VMInitError: + * if the OVF descriptor cannot be located + * if an XML parsing error occurs + * if the XML is not actually an OVF descriptor + * if the OVF hardware validation fails + Exception: will call :meth:`destroy` to clean up before + reraising any exception encountered. """ try: self.output_extension = None @@ -394,7 +413,8 @@ def _init_check_file_entries(self): def output_file(self): """OVF or OVA file that will be created or updated by :meth:`write`. - :raises ValueUnsupportedError: if :func:`detect_type_from_name` fails + Raises: + ValueUnsupportedError: if :func:`detect_type_from_name` fails """ return super(OVF, self).output_file @@ -473,7 +493,8 @@ def platform(self): def validate_hardware(self): """Check sanity of hardware properties for this VM/product/platform. - :return: ``True`` if hardware is sane, ``False`` if not. + Returns: + ``True`` if hardware is sane, ``False`` if not. """ result = True @@ -487,10 +508,13 @@ def validate_hardware(self): def _validate_helper(label, fn, *args): """Call validation function, catch errors and warn user instead. - :param str label: Label to prepend to any warning messages - :param function fn: Validation function to call. - :param args: Arguments to validation function. - :return: True if valid, False if invalid + Args: + label (str): Label to prepend to any warning messages + fn (function): Validation function to call. + args: Arguments to validation function. + + Returns: + bool: True if valid, False if invalid """ try: fn(*args) @@ -743,10 +767,15 @@ def application_url(self, app_url_string): def __getattr__(self, name): """Transparently pass attribute lookups off to name_helper. - :param str name: Attribute being looked up. - :return: Attribute value - :raises AttributeError: Magic methods (``__foo``) will not be passed - through but will raise an AttributeError as usual. + Args: + name (str): Attribute being looked up. + + Returns: + Attribute value + + Raises: + AttributeError: Magic methods (``__foo``) will not be passed + through but will raise an AttributeError as usual. """ # Don't pass 'special' attributes through to the helper if re.match(r"^__", name): @@ -886,8 +915,11 @@ def validate_and_update_networks(self): def _info_string_header(self, width): """Generate OVF/OVA file header for :meth:`info_string`. - :param int width: Line length to wrap to where possible. - :return: File header string + Args: + width (int): Line length to wrap to where possible. + + Returns: + str: File header """ str_list = [] str_list.append('-' * width) @@ -901,11 +933,14 @@ def _info_string_header(self, width): def _info_string_product(self, verbosity_option, wrapper): """Generate product information as part of :meth:`info_string`. - :param str verbosity_option: ``'brief'``, ``None`` (default), - or ``'verbose'`` - :param wrapper: Helper object for wrapping text lines if needed. - :type wrapper: :class:`textwrap.TextWrapper` - :return: Product information string + Args: + verbosity_option (str): ``'brief'``, ``None`` (default), + or ``'verbose'`` + wrapper (textwrap.TextWrapper): Helper object for wrapping text + lines if needed. + + Returns: + str: Product information """ if ((not any([self.product, self.vendor, self.version_short])) and (verbosity_option == 'brief' or not any([ @@ -936,9 +971,12 @@ def _info_string_product(self, verbosity_option, wrapper): def _info_string_annotation(self, wrapper): """Generate annotation information as part of :meth:`info_string`. - :param wrapper: Helper object for wrapping text lines if needed. - :type wrapper: :class:`textwrap.TextWrapper` - :return: Annotation information string, or None + Args: + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. + + Returns: + Annotation information string, or None """ if self.annotation_section is None: return None @@ -962,11 +1000,14 @@ def _info_string_annotation(self, wrapper): def _info_string_eula(self, verbosity_option, wrapper): """Generate EULA information as part of :meth:`info_string`. - :param str verbosity_option: ``'brief'``, ``None`` (default), - or ``'verbose'`` - :param wrapper: Helper object for wrapping text lines if needed. - :type wrapper: :class:`textwrap.TextWrapper` - :return: EULA string + Args: + verbosity_option (str): ``'brief'``, ``None`` (default), + or ``'verbose'`` + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. + + Returns: + str: EULA information """ # An OVF may have zero, one, or more eula_header = False @@ -1014,9 +1055,11 @@ def _info_strings_for_file(self, file_obj): Helper for :meth:`_info_string_files_disks`. - :param file_obj: File to inspect - :type file_obj: :class:`xml.etree.ElementTree.Element` - :return: (file_id, file_size, disk_id, disk_capacity, device_info) + Args: + file_obj (xml.etree.ElementTree.Element): File to inspect + + Returns: + tuple: (file_id, file_size, disk_id, disk_capacity, device_info) """ # FILE_SIZE is optional reported_size = file_obj.get(self.FILE_SIZE) @@ -1047,10 +1090,13 @@ def _info_strings_for_file(self, file_obj): def _info_string_files_disks(self, width, verbosity_option): """Describe files and disks as part of :meth:`info_string`. - :param int width: Line length to wrap to where possible. - :param str verbosity_option: ``'brief'``, ``None`` (default), - or ``'verbose'`` - :return: File/disk information string, or None + Args: + width (int): Line length to wrap to where possible. + verbosity_option (str): ``'brief'``, ``None`` (default), + or ``'verbose'`` + + Returns: + File/disk information string, or None """ file_list = self.references.findall(self.FILE) disk_list = (self.disk_section.findall(self.DISK) @@ -1110,9 +1156,12 @@ def _info_string_files_disks(self, width, verbosity_option): def _info_string_hardware(self, wrapper): """Describe hardware subtypes as part of :meth:`info_string`. - :param wrapper: Helper object for wrapping text lines if needed. - :type wrapper: :class:`textwrap.TextWrapper` - :return: Hardware information string, or None + Args: + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. + + Returns: + Hardware information string, or None """ virtual_system_types = self.system_types scsi_subtypes = list_union( @@ -1147,11 +1196,14 @@ def _info_string_hardware(self, wrapper): def _info_string_networks(self, verbosity_option, wrapper): """Describe virtual networks as part of :meth:`info_string`. - :param str verbosity_option: ``'brief'``, ``None`` (default), - or ``'verbose'`` - :param wrapper: Helper object for wrapping text lines if needed. - :type wrapper: :class:`textwrap.TextWrapper` - :return: Network information string, or None + Args: + verbosity_option (str): ``'brief'``, ``None`` (default), + or ``'verbose'`` + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. + + Returns: + Network information string, or None """ if self.network_section is None: return None @@ -1185,11 +1237,14 @@ def _info_string_networks(self, verbosity_option, wrapper): def _info_string_nics(self, verbosity_option, wrapper): """Describe NICs as part of :meth:`info_string`. - :param str verbosity_option: ``'brief'``, ``None`` (default), - or ``'verbose'`` - :param wrapper: Helper object for wrapping text lines if needed. - :type wrapper: :class:`textwrap.TextWrapper` - :return: NIC information string, or None + Args: + verbosity_option (str): ``'brief'``, ``None`` (default), + or ``'verbose'`` + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. + + Returns: + NIC information string, or None """ if verbosity_option == 'brief': return None @@ -1223,9 +1278,12 @@ def _info_string_nics(self, verbosity_option, wrapper): def _info_string_environment(self, wrapper): """Describe environment for :meth:`info_string`. - :param wrapper: Helper object for wrapping text lines if needed. - :type wrapper: :class:`textwrap.TextWrapper` - :return: Environment information string, or None + Args: + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. + + Returns: + Environment information string, or None """ if not self.environment_transports: return None @@ -1240,11 +1298,14 @@ def _info_string_environment(self, wrapper): def _info_string_properties(self, verbosity_option, wrapper): """Describe config properties for :meth:`info_string`. - :param str verbosity_option: ``'brief'``, ``None`` (default), - or ``'verbose'`` - :param wrapper: Helper object for wrapping text lines if needed. - :type wrapper: :class:`textwrap.TextWrapper` - :return: Property information string, or None + Args: + verbosity_option (str): ``'brief'``, ``None`` (default), + or ``'verbose'`` + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. + + Returns: + Property information string, or None """ properties = self.environment_properties if not properties: @@ -1294,11 +1355,13 @@ def _info_string_properties(self, verbosity_option, wrapper): def info_string(self, width=79, verbosity_option=None): """Get a descriptive string summarizing the contents of this OVF. - :param int width: Line length to wrap to where possible. - :param str verbosity_option: ``'brief'``, ``None`` (default), - or ``'verbose'`` + Args: + width (int): Line length to wrap to where possible. + verbosity_option (str): ``'brief'``, ``None`` (default), + or ``'verbose'`` - :return: Wrapped, appropriately verbose string. + Returns: + Wrapped, appropriately verbose string. """ # Supposedly it's quicker to construct a list of strings then merge # them all together with 'join()' rather than it is to repeatedly @@ -1335,8 +1398,11 @@ def info_string(self, width=79, verbosity_option=None): def device_info_str(self, device_item): """Get a one-line summary of a hardware device. - :param OVFItem device_item: Device to summarize - :return: Descriptive string such as "harddisk @ IDE 1:0" + Args: + device_item (OVFItem): Device to summarize + + Returns: + Descriptive string such as "harddisk @ IDE 1:0" """ if device_item is None: return "" @@ -1365,9 +1431,12 @@ def device_info_str(self, device_item): def profile_info_list(self, width=79, verbose=False): """Get a list describing available configuration profiles. - :param int width: Line length to wrap to if possible - :param str verbose: if True, generate multiple lines per profile - :return: (header, list) + Args: + width (int): Line length to wrap to if possible + verbose (bool): if True, generate multiple lines per profile + + Returns: + (header, list) """ str_list = [] @@ -1439,11 +1508,13 @@ def profile_info_list(self, width=79, verbose=False): def profile_info_string(self, width=79, verbosity_option=None): """Get a string summarizing available configuration profiles. - :param int width: Line length to wrap to if possible - :param str verbosity_option: ``'brief'``, ``None`` (default), - or ``'verbose'`` + Args: + width (int): Line length to wrap to if possible + verbosity_option (str): ``'brief'``, ``None`` (default), + or ``'verbose'`` - :return: Appropriately formatted and verbose string. + Returns: + Appropriately formatted and verbose string. """ header, str_list = self.profile_info_list( width, (verbosity_option != 'brief')) @@ -1452,9 +1523,10 @@ def profile_info_string(self, width=79, verbosity_option=None): def create_configuration_profile(self, pid, label, description): """Create or update a configuration profile with the given ID. - :param str pid: Profile identifier - :param str label: Brief descriptive label for the profile - :param str description: Verbose description of the profile + Args: + pid (str): Profile identifier + label (str): Brief descriptive label for the profile + description (str): Verbose description of the profile """ self.deploy_opt_section = self._ensure_section( self.DEPLOY_OPT_SECTION, "Configuration Profiles") @@ -1476,8 +1548,11 @@ def create_configuration_profile(self, pid, label, description): def delete_configuration_profile(self, profile): """Delete the profile with the given ID. - :param str profile: Profile ID to delete. - :raises LookupError: if the profile does not exist. + Args: + profile (str): Profile ID to delete. + + Raises: + LookupError: if the profile does not exist. """ cfg = self.find_child(self.deploy_opt_section, self.CONFIG, attrib={self.CONFIG_ID: profile}) @@ -1509,8 +1584,9 @@ def delete_configuration_profile(self, profile): def set_cpu_count(self, cpus, profile_list): """Set the number of CPUs. - :param int cpus: Number of CPUs - :param list profile_list: Change only the given profiles + Args: + cpus (int): Number of CPUs + profile_list (list): Change only the given profiles """ logger.info("Updating CPU count in OVF under profile %s to %s", profile_list, cpus) @@ -1523,8 +1599,9 @@ def set_cpu_count(self, cpus, profile_list): def set_memory(self, megabytes, profile_list): """Set the amount of RAM, in megabytes. - :param int megabytes: Memory value, in megabytes - :param list profile_list: Change only the given profiles + Args: + megabytes (int): Memory value, in megabytes + profile_list (list): Change only the given profiles """ logger.info("Updating RAM in OVF under profile %s to %s", profile_list, megabytes) @@ -1541,8 +1618,9 @@ def set_memory(self, megabytes, profile_list): def set_nic_types(self, type_list, profile_list): """Set the hardware type(s) for NICs. - :param list type_list: NIC hardware type(s) - :param list profile_list: Change only the given profiles. + Args: + type_list (list): NIC hardware type(s) + profile_list (list): Change only the given profiles. """ # Just to be safe... type_list = [canonicalize_nic_subtype(t) for t in type_list] @@ -1555,9 +1633,11 @@ def set_nic_types(self, type_list, profile_list): def get_nic_count(self, profile_list): """Get the number of NICs under the given profile(s). - :param list profile_list: Profile(s) of interest. - :rtype: dict - :return: ``{ profile_name : nic_count }`` + Args: + profile_list (list): Profile(s) of interest. + + Returns: + dict: ``{ profile_name : nic_count }`` """ return self.hardware.get_item_count_per_profile('ethernet', profile_list) @@ -1565,8 +1645,9 @@ def get_nic_count(self, profile_list): def set_nic_count(self, count, profile_list): """Set the given profile(s) to have the given number of NICs. - :param int count: number of NICs - :param list profile_list: Change only the given profiles + Args: + count (int): number of NICs + profile_list (list): Change only the given profiles """ logger.info("Updating NIC count in OVF under profile %s to %s", profile_list, count) @@ -1579,8 +1660,9 @@ def create_network(self, label, description): Also serves to update the description of an existing network label. - :param str label: Brief label for the network - :param str description: Verbose description of the network + Args: + label (str): Brief label for the network + description (str): Verbose description of the network """ self.network_section = self._ensure_section( self.NETWORK_SECTION, @@ -1597,8 +1679,9 @@ def set_nic_networks(self, network_list, profile_list): If the length of :attr:`network_list` is less than the number of NICs, will use the last entry in the list for all remaining NICs. - :param list network_list: List of networks to map NICs to - :param list profile_list: Change only the given profiles + Args: + network_list (list): List of networks to map NICs to + profile_list (list): Change only the given profiles """ self.hardware.set_item_values_per_profile('ethernet', self.CONNECTION, @@ -1613,8 +1696,9 @@ def set_nic_mac_addresses(self, mac_list, profile_list): If the length of :attr:`mac_list` is less than the number of NICs, will use the last entry in the list for all remaining NICs. - :param list mac_list: List of MAC addresses to assign to NICs - :param list profile_list: Change only the given profiles + Args: + mac_list (list): List of MAC addresses to assign to NICs + profile_list (list): Change only the given profiles """ self.hardware.set_item_values_per_profile('ethernet', self.ADDRESS, @@ -1625,8 +1709,9 @@ def set_nic_mac_addresses(self, mac_list, profile_list): def set_nic_names(self, name_list, profile_list): """Set the device names for NICs under the given profile(s). - :param list name_list: List of names to assign. - :param list profile_list: Change only the given profiles + Args: + name_list (list): List of names to assign. + profile_list (list): Change only the given profiles """ self.hardware.set_item_values_per_profile('ethernet', self.ELEMENT_NAME, @@ -1636,17 +1721,20 @@ def set_nic_names(self, name_list, profile_list): def get_serial_count(self, profile_list): """Get the number of serial ports under the given profile(s). - :param list profile_list: Profile(s) of interest. - :rtype: dict - :return: ``{ profile_name : serial_count }`` + Args: + profile_list (list): Profile(s) of interest. + + Returns: + dict: ``{ profile_name : serial_count }`` """ return self.hardware.get_item_count_per_profile('serial', profile_list) def set_serial_count(self, count, profile_list): """Set the given profile(s) to have the given number of serial ports. - :param int count: Number of serial ports - :param list profile_list: Change only the given profiles + Args: + count (int): Number of serial ports + profile_list (list): Change only the given profiles """ logger.info("Updating serial port count under profile %s to %s", profile_list, count) @@ -1655,8 +1743,9 @@ def set_serial_count(self, count, profile_list): def set_serial_connectivity(self, conn_list, profile_list): """Set the serial port connectivity under the given profile(s). - :param list conn_list: List of connectivity strings - :param list profile_list: Change only the given profiles + Args: + conn_list (list): List of connectivity strings + profile_list (list): Change only the given profiles """ self.hardware.set_item_values_per_profile('serial', self.ADDRESS, conn_list, @@ -1665,8 +1754,11 @@ def set_serial_connectivity(self, conn_list, profile_list): def get_serial_connectivity(self, profile): """Get the serial port connectivity strings under the given profile. - :param str profile: Profile of interest. - :return: List of connectivity strings + Args: + profile (str): Profile of interest. + + Returns: + list: connectivity strings """ return [item.get_value(self.ADDRESS) for item in self.hardware.find_all_items('serial', profile_list=[profile])] @@ -1674,8 +1766,9 @@ def get_serial_connectivity(self, profile): def set_scsi_subtypes(self, type_list, profile_list): """Set the device subtype(s) for the SCSI controller(s). - :param list type_list: SCSI subtype string list - :param list profile_list: Change only the given profiles + Args: + type_list (list): SCSI subtype string list + profile_list (list): Change only the given profiles """ # TODO validate supported types by platform self.hardware.set_value_for_all_items('scsi', @@ -1686,8 +1779,9 @@ def set_scsi_subtypes(self, type_list, profile_list): def set_ide_subtypes(self, type_list, profile_list): """Set the device subtype(s) for the IDE controller(s). - :param list type_list: IDE subtype string list - :param list profile_list: Change only the given profiles + Args: + type_list (list): IDE subtype string list + profile_list (list): Change only the given profiles """ # TODO validate supported types by platform self.hardware.set_value_for_all_items('ide', @@ -1698,8 +1792,11 @@ def set_ide_subtypes(self, type_list, profile_list): def get_property_value(self, key): """Get the value of the given property. - :param str key: Property identifier - :return: Value of this property as a string, or ``None`` + Args: + key (str): Property identifier + + Returns: + Value of this property as a string, or ``None`` """ if self.ovf_version < 1.0 or self.product_section is None: return None @@ -1715,12 +1812,15 @@ def _validate_value_for_property(self, prop, value): This applies agnostic criteria such as property type and qualifiers; it knows nothing of the property's actual meaning. - :param prop: Existing Property element. - :type prop: :class:`xml.etree.ElementTree.Element` - :param str value: Proposed value to set for this property. - :raises ValueUnsupportedError: if the value does not meet criteria. - :return: the value, potentially canonicalized. - :rtype: str + Args: + prop (xml.etree.ElementTree.Element): Existing Property element. + value (str): Proposed value to set for this property. + + Returns: + str: the value, potentially canonicalized. + + Raises: + ValueUnsupportedError: if the value does not meet criteria. """ key = prop.get(self.PROP_KEY) @@ -1763,17 +1863,21 @@ def set_property_value(self, key, value, label=None, description=None): """Set the value of the given property (converting value if needed). - :param str key: Property identifier - :param object value: Value to set for this property - :param bool user_configurable: Should this property be configurable at - deployment time by the user? - :param str property_type: Value type - 'string' or 'boolean' - :param str label: Brief explanatory label for this property - :param str description: Detailed description of this property - :return: the (converted) value that was set. - :rtype: str - :raises NotImplementedError: if :attr:`ovf_version` is less than 1.0; - OVF version 0.9 is not currently supported. + Args: + key (str): Property identifier + value (object): Value to set for this property + user_configurable (bool): Should this property be configurable at + deployment time by the user? + property_type (str): Value type - 'string' or 'boolean' + label (str): Brief explanatory label for this property + description (str): Detailed description of this property + + Returns: + str: the (converted) value that was set. + + Raises: + NotImplementedError: if :attr:`ovf_version` is less than 1.0; + OVF version 0.9 is not currently supported. """ if self.ovf_version < 1.0: raise NotImplementedError("No support for setting environment " @@ -1815,12 +1919,15 @@ def set_property_value(self, key, value, def config_file_to_properties(self, file_path, user_configurable=None): """Import each line of a text file into a configuration property. - :raises NotImplementedError: if the :attr:`platform` for this OVF - does not define - :const:`~COT.platforms.GenericPlatform.LITERAL_CLI_STRING` - :param str file_path: File name to import. - :param bool user_configurable: Should the properties be configurable at - deployment time by the user? + Args: + file_path (str): File name to import. + user_configurable (bool): Should the resulting properties be + configurable at deployment time by the user? + + Raises: + NotImplementedError: if the :attr:`platform` for this OVF + does not define + :const:`~COT.platforms.GenericPlatform.LITERAL_CLI_STRING` """ i = 0 if not self.platform.LITERAL_CLI_STRING: @@ -1845,15 +1952,16 @@ def convert_disk_if_needed(self, disk_image, kind): is the only format that VMware supports in OVA packages. * CD-ROM iso images are accepted without change. - :param disk_image: Image to inspect and possibly convert - :type disk_image: instance of :class:`~COT.disks.DiskRepresentation` - or subclass - :param str kind: Image type (harddisk/cdrom). - :return: - * :attr:`disk_image`, if no conversion was required - * or a new :class:`~COT.disks.DiskRepresentation` instance - representing a converted image that has been created in - :attr:`output_dir`. + Args: + disk_image (COT.disks.DiskRepresentation): Image to inspect and + possibly convert + kind (str): Image type (harddisk/cdrom) + + Returns: + * :attr:`disk_image`, if no conversion was required + * or a new :class:`~COT.disks.DiskRepresentation` instance + representing a converted image that has been created in + :attr:`output_dir`. """ if kind != 'harddisk': logger.debug("No disk conversion needed") @@ -1875,11 +1983,16 @@ def search_from_filename(self, filename): ``File`` in the OVF, then using that to find a matching ``Disk`` and ``Item`` entries. - :param str filename: Filename to search from - :return: ``(file, disk, ctrl_item, disk_item)``, any or all of which - may be ``None`` - :raises LookupError: If the ``disk_item`` is found but no ``ctrl_item`` - is found to be its parent. + Args: + filename (str): Filename to search from + + Returns: + ``(file, disk, ctrl_item, disk_item)``, any or all of which + may be ``None`` + + Raises: + LookupError: If the ``disk_item`` is found but no ``ctrl_item`` + is found to be its parent. """ file_obj = None disk = None @@ -1918,13 +2031,19 @@ def search_from_file_id(self, file_id): ``File`` in the OVF, then using that to find a matching ``Disk`` and ``Item`` entries. - :param str file_id: File ID to search from - :return: ``(file, disk, ctrl_item, disk_item)``, any or all of which - may be ``None`` - :raises LookupError: If the ``disk`` entry is found but no - corresponding ``file`` is found. - :raises LookupError: If the ``disk_item`` is found but no ``ctrl_item`` - is found to be its parent. + Args: + file_id (str): File ID to search from + + Returns: + ``(file, disk, ctrl_item, disk_item)``, any or all of which + may be ``None`` + + Raises: + LookupError: + * If the ``disk`` entry is found but no corresponding + ``file`` is found. + * If the ``disk_item`` is found but no ``ctrl_item`` + is found to be its parent. """ if file_id is None: return (None, None, None, None) @@ -1969,10 +2088,13 @@ def search_from_controller(self, controller, address): controller and disk ``Item`` elements, then using the disk ``Item`` to find matching ``File`` and/or ``Disk``. - :param str controller: ``'ide'`` or ``'scsi'`` - :param str address: Device address such as ``'1:0'`` - :return: ``(file, disk, ctrl_item, disk_item)``, any or all of which - may be ``None`` + Args: + controller (str): ``'ide'`` or ``'scsi'`` + address (str): Device address such as ``'1:0'`` + + Returns: + ``(file, disk, ctrl_item, disk_item)``, any or all of which + may be ``None`` """ if controller is None or address is None: return (None, None, None, None) @@ -2049,8 +2171,11 @@ def search_from_controller(self, controller, address): def find_open_controller(self, controller_type): """Find the first open slot on a controller of the given type. - :param str controller_type: ``'ide'`` or ``'scsi'`` - :return: ``(ctrl_item, address_string)`` or ``(None, None)`` + Args: + controller_type (str): ``'ide'`` or ``'scsi'`` + + Returns: + ``(ctrl_item, address_string)`` or ``(None, None)`` """ for ctrl_item in self.hardware.find_all_items(controller_type): ctrl_instance = ctrl_item.get_value(self.INSTANCE_ID) @@ -2077,40 +2202,47 @@ def find_open_controller(self, controller_type): def get_id_from_file(self, file_obj): """Get the file ID from the given opaque file object. - :param file_obj: 'File' element - :type file_obj: xml.etree.ElementTree.Element - :return: 'id' attribute value of this element - :rtype: str + Args: + file_obj (xml.etree.ElementTree.Element): 'File' element + + Returns: + str: 'id' attribute value of this element """ return file_obj.get(self.FILE_ID) def get_path_from_file(self, file_obj): """Get the file path from the given opaque file object. - :param file_obj: 'File' element - :type file_obj: xml.etree.ElementTree.Element - :return: 'href' attribute value of this element - :rtype: str + Args: + file_obj (xml.etree.ElementTree.Element): 'File' element + + Returns: + str: 'href' attribute value of this element """ return file_obj.get(self.FILE_HREF) def get_file_ref_from_disk(self, disk): """Get the file reference from the given opaque disk object. - :param disk: 'Disk' element - :type disk: xml.etree.ElementTree.Element - :return: 'fileRef' attribute value of this element - :rtype: str + Args: + disk (xml.etree.ElementTree.Element): 'Disk' element + + Returns: + str: 'fileRef' attribute value of this element """ return disk.get(self.DISK_FILE_REF) def get_common_subtype(self, device_type): """Get the sub-type common to all devices of the given type. - :param str device_type: Device type such as ``'ide'`` or ``'memory'``. - :return: ``None``, if multiple such devices exist and they do not all - have the same sub-type. - :return: Subtype string common to all devices of the type. + Args: + device_type (str): Device type such as ``'ide'`` or ``'memory'``. + + Returns: + Subtype string common to all devices of the type. + + ``None``, if multiple such devices exist and they do not all + have the same sub-type. """ subtype = None for item in self.hardware.find_all_items(device_type): @@ -2129,18 +2261,19 @@ def check_sanity_of_disk_device(self, disk, file_obj, disk_item, ctrl_item): """Check if the given disk is linked properly to the other objects. - :param disk: Disk object to validate - :type disk: xml.etree.ElementTree.Element - :param file_obj: File object which this disk should be linked to - (optional) - :type file_obj: xml.etree.ElementTree.Element - :param OVFItem disk_item: Disk device object which should link to - this disk (optional) - :param OVFItem ctrl_item: Controller device object which should link - to the :attr:`disk_item` - :raises ValueMismatchError: if the given items are not linked properly. - :raises ValueUnsupportedError: if the :attr:`disk_item` has a - ``HostResource`` value in an unrecognized or invalid format. + Args: + disk (xml.etree.ElementTree.Element): Disk object to validate + file_obj (xml.etree.ElementTree.Element): File object which this + disk should be linked to (optional) + disk_item (OVFItem): Disk device object which should link to + this disk (optional) + ctrl_item (OVFItem): Controller device object which should link + to the :attr:`disk_item` + + Raises: + ValueMismatchError: if the given items are not linked properly. + ValueUnsupportedError: if the :attr:`disk_item` has a + ``HostResource`` value in an unrecognized or invalid format. """ if disk_item is None: return @@ -2173,15 +2306,16 @@ def check_sanity_of_disk_device(self, disk, file_obj, def add_file(self, file_path, file_id, file_obj=None, disk=None): """Add a new file object to the VM or overwrite the provided one. - :param str file_path: Path to file to add - :param str file_id: Identifier string for the file in the VM - :param file_obj: Existing file object to overwrite - :type file_obj: xml.etree.ElementTree.Element - :param disk: Existing disk object referencing :attr:`file`. - :type disk: xml.etree.ElementTree.Element + Args: + file_path (str): Path to file to add + file_id (str): Identifier string for the file in the VM + file_obj (xml.etree.ElementTree.Element): Existing file object + to overwrite + disk (xml.etree.ElementTree.Element): Existing disk object + referencing :attr:`file`. - :return: New or updated file object - :rtype: xml.etree.ElementTree.Element + Returns: + xml.etree.ElementTree.Element: New or updated file object """ logger.debug("Adding File to OVF") @@ -2231,13 +2365,15 @@ def add_file(self, file_path, file_id, file_obj=None, disk=None): def remove_file(self, file_obj, disk=None, disk_drive=None): """Remove the given file object from the VM. - :param file_obj: File object to remove - :type file_obj: xml.etree.ElementTree.Element - :param disk: Disk object referencing :attr:`file` - :type disk: xml.etree.ElementTree.Element - :param OVFItem disk_drive: Disk drive mapping :attr:`file` to a device - :raises ValueUnsupportedError: If the ``disk_drive`` is a device type - other than 'cdrom' or 'harddisk' + Args: + file_obj (xml.etree.ElementTree.Element): File object to remove + disk (xml.etree.ElementTree.Element): Disk object referencing + :attr:`file` + disk_drive (OVFItem): Disk drive mapping :attr:`file` to a device + + Raises: + ValueUnsupportedError: If the ``disk_drive`` is a device type + other than 'cdrom' or 'harddisk' """ self.references.remove(file_obj) del self._file_references[file_obj.get(self.FILE_HREF)] @@ -2262,15 +2398,15 @@ def remove_file(self, file_obj, disk=None, disk_drive=None): def add_disk(self, disk_repr, file_id, drive_type, disk=None): """Add a new disk object to the VM or overwrite the provided one. - :param str disk_repr: Disk file representation - :type disk_repr: COT.disks.DiskRepresentation or subclass - :param str file_id: Identifier string for the file/disk mapping - :param str drive_type: 'harddisk' or 'cdrom' - :param disk: Existing disk object to overwrite - :type disk: xml.etree.ElementTree.Element + Args: + disk_repr (COT.disks.DiskRepresentation): Disk file representation + file_id (str): Identifier string for the file/disk mapping + drive_type (str): 'harddisk' or 'cdrom' + disk (xml.etree.ElementTree.Element): Existing disk object to + overwrite - :return: New or updated disk object - :rtype: xml.etree.ElementTree.Element + Returns: + xml.etree.ElementTree.Element: New or updated disk object """ if drive_type != 'harddisk': if disk is not None: @@ -2316,18 +2452,19 @@ def add_controller_device(self, device_type, subtype, address, ctrl_item=None): """Create a new IDE or SCSI controller, or update existing one. - :param str device_type: ``'ide'`` or ``'scsi'`` - :param subtype: Subtype such as ``'virtio'`` (optional), or list - of subtype values - :type subtype: string, list of strings - :param int address: Controller address such as 0 or 1 (optional) - :param OVFItem ctrl_item: Existing controller device to update - (optional) + Args: + device_type (str): ``'ide'`` or ``'scsi'`` + subtype (object): (Optional) subtype string such as ``'virtio'`` + or list of subtype strings + address (int): Controller address such as 0 or 1 (optional) + ctrl_item (OVFItem): Existing controller device to update + (optional) - :return: New or updated controller device object - :rtype: OVFItem + Returns: + OVFItem: New or updated controller device object - :raises ValueTooHighError: if no more controllers can be created + Raises: + ValueTooHighError: if no more controllers can be created """ if ctrl_item is None: logger.info("Controller not found, adding new Item") @@ -2359,15 +2496,20 @@ def add_controller_device(self, device_type, subtype, address, def _create_new_disk_device(self, drive_type, address, name, ctrl_item): """Helper for :meth:`add_disk_device`, in the case of no prior Item. - :param str drive_type: ``'harddisk'`` or ``'cdrom'`` - :param str address: Address on controller, such as "1:0" (optional) - :param str name: Device name string (optional) - :param OVFItem ctrl_item: Controller object to serve as parent - :return: (disk_item, disk_name) - :raises ValueTooHighError: if the requested address is out of range - for the given controller, or if the controller is already full. - :raises ValueUnsupportedError: if ``name`` is not specified and - ``disk_type`` is not 'harddisk' or 'cdrom'. + Args: + drive_type (str): ``'harddisk'`` or ``'cdrom'`` + address (str): Address on controller, such as "1:0" (optional) + name (str): Device name string (optional) + ctrl_item (OVFItem): Controller object to serve as parent + + Returns: + tuple: (disk_item, disk_name) + + Raises: + ValueTooHighError: if the requested address is out of range + for the given controller, or if the controller is already full. + ValueUnsupportedError: if ``name`` is not specified and + ``disk_type`` is not 'harddisk' or 'cdrom'. """ ctrl_instance = ctrl_item.get_value(self.INSTANCE_ID) if address is None: @@ -2412,20 +2554,21 @@ def add_disk_device(self, drive_type, address, name, description, disk, file_obj, ctrl_item, disk_item=None): """Create a new disk hardware device or overwrite an existing one. - :param str drive_type: ``'harddisk'`` or ``'cdrom'`` - :param str address: Address on controller, such as "1:0" (optional) - :param str name: Device name string (optional) - :param str description: Description string (optional) - :param disk: Disk object to map to this device - :type disk: xml.etree.ElementTree.Element - :param file_obj: File object to map to this device - :type file_obj: xml.etree.ElementTree.Element - :param OVFItem ctrl_item: Controller object to serve as parent - :param OVFItem disk_item: Existing disk device to update instead of - making a new device. - - :return: New or updated disk device object. - :rtype: xml.etree.ElementTree.Element + Args: + drive_type (str): ``'harddisk'`` or ``'cdrom'`` + address (str): Address on controller, such as "1:0" (optional) + name (str): Device name string (optional) + description (str): Description string (optional) + disk (xml.etree.ElementTree.Element): Disk object to map to + this device + file_obj (xml.etree.ElementTree.Element): File object to map to + this device + ctrl_item (OVFItem): Controller object to serve as parent + disk_item (OVFItem): Existing disk device to update instead of + making a new device. + + Returns: + xml.etree.ElementTree.Element: New or updated disk device object. """ if disk_item is None: logger.info("Disk Item not found, adding new Item") @@ -2459,10 +2602,15 @@ def add_disk_device(self, drive_type, address, name, description, def untar(self, file_path): """Untar the OVF descriptor from an .ova to the working directory. - :param str file_path: OVA file path - :raises VMInitError: if the given file does not represent a valid - OVA archive. - :return: Path to extracted OVF descriptor + Args: + file_path (str): OVA file path + + Returns: + Path to extracted OVF descriptor + + Raises: + VMInitError: if the given file does not represent a valid + OVA archive. """ logger.verbose("Untarring %s to working directory %s", file_path, self.working_dir) @@ -2518,10 +2666,13 @@ def untar(self, file_path): def generate_manifest(self, ovf_file): """Construct the manifest file for this package, if possible. - :param str ovf_file: OVF descriptor file path - :returns: True if the manifest was successfully generated, - False if not successful (such as if checksum helper tools are - unavailable). + Args: + ovf_file (str): OVF descriptor file path + + Returns: + bool: True if the manifest was successfully generated, + False if not successful (such as if checksum helper tools are + unavailable). """ (prefix, _) = os.path.splitext(ovf_file) logger.verbose("Generating manifest for %s", ovf_file) @@ -2552,8 +2703,9 @@ def generate_manifest(self, ovf_file): def tar(self, ovf_descriptor, tar_file): """Create a .ova tar file based on the given OVF descriptor. - :param str ovf_descriptor: File path for an OVF descriptor - :param str tar_file: File path for the desired OVA archive. + Args: + ovf_descriptor (str): File path for an OVF descriptor + tar_file (str): File path for the desired OVA archive. """ logger.verbose("Creating tar file %s", tar_file) @@ -2597,14 +2749,17 @@ def _ensure_section(self, section_tag, info_string, attrib=None, parent=None): """If the OVF doesn't already have the given Section, create it. - :param str section_tag: XML tag of the desired section. - :param str info_string: Info string to set if a new Section is created. - :param dict attrib: Attributes to filter by when looking for any - existing section (optional). - :param xml.etree.ElementTree.Element parent: Parent element (optional). - If not specified, :attr:`envelope` will be the parent. - :return: Section element that was found or created - :rtype: xml.etree.ElementTree.Element + Args: + section_tag (str): XML tag of the desired section. + info_string (str): Info string to set if a new Section is created. + attrib (dict): Attributes to filter by when looking for any + existing section (optional). + parent (xml.etree.ElementTree.Element): Parent element (optional). + If not specified, :attr:`envelope` will be the parent. + + Returns: + xml.etree.ElementTree.Element: Section element that was found or + created """ if parent is None: parent = self.envelope @@ -2637,10 +2792,13 @@ def _set_product_section_child(self, child_tag, child_text): Creates the ProductSection itself if necessary. - :param str child_tag: XML tag of the product section child element. - :param str child_text: Text to set for the child element. - :return: The product section element that was updated or created - :rtype: xml.etree.ElementTree.Element + Args: + child_tag (str): XML tag of the product section child element. + child_text (str): Text to set for the child element. + + Returns: + xml.etree.ElementTree.Element: The product section element that + was updated or created """ self.product_section = self._ensure_section( self.PRODUCT_SECTION, @@ -2653,9 +2811,12 @@ def _set_product_section_child(self, child_tag, child_text): def find_parent_from_item(self, item): """Find the parent Item of the given Item. - :param OVFItem item: Item whose parent is desired - :return: :class:`OVFItem` instance representing the parent device, - or None + Args: + item (OVFItem): Item whose parent is desired + + Returns: + :class:`OVFItem` instance representing the parent device, + or None """ if item is None: return None @@ -2671,9 +2832,11 @@ def find_parent_from_item(self, item): def find_item_from_disk(self, disk): """Find the disk Item that references the given Disk. - :param disk: Disk element - :type disk: :class:`xml.etree.ElementTree.Element` - :return: :class:`OVFItem` instance, or None + Args: + disk (xml.etree.ElementTree.Element): Disk element + + Returns: + :class:`OVFItem` instance, or None """ if disk is None: return None @@ -2694,9 +2857,11 @@ def find_item_from_disk(self, disk): def find_item_from_file(self, file_obj): """Find the disk Item that references the given File. - :param file_obj: File element - :type file_obj: :class:`xml.etree.ElementTree.Element` - :return: :class:`OVFItem` instance, or None. + Args: + file_obj (xml.etree.ElementTree.Element): File element + + Returns: + :class:`OVFItem` instance, or None. """ if file_obj is None: return None @@ -2716,9 +2881,12 @@ def find_item_from_file(self, file_obj): def find_disk_from_file_id(self, file_id): """Find the Disk that uses the given file_id for backing. - :param str file_id: File identifier string - :return: Disk element matching the file, or None - :rtype: xml.etree.ElementTree.Element, None + Args: + file_id (str): File identifier string + + Returns: + xml.etree.ElementTree.Element: Disk element matching the file, + or None """ if file_id is None or self.disk_section is None: return None @@ -2729,9 +2897,14 @@ def find_disk_from_file_id(self, file_id): def find_empty_drive(self, drive_type): """Find a disk device that exists but contains no data. - :param str drive_type: Either 'cdrom' or 'harddisk' - :return: :class:`OVFItem` representing this disk device, or None. - :raises ValueUnsupportedError: if ``drive_type`` is unrecognized. + Args: + drive_type (str): Either 'cdrom' or 'harddisk' + + Returns: + :class:`OVFItem` representing this disk device, or None. + + Raises: + ValueUnsupportedError: if ``drive_type`` is unrecognized. """ if drive_type == 'cdrom': # Find a drive that has no HostResource property @@ -2762,9 +2935,14 @@ def find_empty_drive(self, drive_type): def find_device_location(self, device): """Find the controller type and address of a given device object. - :param OVFItem device: Hardware device object. - :returns: ``(type, address)``, such as ``("ide", "1:0")``. - :raises LookupError: if the controller is not found. + Args: + device (OVFItem): Hardware device object. + + Returns: + ``(type, address)``, such as ``("ide", "1:0")``. + + Raises: + LookupError: if the controller is not found. """ controller = self.find_parent_from_item(device) if controller is None: @@ -2776,19 +2954,22 @@ def find_device_location(self, device): def get_id_from_disk(self, disk): """Get the identifier string associated with the given Disk object. - :param disk: Disk object to inspect - :type disk: xml.etree.ElementTree.Element - :return: Disk identifier string + Args: + disk (xml.etree.ElementTree.Element): Disk object to inspect + + Returns: + str: Disk identifier """ return disk.get(self.DISK_ID) def get_capacity_from_disk(self, disk): """Get the capacity of the given Disk in bytes. - :param disk: Disk element to inspect - :type disk: xml.etree.ElementTree.Element - :return: Disk capacity, in bytes - :rtype: int + Args: + disk (xml.etree.ElementTree.Element): Disk element to inspect + + Returns: + int: Disk capacity, in bytes """ cap = int(disk.get(self.DISK_CAPACITY)) cap_units = disk.get(self.DISK_CAP_UNITS, 'byte') @@ -2800,9 +2981,9 @@ def set_capacity_of_disk(self, disk, capacity_bytes): Tries to use the most human-readable form possible (i.e., 8 GiB instead of 8589934592 bytes). - :param disk: Disk to update - :type disk: xml.etree.ElementTree.Element - :param int capacity_bytes: Disk capacity, in bytes + Args: + disk (xml.etree.ElementTree.Element): Disk to update + capacity_bytes (int): Disk capacity, in bytes """ if self.ovf_version < 1.0: # In OVF 0.9 only bytes is supported as a unit diff --git a/docs/conf.py b/docs/conf.py index bb691f3..9aba237 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -244,6 +244,7 @@ def help_text_to_rst(help, dirpath): 'sphinx.ext.ifconfig', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', + 'sphinxcontrib.napoleon', ] # -- Autodoc configuration -------------------- @@ -272,6 +273,10 @@ def autodoc_skip_member(app, what, name, obj, skip, options): 'requests': ('http://docs.python-requests.org/en/latest', None), } +# -- Napoleon configuration ------------------- + +napoleon_use_rtype = True + # -- General configuration, continued --------- # Add any paths that contain templates here, relative to this directory. From 1dd18a9d5b0246ff0d3afe2ae1acd94b9ad544ec Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 31 Oct 2016 13:06:56 -0400 Subject: [PATCH 42/59] Move COT/*.py to google style docstrings --- COT/add_disk.py | 174 +++++++------- COT/add_file.py | 21 +- COT/cli.py | 136 ++++++----- COT/data_validation.py | 213 ++++++++++------- COT/deploy.py | 82 ++++--- COT/deploy_esxi.py | 88 ++++--- COT/edit_hardware.py | 62 ++--- COT/edit_product.py | 16 +- COT/edit_properties.py | 27 ++- COT/file_reference.py | 76 +++--- COT/help.py | 14 +- COT/helpers/helper.py | 4 +- COT/helpers/tests/test_helper.py | 8 +- COT/info.py | 16 +- COT/inject_config.py | 51 ++-- COT/install_helpers.py | 50 ++-- COT/remove_file.py | 18 +- COT/submodule.py | 43 ++-- COT/tests/test_edit_properties.py | 6 +- COT/tests/test_info.py | 2 +- COT/ui_shared.py | 70 +++--- COT/vm_context_manager.py | 2 +- COT/vm_description.py | 384 +++++++++++++++++------------- COT/vm_factory.py | 18 +- COT/xml_file.py | 130 +++++----- docs/conf.py | 4 +- setup.py | 4 +- 27 files changed, 994 insertions(+), 725 deletions(-) diff --git a/COT/add_disk.py b/COT/add_disk.py index 09c0a26..1f6237e 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -53,9 +53,12 @@ def validate_controller_address(controller, address): Helper method for the :attr:`controller`/:attr:`address` setters. - :param str controller: ``'ide'`` or ``'scsi'`` - :param str address: A string like '0:0' or '2:10' - :raises InvalidInputError: if the address/controller combo is invalid. + Args: + controller (str): ``'ide'`` or ``'scsi'`` + address (str): A string like '0:0' or '2:10' + + Raises: + InvalidInputError: if the address/controller combo is invalid. """ logger.info("validate_controller_address: %s, %s", controller, address) if controller is not None and address is not None: @@ -76,7 +79,7 @@ class COTAddDisk(COTSubmodule): """Add or replace a disk in a virtual machine. Inherited attributes: - :attr:`~COTGenericSubmodule.UI`, + :attr:`~COTGenericSubmodule.ui`, :attr:`~COTSubmodule.package`, :attr:`~COTSubmodule.output` @@ -94,8 +97,8 @@ class COTAddDisk(COTSubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTAddDisk, self).__init__(ui) self._disk_image = None @@ -116,7 +119,8 @@ def __init__(self, ui): def disk_image(self): """Disk image file to add to the VM. - :raises InvalidInputError: if the file does not exist. + Raises: + InvalidInputError: if the file does not exist. """ return self._disk_image @@ -128,7 +132,8 @@ def disk_image(self, value): def address(self): """Disk device address on controller (``1:0``, etc.). - :raises InvalidInputError: see :meth:`validate_controller_address` + Raises: + InvalidInputError: see :meth:`validate_controller_address` """ return self._address @@ -142,7 +147,8 @@ def address(self, value): def controller(self): """Disk controller type (``ide``, ``scsi``). - :raises InvalidInputError: see :meth:`validate_controller_address` + Raises: + InvalidInputError: see :meth:`validate_controller_address` """ return self._controller @@ -155,7 +161,8 @@ def controller(self, value): def ready_to_run(self): """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.disk_image is None: return False, "DISK_IMAGE is a mandatory argument!" @@ -167,12 +174,13 @@ def ready_to_run(self): def run(self): """Do the actual work of this submodule. - :raises InvalidInputError: if :func:`ready_to_run` reports ``False`` + Raises: + InvalidInputError: if :meth:`ready_to_run` reports ``False`` """ super(COTAddDisk, self).run() add_disk_worker(self.vm, - ui=self.UI, + ui=self.ui, disk_image=self.disk_image, drive_type=self.drive_type, subtype=self.subtype, @@ -184,11 +192,11 @@ def run(self): def create_subparser(self): """Create 'add-disk' CLI subparser.""" - p = self.UI.add_subparser( + p = self.ui.add_subparser( 'add-disk', aliases=['add-drive'], add_help=False, - usage=self.UI.fill_usage("add-disk", [ + usage=self.ui.fill_usage("add-disk", [ "DISK_IMAGE PACKAGE [-o OUTPUT] [-f FILE_ID] \ [-t {harddisk,cdrom}] [-c {ide,scsi}] [-s SUBTYPE] [-a ADDRESS] \ [-d DESCRIPTION] [-n DISKNAME]" @@ -255,9 +263,12 @@ def create_subparser(self): def guess_drive_type_from_extension(disk_file_name): """Guess the disk type (harddisk/cdrom) from the disk file name. - :param str disk_file_name: File name or file path. - :return: "cdrom" or "harddisk" - :raises InvalidInputError: if the disk type cannot be guessed. + Args: + disk_file_name (str): File name or file path. + Returns: + str: "cdrom" or "harddisk" + Raises: + InvalidInputError: if the disk type cannot be guessed. """ disk_extension = os.path.splitext(disk_file_name)[1] ext_type_map = { @@ -303,15 +314,18 @@ def search_for_elements(vm, disk_file, file_id, controller, address): of the above arguments - in which case we need to make sure that all relevant approaches agree on what sections we're talking about... - :param vm: Virtual machine object - :type vm: :class:`~COT.vm_description.VMDescription` - :param str disk_file: Disk file name or path - :param str file_id: File identifier - :param str controller: controller type, "ide" or "scsi" - :param str address: device address, such as "1:0" + Args: + vm (VMDescription): Virtual machine object + disk_file (str): Disk file name or path + file_id (str): File identifier + controller (str): controller type, "ide" or "scsi" + address (str): device address, such as "1:0" + + Raises: + ValueMismatchError: if the criteria select a non-unique set. - :raises ValueMismatchError: if the criteria select a non-unique set. - :return: (file_object, disk_object, controller_item, disk_item) + Returns: + tuple: (file_object, disk_object, controller_item, disk_item) """ # 1) Check whether the DISK_IMAGE file name matches an existing File # in the OVF (and from there, find the associated Disk and Items) @@ -344,13 +358,15 @@ def search_for_elements(vm, disk_file, file_id, controller, address): def guess_controller_type(vm, ctrl_item, drive_type): """If a controller type wasn't specified, try to guess from context. - :param vm: Virtual machine object - :type vm: :class:`~COT.vm_description.VMDescription` - :param object ctrl_item: Any known controller object - :param str drive_type: "cdrom" or "harddisk" - :return: 'ide' or 'scsi' - :raises ValueUnsupportedError: if ``ctrl_item`` is not an IDE or SCSI - controller device. + Args: + vm (VMDescription): Virtual machine object + ctrl_item (object): Any known controller object + drive_type (str): "cdrom" or "harddisk" + Returns: + str: 'ide' or 'scsi' + Raises: + ValueUnsupportedError: if ``ctrl_item`` is not an IDE or SCSI + controller device. """ if ctrl_item is None: # If the user didn't tell us which controller type they wanted, @@ -376,15 +392,17 @@ def validate_elements(vm, file_obj, disk_obj, disk_item, ctrl_item, file_id, ctrl_type): """Validate any existing file, disk, controller item, and disk item. - :raises ValueMismatchError: if the search criteria select a non-unique set. - :param vm: Virtual machine object - :type vm: :class:`~COT.vm_description.VMDescription` - :param object file_obj: Known file object - :param object disk_obj: Known disk object - :param object disk_item: Known disk device object - :param object ctrl_item: Known controller device object - :param str file_id: File identifier string - :param str ctrl_type: Controller type ("ide" or "scsi") + Raises: + ValueMismatchError: if the search criteria select a non-unique set. + + Args: + vm (VMDescription): Virtual machine object + file_obj (object): Known file object + disk_obj (object): Known disk object + disk_item (object): Known disk device object + ctrl_item (object): Known controller device object + file_id (str): File identifier string + ctrl_type (str): Controller type ("ide" or "scsi") """ # Ok, we now have confirmed that we have at most one of each of these # four objects. Now it's time for some sanity checking... @@ -424,18 +442,17 @@ def confirm_elements(vm, ui, file_obj, disk_image, disk_obj, disk_item, drive_type, controller, ctrl_item, subtype): """Get user confirmation of any risky or unusual operations. - :param vm: Virtual machine object - :type vm: :class:`~COT.vm_description.VMDescription` - :param ui: User interface object - :type ui: :class:`~COT.ui_shared.UI` - :param object file_obj: Known file object - :param str disk_image: Filename or path for disk file - :param object disk_obj: Known disk object - :param object disk_item: Known disk device object - :param str drive_type: "harddisk" or "cdrom" - :param str controller: Controller type ("ide" or "scsi") - :param object ctrl_item: Known controller device object - :param str subtype: Controller subtype (such as "virtio") + Args: + vm (VMDescription): Virtual machine object + ui (UI): User interface object + file_obj (object): Known file object + disk_image (str): Filename or path for disk file + disk_obj (object): Known disk object + disk_item (object): Known disk device object + drive_type (str): "harddisk" or "cdrom" + controller (str): Controller type ("ide" or "scsi") + ctrl_item (object): Known controller device object + subtype (str): Controller subtype (such as "virtio") """ # TODO: more refactoring! if file_obj is not None: @@ -489,37 +506,26 @@ def add_disk_worker(vm, All parameters except ``vm``, ``ui``, and ``disk_image`` are optional and will be automatically determined by COT if unspecified. - :param vm: The virtual machine being edited. - :type vm: :class:`~COT.ovf.OVF` or other - :class:`~COT.vm_description.VMDescription` subclass - - :param ui: User interface in effect. - :type ui: instance of :class:`~COT.ui_shared.UI` or subclass. - - :param disk_image: Disk image to add to the VM. - :type disk_image: instance of :class:`~COT.disks.DiskRepresentation` or - subclass. - - :param str drive_type: Disk drive type: ``'cdrom'`` or ``'harddisk'``. - If not specified, will be derived automatically from the - disk_image file name extension. - - :param str file_id: Identifier of the disk file in the VM. If not - specified, the VM will automatically derive an appropriate value. - - :param str controller: Disk controller type: ``'ide'`` or ``'scsi'``. - If not specified, will be derived from the `type` and the - `platform` of the given `vm`. - - :param str subtype: Controller subtype ('virtio', 'lsilogic', etc.) - :param str address: Disk device address on its controller - (such as ``'1:0'``). If this matches an existing disk device, - that device will be overwritten. If not specified, the first - available address not already occupied by an existing device - will be selected. - - :param str diskname: Name for disk device - :param str description: Description of disk device + Args: + vm (VMDescription): The virtual machine being edited. + ui (UI): User interface in effect. + disk_image (DiskRepresentation): Disk image to add to the VM. + drive_type (str): Disk drive type: ``'cdrom'`` or ``'harddisk'``. + If not specified, will be derived automatically from the + disk_image file name extension. + file_id (str): Identifier of the disk file in the VM. If not + specified, the VM will automatically derive an appropriate value. + controller (str): Disk controller type: ``'ide'`` or ``'scsi'``. + If not specified, will be derived from the `type` and the + `platform` of the given `vm`. + subtype (str): Controller subtype ('virtio', 'lsilogic', etc.) + address (str): Disk device address on its controller + (such as ``'1:0'``). If this matches an existing disk device, + that device will be overwritten. If not specified, the first + available address not already occupied by an existing device + will be selected. + diskname (str): Name for disk device + description (str): Description of disk device """ if drive_type is None: drive_type = guess_drive_type_from_extension(disk_image.path) diff --git a/COT/add_file.py b/COT/add_file.py index 3b86f32..15a7c85 100644 --- a/COT/add_file.py +++ b/COT/add_file.py @@ -33,7 +33,7 @@ class COTAddFile(COTSubmodule): """Add a file (such as a README) to the package. Inherited attributes: - :attr:`~COTGenericSubmodule.UI`, + :attr:`~COTGenericSubmodule.ui`, :attr:`~COTSubmodule.package`, :attr:`~COTSubmodule.output` @@ -45,8 +45,8 @@ class COTAddFile(COTSubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTAddFile, self).__init__(ui) self._file = None @@ -57,7 +57,8 @@ def __init__(self, ui): def file(self): """File to be added to the package. - :raises InvalidInputError: if the file does not exist. + Raises: + InvalidInputError: if the file does not exist. """ return self._file @@ -71,7 +72,8 @@ def file(self, value): def ready_to_run(self): """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.file is None: return False, "FILE is a mandatory argument!" @@ -80,7 +82,8 @@ def ready_to_run(self): def run(self): """Do the actual work of this submodule. - :raises InvalidInputError: if :func:`ready_to_run` reports ``False`` + Raises: + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTAddFile, self).run() @@ -98,7 +101,7 @@ def run(self): self.file_id = filename if file_obj is not None: - self.UI.confirm_or_die("Replace existing file {0} with {1}?" + self.ui.confirm_or_die("Replace existing file {0} with {1}?" .format(vm.get_path_from_file(file_obj), self.file)) logger.warning("Overwriting existing File in OVF") @@ -107,9 +110,9 @@ def run(self): def create_subparser(self): """Create 'add-file' CLI subparser.""" - p = self.UI.add_subparser( + p = self.ui.add_subparser( 'add-file', - usage=self.UI.fill_usage("add-file", [ + usage=self.ui.fill_usage("add-file", [ "FILE PACKAGE [-o OUTPUT] [-f FILE_ID]", ]), help="Add a file to an OVF package", diff --git a/COT/cli.py b/COT/cli.py index c199d00..38859fb 100644 --- a/COT/cli.py +++ b/COT/cli.py @@ -68,9 +68,11 @@ def formatter(verbosity=logging.INFO): We offer different (more verbose) formatting when debugging is enabled, hence this need. - :param int verbosity: Logging level as defined by :mod:`logging`. - :return: Formatter object for use with :mod:`logging`. - :rtype: instance of :class:`colorlog.ColoredFormatter` + 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 = { @@ -118,8 +120,9 @@ class CLI(UI): def __init__(self, terminal_width=None): """Create CLI handler instance. - :param int terminal_width: (optional) Set the terminal width for this - CLI, independent of the actual terminal in use. + Args: + terminal_width (int): (optional) Set the terminal width for this + CLI, independent of the actual terminal in use. """ super(CLI, self).__init__(force=True) # In python 2.7, we want raw_input, but in python 3 we want input. @@ -143,7 +146,7 @@ def __init__(self, terminal_width=None): argcomplete.autocomplete(self.parser) from COT.helpers import Helper - Helper.UI = self + Helper.USER_INTERFACE = self @property def terminal_width(self): @@ -175,10 +178,12 @@ def fill_usage(self, subcommand, usage_list): cot add-file FILE PACKAGE [-o OUTPUT] [-f FILE_ID] - :param str subcommand: Subcommand name/keyword - :param list usage_list: List of usage strings for this subcommand. - :returns: String containing all usage strings, each appropriately - wrapped to the :func:`terminal_width` value. + Args: + subcommand (str): Subcommand name/keyword + usage_list (list): List of usage strings for this subcommand. + Returns: + string: All usage strings, each appropriately wrapped to the + :func:`terminal_width` value. """ # Automatically add a line for --help to the usage output_lines = ["\n cot "+subcommand+" --help"] @@ -259,12 +264,13 @@ def fill_examples(self, example_list): cot deploy foo.ova esxi 192.0.2.100 -u admin -c 1CPU-2.5GB - :param list example_list: List of (description, CLI example) - tuples. + Args: + example_list (list): List of (description, CLI example) tuples. - :return: Examples wrapped appropriately to the :func:`terminal_width` - value. CLI examples will be wrapped with backslashes and - a hanging indent. + Returns: + str: Examples wrapped appropriately to the :func:`terminal_width` + value. CLI examples will be wrapped with backslashes and + a hanging indent. """ output_lines = ["Examples:"] # Just as in fill_usage, the default textwrap behavior @@ -307,7 +313,8 @@ def set_verbosity(self, level): Will call :func:`formatter` and associate the resulting formatter with logging. - :param int level: Logging level as defined by :mod:`logging` + Args: + level (int): Logging level as defined by :mod:`logging` """ if not self.handler: self.handler = logging.StreamHandler() @@ -323,8 +330,10 @@ def run(self, argv): Calls :func:`parse_args` followed by :func:`main`. - :param list argv: The CLI argv value (not including argv[0]) - :return: Return code from :func:`main` + Args: + argv (list): The CLI argv value (not including argv[0]) + Returns: + Return code from :func:`main` """ args = self.parse_args(argv) return self.main(args) @@ -334,9 +343,10 @@ def confirm(self, prompt): Auto-accepts if :attr:`force` is set to ``True``. - :param str prompt: Message to prompt the user with - :return: ``True`` (user confirms acceptance) or ``False`` - (user declines) + Args: + prompt (str): Message to prompt the user with + Returns: + bool: ``True`` (user accepts) or ``False`` (user declines) """ if self.force: logger.warning("Automatically agreeing to '%s'", prompt) @@ -367,12 +377,13 @@ def get_input(self, prompt, default_value): Auto-inputs the :attr:`default_value` if :attr:`force` is set to ``True``. - :param str prompt: Message to prompt the user with - :param str default_value: Default value to input if the user simply - hits Enter without entering a value, or if :attr:`force`. + Args: + prompt (str): Message to prompt the user with + default_value (str): Default value to input if the user simply + hits Enter without entering a value, or if :attr:`force`. - :return: Input value - :rtype: str + Returns: + str: Input value """ if self.force: logger.warning("Automatically entering '%s' in response to '%s'", @@ -387,11 +398,15 @@ def get_input(self, prompt, default_value): def get_password(self, username, host): """Get password string from the user. - :param str username: Username the password is associated with - :param str host: Host the password is associated with - :raises InvalidInputError: if :attr:`force` is ``True`` - (as there is no "default" password value) - :return: Password string + Args: + username (str): Username the password is associated with + host (str): Host the password is associated with + + Raises: + InvalidInputError: if :attr:`force` is ``True`` + (as there is no "default" password value) + Returns: + str: Password string """ if self.force: raise InvalidInputError("No password specified for {0}@{1}" @@ -498,15 +513,17 @@ def add_subparser(self, title, **kwargs): """Create a subparser under the specified parent. - :param str title: Canonical keyword for this subparser - :param object parent: Subparser grouping object returned by - :meth:`ArgumentParser.add_subparsers` - :param list aliases: Aliases for ``title``. Only used - in Python 3.x. - :param str lookup_prefix: String to prepend to ``title`` and - each alias in ``aliases`` for lookup purposes. - :param kwargs: Passed through to :meth:`parent.add_parser` - :return: Subparser object + Args: + title (str): Canonical keyword for this subparser + parent (object): Subparser grouping object returned by + :meth:`ArgumentParser.add_subparsers` + aliases (list): Aliases for ``title``. Only used in Python 3.x. + lookup_prefix (str): String to prepend to ``title`` and + each alias in ``aliases`` for lookup purposes. + kwargs (dict): Passed through to :meth:`parent.add_parser` + + Returns: + object: Subparser object """ # Subparser aliases are only supported by argparse in Python 3.2+ if sys.hexversion >= 0x03020000 and aliases: @@ -527,9 +544,10 @@ def add_subparser(self, title, def parse_args(self, argv): """Parse the given CLI arguments into a namespace object. - :param list argv: List of CLI arguments, not including argv0 - :return: Parser namespace object - :rtype: :class:`argparse.Namespace` + Args: + argv (list): List of CLI arguments, not including argv0 + Returns: + argparse.Namespace: Parser namespace object """ # Parse the user input args = self.parser.parse_args(argv) @@ -545,10 +563,10 @@ def parse_args(self, argv): def args_to_dict(args): """Convert args to a dict and perform any needed cleanup. - :param args: Namespace returned from :meth:`parse_args`. - :type args: :class:`argparse.Namespace` - :return: Dictionary of arg to value - :rtype: dict + Args: + args (argparse.Namespace): Namespace from :meth:`parse_args`. + Returns: + dict: Dictionary of arg to value """ arg_dict = vars(args) del arg_dict["_verbosity"] @@ -569,8 +587,10 @@ def args_to_dict(args): def set_instance_attributes(arg_dict): """Set attributes of the :attr:`instance` based on the given arg_dict. - :param dict arg_dict: Dictionary of (attribute, value). - :raises InvalidInputError: if attributes are not validly set. + Args: + arg_dict (dict): Dictionary of (attribute, value). + Raises: + InvalidInputError: if attributes are not validly set. """ # Set mandatory (CAPITALIZED) args first, then optional args for (arg, value) in arg_dict.items(): @@ -596,15 +616,17 @@ def main(self, args): :func:`~COT.submodule.COTGenericSubmodule.finished`. * Catches various exceptions and handles them appropriately. - :param args: Parser namespace object returned from :func:`parse_args`. - :type args: object - :rtype: int - :return: Exit code for the COT executable. + Args: + args (argparse.Namespace): Parser namespace object returned from + :func:`parse_args`. + + Returns: + int: Exit code for the COT executable. - * 0 on successful completion - * 1 on runtime error - * 2 on input error (parser error, - :class:`~COT.data_validation.InvalidInputError`, etc.) + * 0 on successful completion + * 1 on runtime error + * 2 on input error (parser error, + :class:`~COT.data_validation.InvalidInputError`, etc.) """ # pylint: disable=protected-access self.force = args._force diff --git a/COT/data_validation.py b/COT/data_validation.py index af6e376..6d69fc8 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -65,8 +65,10 @@ def to_string(obj): """Get string representation of an object, special-case for XML Element. - :param object obj: Object to represent as a string. - :return: string representation + Args: + obj (object): Object to represent as a string. + Returns: + str: string representation """ if ET.iselement(obj): return ET.tostring(obj) @@ -77,16 +79,18 @@ def to_string(obj): def alphanum_split(key): """Split the key into a list of [text, int, text, int, ...]. - :param str key: String to split. - :return: List of tokens + Args: + key (str): String to split. + Returns: + List of tokens """ def text_to_int(text): """Convert number strings to ints, leave other strings as text. - :param text: Input to convert - :type text: str, int - :return: Converted value - :rtype: str, int + Args: + text (object): Input to convert (str or int) + Returns: + object: Converted value (str or int) """ return int(text) if text.isdigit() else text @@ -100,8 +104,10 @@ def natural_sort(l): See also http://nedbatchelder.com/blog/200712/human_sorting.html - :param list l: List to sort - :return: Sorted list + Args: + l (list): List to sort + Returns: + list: Sorted list """ # Sort based on alphanum_split return value return sorted(l, key=alphanum_split) @@ -110,11 +116,13 @@ def natural_sort(l): def match_or_die(first_label, first, second_label, second): """Make sure "first" and "second" are equal or raise an error. - :param str first_label: Descriptive label for :attr:`first` - :param object first: First object to compare - :param str second_label: Descriptive label for :attr:`second` - :param object second: Second object to compare - :raises ValueMismatchError: if ``first != second`` + Args: + first_label (str): Descriptive label for :attr:`first` + first (object): First object to compare + second_label (str): Descriptive label for :attr:`second` + second (object): Second object to compare + Raises: + ValueMismatchError: if ``first != second`` """ if first != second: raise ValueMismatchError("{0} {1} does not match {2} {3}" @@ -127,13 +135,16 @@ def match_or_die(first_label, first, second_label, second): def canonicalize_helper(label, user_input, mappings, re_flags=0): """Try to find a mapping of input to output. - :param str label: Label to use in any error raised - :param str user_input: User-provided string - :param list mappings: List of ``(expr, canonical)`` pairs for mapping. - :param int re_flags: ``re.IGNORECASE``, etc. if desired - :returns: The canonical string - :raises ValueUnsupportedError: If no ``expr`` in ``mappings`` matches - ``input``. + Args: + label (str): Label to use in any error raised + user_input (str): User-provided string + mappings (list): List of ``(expr, canonical)`` pairs for mapping. + re_flags (int): ``re.IGNORECASE``, etc. if desired + Returns: + str: The canonical string + Raises: + ValueUnsupportedError: If no ``expr`` in ``mappings`` matches the given + ``user_input``. """ if user_input is None or user_input == "": return None @@ -146,13 +157,16 @@ def canonicalize_helper(label, user_input, mappings, re_flags=0): def canonicalize_ide_subtype(subtype): """Try to convert the given IDE controller string to a canonical form. - :param str subtype: User-provided string - :returns: The canonical string, one of: + Args: + subtype (str): User-provided string + Returns: + str: The canonical string, one of: - - ``PIIX4`` - - ``virtio`` + - ``PIIX4`` + - ``virtio`` - :raises ValueUnsupportedError: If the canonical string cannot be determined + Raises: + ValueUnsupportedError: If the canonical string cannot be determined """ return canonicalize_helper("IDE controller subtype", subtype, [ @@ -176,10 +190,12 @@ def canonicalize_ide_subtype(subtype): def canonicalize_nic_subtype(subtype): """Try to convert the given NIC subtype string to a canonical form. - :param str subtype: User-provided string - :returns: The canonical string, one of :data:`NIC_TYPES` - - :raises ValueUnsupportedError: If the canonical string cannot be determined + Args: + subtype (str): User-provided string + Returns: + str: The canonical string, one of :data:`NIC_TYPES` + Raises: + ValueUnsupportedError: If the canonical string cannot be determined .. seealso:: :meth:`COT.platforms.GenericPlatform.validate_nic_type` @@ -191,16 +207,19 @@ def canonicalize_nic_subtype(subtype): def canonicalize_scsi_subtype(subtype): """Try to convert the given SCSI controller string to a canonical form. - :param str subtype: User-provided string - :returns: The canonical string, one of: + Args: + subtype (str): User-provided string + Returns: + str: The canonical string, one of: - - ``buslogic`` - - ``lsilogic`` - - ``lsilogicsas`` - - ``virtio`` - - ``VirtualSCSI`` + - ``buslogic`` + - ``lsilogic`` + - ``lsilogicsas`` + - ``virtio`` + - ``VirtualSCSI`` - :raises ValueUnsupportedError: If the canonical string cannot be determined + Raises: + ValueUnsupportedError: If the canonical string cannot be determined """ return canonicalize_helper("SCSI controller subtype", subtype, [ @@ -216,10 +235,13 @@ def canonicalize_scsi_subtype(subtype): def check_for_conflict(label, li): """Make sure the list does not contain references to more than one object. - :param str label: Descriptive label to be used if an error is raised - :param list li: List of object references (which may include ``None``) - :raises ValueMismatchError: if references differ - :returns: the object or ``None`` + Args: + label (str): Descriptive label to be used if an error is raised + li (list): List of object references (which may include ``None``) + Raises: + ValueMismatchError: if references differ + Returns: + object: the object or ``None`` """ obj = None for i, obj1 in enumerate(li): @@ -239,9 +261,11 @@ def check_for_conflict(label, li): def file_checksum(path_or_obj, checksum_type): """Get the checksum of the given file. - :param str path_or_obj: File path to checksum OR an opened file object - :param str checksum_type: Supported values are 'md5' and 'sha1'. - :return: String containing hexadecimal file checksum + Args: + path_or_obj (str): File path to checksum OR an opened file object + checksum_type (str): Supported values are 'md5' and 'sha1'. + Returns: + str: Hexadecimal file checksum """ # pylint: disable=redefined-variable-type if checksum_type == 'md5': @@ -285,9 +309,12 @@ def mac_address(string): * xx-xx-xx-xx-xx-xx * xxxx.xxxx.xxxx - :param str string: String to validate - :raises InvalidInputError: if string is not a valid MAC address - :return: Validated string(with leading/trailing whitespace stripped) + Args: + string (str): String to validate + Raises: + InvalidInputError: if string is not a valid MAC address + Returns: + str: Validated string(with leading/trailing whitespace stripped) """ string = string.strip() if not (re.match(r"([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$", string) or @@ -304,9 +331,12 @@ def device_address(string): Validate string is an appropriately formed device address such as '1:0'. - :param str string: String to validate - :raises InvalidInputError: if string is not a well-formatted device address - :return: Validated string (with leading/trailing whitespace stripped) + Args: + string (str): String to validate + Raises: + InvalidInputError: if string is not a well-formatted device address + Returns: + str: Validated string (with leading/trailing whitespace stripped) """ string = string.strip() if not re.match(r"\d+:\d+$", string): @@ -318,9 +348,12 @@ def device_address(string): def no_whitespace(string): """Parser helper function for arguments not allowed to contain whitespace. - :param str string: String to validate - :raises InvalidInputError: if string contains internal whitespace - :return: Validated string (with leading/trailing whitespace stripped) + Args: + string (str): String to validate + Raises: + InvalidInputError: if string contains internal whitespace + Returns: + str: Validated string (with leading/trailing whitespace stripped) """ string = string.strip() if len(string.split()) > 1: @@ -334,14 +367,19 @@ def validate_int(string, label="input"): """Parser helper function for validating integer arguments in a range. - :param str string: String to convert to an integer and validate - :param int minimum: Minimum valid value (optional) - :param int maximum: Maximum valid value (optional) - :param str label: Label to include in any errors raised - :return: Validated integer value - :raises ValueUnsupportedError: if :attr:`string` can't be converted to int - :raises ValueTooLowError: if value is less than :attr:`minimum` - :raises ValueTooHighError: if value is more than :attr:`maximum` + Args: + string (str): String to convert to an integer and validate + minimum (int): Minimum valid value (optional) + maximum (int): Maximum valid value (optional) + label (str): Label to include in any errors raised + + Returns: + int: Validated integer value + + Raises: + ValueUnsupportedError: if :attr:`string` can't be converted to int + ValueTooLowError: if value is less than :attr:`minimum` + ValueTooHighError: if value is more than :attr:`maximum` """ try: i = int(string) @@ -359,8 +397,13 @@ def non_negative_int(string): Alias for :func:`validate_int` setting :attr:`minimum` to 0. - :param str string: String to validate. - :return: Validated integer value + Args: + string (str): String to validate. + Returns: + int: Validated integer value + Raises: + ValueUnsupportedError: if :attr:`string` can't be converted to int + ValueTooLowError: if value is less than 0 """ return validate_int(string, minimum=0) @@ -370,8 +413,13 @@ def positive_int(string): Alias for :func:`validate_int` setting :attr:`minimum` to 1. - :param str string: String to validate. - :return: Validated integer value + Args: + string (str): String to validate. + Returns: + int: Validated integer value + Raises: + ValueUnsupportedError: if :attr:`string` can't be converted to int + ValueTooLowError: if value is less than 1 """ return validate_int(string, minimum=1) @@ -381,9 +429,12 @@ def truth_value(value): Wrapper for :func:`distutils.util.strtobool` - :param str value: String to parse/validate - :return: True or False - :raises ValueUnsupportedError: if the value can't be parsed to a boolean. + Args: + value (str): String to parse/validate + Returns: + bool: True or False + Raises: + ValueUnsupportedError: if the value can't be parsed to a boolean. """ if isinstance(value, bool): return value @@ -414,10 +465,10 @@ class InvalidInputError(ValueError): class ValueUnsupportedError(InvalidInputError): """An unsupported value was provided. - :param str value_type: descriptive string - :param str actual_value: invalid value that was provided - :param expected_value: expected (valid) value or values (item or list) - :type expected_value: str, int, list + Args: + value_type (str): descriptive string + actual_value (str): invalid value that was provided + expected_value (object): expected/valid value(s) (item or list) """ def __init__(self, value_type, actual_value, expected_value): @@ -437,9 +488,10 @@ def __str__(self): class ValueTooLowError(ValueUnsupportedError): """A numerical input was less than the lowest supported value. - :param str value_type: descriptive string - :param int actual_value: invalid value that was provided - :param int expected_value: minimum supported value + Args: + value_type (str): descriptive string + actual_value (int): invalid value that was provided + expected_value (int): minimum supported value """ def __str__(self): @@ -452,9 +504,10 @@ def __str__(self): class ValueTooHighError(ValueUnsupportedError): """A numerical input was higher than the highest supported value. - :param str value_type: descriptive string - :param int actual_value: invalid value that was provided - :param int expected_value: maximum supported value + Args: + value_type (str): descriptive string + actual_value (int): invalid value that was provided + expected_value (int): maximum supported value """ def __str__(self): diff --git a/COT/deploy.py b/COT/deploy.py index c3394eb..3e65d9a 100644 --- a/COT/deploy.py +++ b/COT/deploy.py @@ -41,7 +41,8 @@ def from_cli_string(cls, cli_string): Based on the QEMU CLI for serial ports. - :param str cli_string: String of the form 'kind:value[,opts]' + Args: + cli_string (str): String of the form 'kind:value[,opts]' :: @@ -52,8 +53,10 @@ def from_cli_string(cls, cli_string): >>> str(SerialConnection.from_cli_string('telnet://1.1.1.1:1111')) '' - :return: SerialConnection instance or None. - :raises InvalidInputError: if ``cli_string`` cannot be parsed + Returns: + SerialConnection: Created instance or None. + Raises: + InvalidInputError: if ``cli_string`` cannot be parsed """ if cli_string is None: return None @@ -84,9 +87,12 @@ def from_cli_string(cls, cli_string): def validate_kind(cls, kind): """Validate the connection type string and munge it as needed. - :param str kind: Connection type string, possibly in need of munging. - :return: A valid type string - :raises ValueUnsupportedError: if ``kind`` is not recognized as valid + Args: + kind (str): Connection type string, possibly in need of munging. + Returns: + str: A valid type string + Raises: + ValueUnsupportedError: if ``kind`` is not recognized as valid """ kind = kind.lower() if kind == '': @@ -107,11 +113,14 @@ def validate_kind(cls, kind): def validate_value(cls, kind, value): """Check that the given value is valid for the given connection kind. - :param str kind: Connection type, valid per :func:`validate_kind`. - :param str value: Connection value such as '/dev/ttyS0' or '1.1.1.1:80' - :return: Munged value string. - :raises InvalidInputError: if value string is not recognized as valid - :raises NotImplementedError: if ``kind`` is not valid + Args: + kind (str): Connection type, valid per :func:`validate_kind`. + value (str): Connection value such as '/dev/ttyS0' or '1.1.1.1:80' + Returns: + str: Munged value string. + Raises: + InvalidInputError: if value string is not recognized as valid + NotImplementedError: if ``kind`` is not valid """ if kind == 'device' or kind == 'file' or kind == 'pipe': # TODO: Validate that device path exists on target? @@ -137,11 +146,14 @@ def validate_value(cls, kind, value): def validate_options(cls, kind, _value, options): """Check that the given set of options are valid for this connection. - :param str kind: Validated 'kind' string. - :param str _value: Validated 'value' string. Currently unused. - :param dict options: Input options dictionary. - :return: validated options dict - :raises InvalidInputError: if options are not valid. + Args: + kind (str): Validated 'kind' string. + _value (str): Validated 'value' string. Currently unused. + options (dict): Input options dictionary. + Returns: + dict: Validated options + Raises: + InvalidInputError: if options are not valid. """ if kind == 'file': if 'datastore' not in options: @@ -152,9 +164,10 @@ def validate_options(cls, kind, _value, options): def __init__(self, kind, value, options): """Construct a SerialConnection object of the given kind and value. - :param str kind: Connection type string, possibly in need of munging. - :param str value: Connection value such as '/dev/ttyS0' or '1.1.1.1:80' - :param dict options: Input options dictionary. + Args: + kind (str): Connection type string, possibly in need of munging. + value (str): Connection value such as '/dev/ttyS0' or '1.1.1.1:80' + options (dict): Input options dictionary. """ logger.debug("Creating SerialConnection: " "kind: %s, value: %s, options: %s", @@ -179,7 +192,7 @@ class COTDeploy(COTReadOnlySubmodule): to be common across all concrete subclasses. Inherited attributes: - :attr:`~COT.submodule.COTGenericSubmodule.UI`, + :attr:`~COT.submodule.COTGenericSubmodule.ui`, :attr:`~COT.submodule.COTReadOnlySubmodule.package`, Attributes: @@ -198,8 +211,8 @@ class COTDeploy(COTReadOnlySubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTDeploy, self).__init__(ui) # User inputs @@ -234,7 +247,8 @@ def __init__(self, ui): def hypervisor(self): """Hypervisor to deploy to. - :raises InvalidInputError: if not a recognized value. + Raises: + InvalidInputError: if not a recognized value. """ return self._hypervisor @@ -249,7 +263,8 @@ def hypervisor(self, value): def configuration(self): """VM configuration profile to use for deployment. - :raises InvalidInputError: if not a profile defined in the VM. + Raises: + InvalidInputError: if not a profile defined in the VM. """ return self._configuration @@ -313,7 +328,8 @@ def serial_connection(self, value): def ready_to_run(self): """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.hypervisor is None: return False, "HYPERVISOR is a mandatory argument" @@ -335,10 +351,10 @@ def run(self): self.configuration) else: header, profile_info_list = self.vm.profile_info_list( - self.UI.terminal_width - 1) + self.ui.terminal_width - 1) # Correct for the indentation of the list: header = "\n".join([" " + h for h in header.split("\n")]) - self.configuration = self.UI.choose_from_list( + self.configuration = self.ui.choose_from_list( header=header, option_list=profile_list, info_list=profile_info_list, @@ -353,7 +369,7 @@ def run(self): serial_count = self.vm.get_serial_count( [self.configuration])[self.configuration] if len(self.serial_connection) > serial_count: - self.UI.confirm_or_die( + self.ui.confirm_or_die( "{0} configuration '{1}' defines only {2} serial ports, " "but you have given connection information for {3} ports." "\nContinue to create additional ports?" @@ -373,16 +389,16 @@ def create_subparser(self): ``'cot deploy PACKAGE '`` so subclasses of this module should call ``super().create_subparser()`` (to create the main 'deploy' subparser if it doesn't already exist) then call - ``self.UI.add_parser(..., parent=self.subparsers, ...)`` to add + ``self.ui.add_parser(..., parent=self.subparsers, ...)`` to add their own sub-subparser. """ import argparse - if self.UI.subparser_lookup.get('deploy', None) is None: + if self.ui.subparser_lookup.get('deploy', None) is None: # Create 'cot deploy' parser - p = self.UI.add_subparser( + p = self.ui.add_subparser( 'deploy', - usage=self.UI.fill_usage("deploy", [ + usage=self.ui.fill_usage("deploy", [ "PACKAGE esxi ...", ]), help="Create a new VM on the target hypervisor from the " @@ -405,7 +421,7 @@ def create_subparser(self): self.subparsers = next( # pylint: disable=protected-access action for - action in self.UI.subparser_lookup['deploy']._actions if + action in self.ui.subparser_lookup['deploy']._actions if type(action).name == '_SubParsersAction') # Create a generic parser with arguments to be shared by all diff --git a/COT/deploy_esxi.py b/COT/deploy_esxi.py index 313f2f2..e392d1f 100644 --- a/COT/deploy_esxi.py +++ b/COT/deploy_esxi.py @@ -57,12 +57,12 @@ class SmarterConnection(SmartConnection): def __init__(self, ui, host, user, pwd, port=443): """Create a connection to the given server. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. For the other parameters, see :class:`pyVim.connect.SmartConnection` """ - self.UI = ui + self.ui = ui self.server = host self.username = user self.password = pwd @@ -77,8 +77,9 @@ def __enter__(self): validation failures and connect anyway. It also produces slightly more meaningful error messages on failure. - :raises vim.fault.HostConnectFault: - :raises requests.exceptions.ConnectionError: + Raises: + vim.fault.HostConnectFault: TODO + requests.exceptions.ConnectionError: TODO """ logger.verbose("Establishing connection to %s:%s...", self.server, self.port) @@ -89,7 +90,7 @@ def __enter__(self): raise # Self-signed certificates are pretty common for ESXi servers logger.warning(e.msg) - self.UI.confirm_or_die("SSL certificate for {0} is self-signed or " + self.ui.confirm_or_die("SSL certificate for {0} is self-signed or " "otherwise not recognized as valid. " "Accept certificate anyway?" .format(self.server)) @@ -111,7 +112,7 @@ def __exit__(self, # pylint: disable=arguments-differ exc_type, exc_value, trace): """Disconnect from the server. - For the parameters, see :module:`contextlib`. + For the parameters, see :mod:`contextlib`. """ super(SmarterConnection, self).__exit__() if exc_type is not None: @@ -125,8 +126,10 @@ def unwrap_connection_error(outer_e): ConnectionError often wraps another exception with more context; this function dives inside the ConnectionError to find that context. - :param ConnectionError outer_e: ConnectionError to unwrap - :return: extracted (errno, inner_message) + Args: + outer_e (ConnectionError): ConnectionError to unwrap + Returns: + tuple: extracted (errno, inner_message) """ errno = None inner_message = None @@ -156,11 +159,12 @@ def unwrap_connection_error(outer_e): def get_object_from_connection(conn, vimtype, name): """Look up an object by name. - :param conn: Connection to ESXi. - :type conn: :class:`SmarterConnection` - :param object vimtype: currently only ``vim.VirtualMachine`` - :param str name: Name of the object to look up. - :return: Located object + 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() @@ -179,9 +183,9 @@ class PyVmomiVMReconfigSpec(object): def __init__(self, conn, vm_name): """Use the given name to look up a VM using the given connection. - :param conn: Connection to ESXi. - :type conn: :class:`SmarterConnection` - :param str vm_name: Virtual machine name. + Args: + conn (SmarterConnection): Connection to ESXi. + vm_name (str): Virtual machine name. """ self.vm = get_object_from_connection(conn, vim.VirtualMachine, vm_name) if not self.vm: @@ -195,7 +199,7 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, trace): """If the block exited cleanly, apply the ConfigSpec to the VM. - For the parameters, see :module:`contextlib`. + For the parameters, see :mod:`contextlib`. """ # Did we exit cleanly? if exc_type is None: @@ -207,7 +211,7 @@ class COTDeployESXi(COTDeploy): """Submodule for deploying VMs on ESXi and VMware vCenter/vSphere. Inherited attributes: - :attr:`~COT.submodule.COTGenericSubmodule.UI`, + :attr:`~COT.submodule.COTGenericSubmodule.ui`, :attr:`~COT.submodule.COTReadOnlySubmodule.package`, :attr:`~COT.deploy.COTDeploy.generic_parser`, :attr:`~COT.deploy.COTDeploy.parser`, @@ -230,8 +234,8 @@ class COTDeployESXi(COTDeploy): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTDeployESXi, self).__init__(ui) self.datastore = None @@ -285,7 +289,8 @@ def serial_connection(self, value): def ready_to_run(self): """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.locator is None: return False, "LOCATOR is a mandatory argument" @@ -294,9 +299,11 @@ def ready_to_run(self): def fixup_ovftool_args(self, ovftool_args, target): """Make any needed modifications to the ovftool arguments. - :param list ovftool_args: Any existing ovftool arguments to begin with. - :param str target: deployment target URI - :return: Updated ovftool arguments + Args: + ovftool_args (list): Any existing ovftool arguments to begin with. + target (str): deployment target URI + Returns: + list: Updated ovftool arguments """ # pass selected configuration profile to ovftool if self.configuration is not None: @@ -333,7 +340,8 @@ def fixup_ovftool_args(self, ovftool_args, target): def run(self): """Do the actual work of this submodule - deploying to ESXi. - :raises InvalidInputError: if :func:`ready_to_run` reports ``False`` + Raises: + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTDeployESXi, self).run() @@ -341,7 +349,7 @@ def run(self): if self.username is None: self.username = getpass.getuser() if self.password is None: - self.password = self.UI.get_password(self.username, self.server) + self.password = self.ui.get_password(self.username, self.server) target = ("vi://" + self.username + ":" + self.password + "@" + self.locator) @@ -355,7 +363,7 @@ def run(self): # Otherwise we may need to help and/or warn the user: if vm.environment_properties and not re.search("/host/", self.locator): if self.ovftool.version < StrictVersion("4.0.0"): - self.UI.confirm_or_die( + self.ui.confirm_or_die( "When deploying an OVF directly to a vSphere target " "using ovftool prior to version 4.0.0, any OVF " "environment properties will not be made available " @@ -370,7 +378,7 @@ def run(self): "option. OVF environment properties will " "be ignored.") elif not self.power_on: - self.UI.confirm_or_die( + self.ui.confirm_or_die( "When deploying an OVF directly to a vSphere target, " "OVF environment properties can only be made available to " "the new guest if the guest is to be powered on " @@ -406,13 +414,14 @@ def run(self): def fixup_serial_ports(self): """Use PyVmomi to create and configure serial ports for the new VM. - :raises NotImplementedError: If any - :class:`~COT.deploy.SerialConnection` in :attr:`serial_connection` - has a :attr:`~COT.deploy.SerialConnection.kind` other than - 'tcp', 'telnet', or 'device' + Raises: + NotImplementedError: If any :class:`~COT.deploy.SerialConnection` + in :attr:`serial_connection` has a + :attr:`~COT.deploy.SerialConnection.kind` other than + 'tcp', 'telnet', or 'device' """ logger.info("Fixing up serial ports...") - with SmarterConnection(self.UI, self.server, + with SmarterConnection(self.ui, self.server, self.username, self.password) as conn: logger.verbose("Connection established") with PyVmomiVMReconfigSpec(conn, self.vm_name) as spec: @@ -427,8 +436,9 @@ def fixup_serial_ports(self): def _create_serial_port(s, spec): """Use PyVmomi to create a serial connection on a VM. - :param PyVmomiVMReconfigSpec spec: PyVmomi VM spec object - :param SerialConnection s: Serial connection to create + Args: + s (SerialConnection): Serial connection to create + spec (PyVmomiVMReconfigSpec): PyVmomi VM spec object """ logger.verbose(s) serial_spec = vim.vm.device.VirtualDeviceSpec() @@ -472,13 +482,13 @@ def create_subparser(self): import argparse # Create 'cot deploy ... esxi' parser - p = self.UI.add_subparser( + p = self.ui.add_subparser( 'esxi', aliases=['vcenter', 'vmware', 'vsphere'], parent=self.subparsers, lookup_prefix="deploy-", parents=[self.generic_parser], - usage=self.UI.fill_usage("deploy PACKAGE esxi", [ + usage=self.ui.fill_usage("deploy PACKAGE esxi", [ "LOCATOR [-u USERNAME] [-p PASSWORD] [-c CONFIGURATION] " "[-n VM_NAME] [-P] [-N OVF1=HOST1 [-N OVF2=HOST2 ...]] " "[-S KIND1:VAL1[,OPTS1] [-S KIND2:VAL2[,OPTS2] ...]] " @@ -487,7 +497,7 @@ def create_subparser(self): formatter_class=argparse.RawDescriptionHelpFormatter, help="Deploy to ESXi, vSphere, or vCenter", description="Deploy OVF/OVA to ESXi/vCenter/vSphere hypervisor", - epilog=self.UI.fill_examples([ + epilog=self.ui.fill_examples([ ("Deploy to vSphere/ESXi server 192.0.2.100 with credentials" " admin/admin, creating a VM named 'test_vm' from foo.ova.", 'cot deploy foo.ova esxi 192.0.2.100 -u admin -p admin' diff --git a/COT/edit_hardware.py b/COT/edit_hardware.py index e56e7a4..9d97740 100644 --- a/COT/edit_hardware.py +++ b/COT/edit_hardware.py @@ -55,7 +55,7 @@ class COTEditHardware(COTSubmodule): """Edit hardware information (CPUs, RAM, NICs, etc.). Inherited attributes: - :attr:`~COTGenericSubmodule.UI`, + :attr:`~COTGenericSubmodule.ui`, :attr:`~COTSubmodule.package`, :attr:`~COTSubmodule.output` @@ -80,8 +80,8 @@ class COTEditHardware(COTSubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTEditHardware, self).__init__(ui) self.profiles = None @@ -301,7 +301,8 @@ def ide_subtypes(self, value): def ready_to_run(self): """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ # Need some work to do! if not any([x is not None and x is not False for x in [ @@ -334,13 +335,13 @@ def _run_create_new_profiles(self): profile_list = self.vm.config_profiles for profile in self.profiles: # pylint: disable=not-an-iterable if profile not in profile_list: - self.UI.confirm_or_die( + self.ui.confirm_or_die( "Profile '{0}' does not exist. Create it?" .format(profile)) - label = self.UI.get_input( + label = self.ui.get_input( "Please enter a label for this configuration profile", profile) - desc = self.UI.get_input( + desc = self.ui.get_input( "Please enter a description for this " "configuration profile", label) self.vm.create_configuration_profile(profile, label=label, @@ -352,7 +353,7 @@ def _run_delete_other_profiles(self): Helper for :meth:`_run_update_profiles`. """ if self.profiles is None: - self.UI.confirm_or_die( + self.ui.confirm_or_die( "--delete-all-other-profiles was specified but no " "--profiles was specified. Really proceed to delete ALL " "configuration profiles?") @@ -362,7 +363,7 @@ def _run_delete_other_profiles(self): set(self.profiles)) for profile in profiles_to_delete: if self.profiles is not None: - if not self.UI.confirm("Delete profile {0}?".format(profile)): + if not self.ui.confirm("Delete profile {0}?".format(profile)): logger.verbose("Skipping deletion of profile %s", profile) continue # else (profiles == None) we already confirmed earlier @@ -373,13 +374,13 @@ def _run_update_profiles(self): if self.profiles is not None: # Warn user about non-profile-aware properties if self.virtual_system_type is not None: - self.UI.confirm_or_die( + self.ui.confirm_or_die( "VirtualSystemType is not filtered by configuration" " profile. Requested system type(s) '{0}' will be set for" " ALL profiles, not just profile(s) {1}. Continue?" .format(" ".join(self.virtual_system_type), self.profiles)) if self.network_descriptions is not None: - self.UI.confirm_or_die( + self.ui.confirm_or_die( "Network descriptions are not filtered by configuration" " profile. Requested network descriptions will be set for" " networks across ALL profiles, not just profile(s) {0}." @@ -450,7 +451,7 @@ def _run_update_nics(self): if self.nics is not None: for (profile, count) in nics_dict.items(): if self.nics < count: - self.UI.confirm_or_die( + self.ui.confirm_or_die( "Profile {0} currently has {1} NIC(s). " "Delete {2} NIC(s) to reduce to {3} total?" .format(profile, count, @@ -506,11 +507,11 @@ def _run_update_networks(self): new_desc = None if network not in existing_networks: - self.UI.confirm_or_die( + self.ui.confirm_or_die( "Network {0} is not currently defined. " "Create it?".format(network)) if not new_desc: - new_desc = self.UI.get_input( + new_desc = self.ui.get_input( "Please enter a description for this network", network) # create or update @@ -524,7 +525,7 @@ def _run_update_serial(self): serial_dict = self.vm.get_serial_count(self.profiles) for (profile, count) in serial_dict.items(): if self.serial_ports < count: - self.UI.confirm_or_die( + self.ui.confirm_or_die( "Profile {0} currently has {1} serial port(s). " "Delete {2} port(s) to reduce to {3} total?" .format(profile, count, (count - self.serial_ports), @@ -535,7 +536,7 @@ def _run_update_serial(self): serial_dict = self.vm.get_serial_count(self.profiles) for (profile, count) in serial_dict.items(): if len(self.serial_connectivity) < count: - self.UI.confirm_or_die( + self.ui.confirm_or_die( "There are {0} serial port(s) under profile {1}, but " "you have specified connectivity information for only " "{2}. " @@ -548,7 +549,8 @@ def _run_update_serial(self): def run(self): """Do the actual work of this submodule. - :raises InvalidInputError: if :func:`ready_to_run` reports ``False`` + Raises: + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTEditHardware, self).run() @@ -581,14 +583,14 @@ def run(self): def create_subparser(self): """Create 'edit-hardware' CLI subparser.""" - wrapper = textwrap.TextWrapper(width=self.UI.terminal_width - 1, + wrapper = textwrap.TextWrapper(width=self.ui.terminal_width - 1, initial_indent=' ', subsequent_indent=' ') - p = self.UI.add_subparser( + p = self.ui.add_subparser( 'edit-hardware', add_help=False, formatter_class=argparse.RawDescriptionHelpFormatter, - usage=self.UI.fill_usage("edit-hardware", [ + usage=self.ui.fill_usage("edit-hardware", [ "PACKAGE [-o OUTPUT] -v TYPE [TYPE2 ...]", "PACKAGE [-o OUTPUT] \ [-p PROFILE [PROFILE2 ...] [--delete-all-other-profiles]] [-c CPUS] \ @@ -606,7 +608,7 @@ def create_subparser(self): " strings. The syntax for the wildcard option is '{' followed" " by a number to start incrementing from, followed by '}'." " See examples below." - ) + "\n\n" + self.UI.fill_examples([ + ) + "\n\n" + self.ui.fill_examples([ ('Create a new profile named "1CPU-8GB" with 1 CPU and 8' ' gigabytes of RAM', 'cot edit-hardware csr1000v.ova --output csr1000v_custom.ova' @@ -734,10 +736,13 @@ def expand_list_wildcard(name_list, length, quiet=False): >>> expand_list_wildcard(["mgmt0", "eth{10}"], 4) ['mgmt0', 'eth10', 'eth11', 'eth12'] - :param list name_list: List of names to assign, or None - :param list length: Length to expand to - :param bool quiet: Silence usual log messages generated by this function. - :return: Expanded list, or empty list if ``name_list`` is None or empty. + Args: + name_list (list): List of names to assign, or None + length (list): Length to expand to + quiet (bool): Silence usual log messages generated by this function. + + Returns: + list: Expanded list, or empty list if ``name_list`` is None or empty. """ if not name_list: return [] @@ -779,8 +784,11 @@ def guess_list_wildcard(known_values): >>> guess_list_wildcard(['fake1', 'fake2', 'real4', 'real5']) ['fake1', 'fake2', 'real{4}'] - :param list known_values: Values to guess from - :return: Guessed wildcard list, or None if unable to guess + Args: + known_values (list): Values to guess from + + Returns: + list: Guessed wildcard list, or None if unable to guess """ logger.debug("Attempting to infer a pattern from %s", known_values) # Guess sequences ending with simple N, N+1, N+2 diff --git a/COT/edit_product.py b/COT/edit_product.py index 0832760..5f87694 100644 --- a/COT/edit_product.py +++ b/COT/edit_product.py @@ -35,7 +35,7 @@ class COTEditProduct(COTSubmodule): """Edit product, vendor, and version information strings. Inherited attributes: - :attr:`~COTGenericSubmodule.UI`, + :attr:`~COTGenericSubmodule.ui`, :attr:`~COTSubmodule.package`, :attr:`~COTSubmodule.output` @@ -53,8 +53,8 @@ class COTEditProduct(COTSubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTEditProduct, self).__init__(ui) self.product_class = None @@ -77,7 +77,8 @@ def __init__(self, ui): def ready_to_run(self): """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if not any([ self.product_class, @@ -96,7 +97,8 @@ def ready_to_run(self): def run(self): """Do the actual work of this submodule. - :raises InvalidInputError: if :func:`ready_to_run` reports ``False`` + Raises: + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTEditProduct, self).run() @@ -143,11 +145,11 @@ def run(self): def create_subparser(self): """Create 'edit-product' CLI subparser.""" - p = self.UI.add_subparser( + p = self.ui.add_subparser( 'edit-product', aliases=['set-product', 'set-version'], help="""Edit product info in an OVF""", - usage=self.UI.fill_usage("edit-product", [ + usage=self.ui.fill_usage("edit-product", [ "PACKAGE [-o OUTPUT] [-c PRODUCT_CLASS] \ [-p PRODUCT] [-n VENDOR] [-v SHORT_VERSION] [-V FULL_VERSION] \ [-u PRODUCT_URL ] [-r VENDOR_URL] [-l APPLICATION_URL]", diff --git a/COT/edit_properties.py b/COT/edit_properties.py index f600449..09a9dcc 100644 --- a/COT/edit_properties.py +++ b/COT/edit_properties.py @@ -42,7 +42,7 @@ class COTEditProperties(COTSubmodule): """Edit OVF environment XML properties. Inherited attributes: - :attr:`~COTGenericSubmodule.UI`, + :attr:`~COTGenericSubmodule.ui`, :attr:`~COTSubmodule.package`, :attr:`~COTSubmodule.output` @@ -56,8 +56,8 @@ class COTEditProperties(COTSubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTEditProperties, self).__init__(ui) self._config_file = None @@ -74,7 +74,8 @@ def __init__(self, ui): def config_file(self): """Path to plaintext file to read configuration lines from. - :raises InvalidInputError: if the file does not exist. + Raises: + InvalidInputError: if the file does not exist. """ return self._config_file @@ -137,7 +138,8 @@ def transports(self, value): def ready_to_run(self): """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.labels and not self.properties: return False, ("The --label option requires also specifying " @@ -160,7 +162,8 @@ def ready_to_run(self): def run(self): """Do the actual work of this submodule. - :raises InvalidInputError: if :func:`ready_to_run` reports ``False`` + Raises: + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTEditProperties, self).run() @@ -175,7 +178,7 @@ def run(self): desc = self.descriptions[i] if self.descriptions else None curr_value = self.vm.get_property_value(key) if curr_value is None: - self.UI.confirm_or_die( + self.ui.confirm_or_die( "Property '{0}' does not yet exist.\n" "Create it?".format(key)) self.vm.set_property_value( @@ -205,7 +208,7 @@ def edit_properties_interactive(self): key_list = [p['key'] for p in pa] string_list = ["""{0:25} "{1}" """.format(p['key'], p['label']) for p in pa] - user_input = self.UI.choose_from_list( + user_input = self.ui.choose_from_list( header="Please choose a property to edit:", option_list=key_list, info_list=string_list, @@ -234,7 +237,7 @@ def edit_properties_interactive(self): ]) while True: - new_value = self.UI.get_input(prompt, + new_value = self.ui.get_input(prompt, default_value=old_value) if new_value == old_value: logger.info("Value for property '%s' is unchanged", key) @@ -256,12 +259,12 @@ def edit_properties_interactive(self): def create_subparser(self): """Create 'edit-properties' CLI subparser.""" - p = self.UI.add_subparser( + p = self.ui.add_subparser( 'edit-properties', aliases=['set-properties', 'edit-environment', 'set-environment'], add_help=False, help="""Edit or create environment properties of an OVF""", - usage=self.UI.fill_usage("edit-properties", [ + usage=self.ui.fill_usage("edit-properties", [ "PACKAGE [-p KEY1=VALUE1 [-p KEY2=VALUE2 ...]] " "[-l LABEL1 [-l LABEL2 ...]] [-d DESC1 [-d DESC2 ...]] " "[-c CONFIG_FILE] [-u [USER_CONFIGURABLE]] " @@ -274,7 +277,7 @@ def create_subparser(self): keys and values as command-line arguments or may provide a config-file to read from. If neither --config-file, --properties, nor --transport are given, the program will run interactively.""", - epilog=self.UI.fill_examples([ + epilog=self.ui.fill_examples([ ("Add configuration from a text file and mark the resulting" " properties as non-user-configurable.", 'cot edit-properties input.ovf -c config.txt -u=0'), diff --git a/COT/file_reference.py b/COT/file_reference.py index ea20dd3..7d3ff81 100644 --- a/COT/file_reference.py +++ b/COT/file_reference.py @@ -32,10 +32,11 @@ class FileOnDisk(object): def __init__(self, file_path, filename=None): """Create a reference to a file on disk. - :param str file_path: File path or directory path - :param str filename: If specified, file_path is considered to be - a directory containing this filename. If not specified, the - final element in file_path is considered the filename. + Args: + file_path (str): File path or directory path + filename (str): If specified, file_path is considered to be + a directory containing this filename. If not specified, the + final element in file_path is considered the filename. :: @@ -44,7 +45,8 @@ def __init__(self, file_path, filename=None): >>> a == b True - :raises IOError: if no such file exists + Raises: + IOError: if no such file exists """ if filename is None: self.file_path = file_path @@ -61,16 +63,20 @@ def __eq__(self, other): No attempt is made to check file equivalence, symlinks, etc. - :param object other: Other object to compare against - :return: True if the paths are the same, else False + Args: + other (object): Other object to compare against + Returns: + bool: True if the paths are the same, else False """ return type(other) is type(self) and self.file_path == other.file_path def __ne__(self, other): """FileOnDisk instances are not equal if they have different paths. - :param object other: Other object to compare against - :return: False if the paths are the same, else True + Args: + other (object): Other object to compare against + Returns: + bool: False if the paths are the same, else True """ return not self.__eq__(other) @@ -87,8 +93,10 @@ def size(self): def open(self, mode): """Open the file and return a reference to the file object. - :param str mode: Mode such as 'r', 'w', 'a', 'w+', etc. - :return: File object + Args: + mode (str): Mode such as 'r', 'w', 'a', 'w+', etc. + Returns: + file: File object """ self.obj = open(self.file_path, mode) return self.obj @@ -100,7 +108,8 @@ def close(self): def copy_to(self, dest_dir): """Copy this file to the given destination directory. - :param str dest_dir: Destination directory or filename. + Args: + dest_dir (str): Destination directory or filename. """ if self.file_path == os.path.join(dest_dir, self.filename): return @@ -110,8 +119,8 @@ def copy_to(self, dest_dir): def add_to_archive(self, tarf): """Copy this file into the given tarfile object. - :param tarf: Add this file to that archive. - :type tarf: :class:`tarfile.TarFile` + Args: + tarf (tarfile.TarFile): Add this file to that archive. """ logger.info("Adding %s to TAR file as %s", self.file_path, self.filename) @@ -124,10 +133,13 @@ class FileInTAR(object): def __init__(self, tarfile_path, filename): """Create a reference to a file contained in a TAR archive. - :param str tarfile_path: Path to TAR archive to read - :param str filename: File name in the TAR archive. - :raises IOError: if ``tarfile_path`` doesn't reference a TAR file, - or the TAR file does not contain ``filename``. + Args: + tarfile_path (str): Path to TAR archive to read + filename (str): File name in the TAR archive. + + Raises: + IOError: if ``tarfile_path`` doesn't reference a TAR file, + or the TAR file does not contain ``filename``. """ if not tarfile.is_tarfile(tarfile_path): raise IOError("{0} is not a valid TAR file.".format(tarfile_path)) @@ -145,8 +157,10 @@ def __eq__(self, other): No attempt is made to check file equivalence, symlinks, etc. - :param object other: Other object to compare against - :return: True if the filename and tarfile_path are the same, else False + Args: + other (object): Other object to compare against + Returns: + bool: True if filename and tarfile_path are the same, else False """ if type(other) is type(self): return (self.tarfile_path == other.tarfile_path and @@ -156,8 +170,10 @@ def __eq__(self, other): def __ne__(self, other): """FileInTar are not equal if they have different paths or names. - :param object other: Other object to compare against - :return: False if the filename and tarfile_path are the same, else True + Args: + other (object): Other object to compare against + Returns: + bool: False if filename and tarfile_path are the same, else True """ return not self.__eq__(other) @@ -180,9 +196,12 @@ def size(self): def open(self, mode): """Open the TAR and return a reference to the relevant file object. - :param str mode: Only 'r' and 'rb' modes are supported. - :return: File object - :raises ValueError: if ``mode`` is not valid. + Args: + mode (str): Only 'r' and 'rb' modes are supported. + Returns: + file: File object + Raises: + ValueError: if ``mode`` is not valid. """ # We can only extract a file object from a TAR file in read mode. if mode != 'r' and mode != 'rb': @@ -203,7 +222,8 @@ def close(self): def copy_to(self, dest_dir): """Extract this file to the given destination directory. - :param str dest_dir: Destination directory or filename. + Args: + dest_dir (str): Destination directory or filename. """ with closing(tarfile.open(self.tarfile_path, 'r')) as tarf: logger.info("Extracting %s from %s to %s", @@ -213,8 +233,8 @@ def copy_to(self, dest_dir): def add_to_archive(self, tarf): """Copy this file into the given tarfile object. - :param tarf: Add this file to that archive. - :type tarf: :class:`tarfile.TarFile` + Args: + tarf (tarfile.TarFile): Add this file to that archive. """ self.open('r') try: diff --git a/COT/help.py b/COT/help.py index 63af7e4..6c41bd7 100644 --- a/COT/help.py +++ b/COT/help.py @@ -28,7 +28,7 @@ class COTHelp(COTGenericSubmodule): """Provide 'help ' syntax. Inherited attributes: - :attr:`~COTGenericSubmodule.UI` + :attr:`~COTGenericSubmodule.ui` Attributes: :attr:`subcommand` @@ -37,8 +37,8 @@ class COTHelp(COTGenericSubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTHelp, self).__init__(ui) self._subcommand = None @@ -53,7 +53,7 @@ def subcommand(self): @subcommand.setter def subcommand(self, value): - valid_cmds = sorted(self.UI.subparser_lookup.keys()) + valid_cmds = sorted(self.ui.subparser_lookup.keys()) if value is not None and value not in valid_cmds: raise InvalidInputError("Invalid command '{0}' (choose from '{1}')" .format(value, "', '".join(valid_cmds))) @@ -64,14 +64,14 @@ def run(self): super(COTHelp, self).run() if self.subcommand: - subp = self.UI.subparser_lookup[self.subcommand] + subp = self.ui.subparser_lookup[self.subcommand] subp.print_help() else: - self.UI.parser.print_help() + self.ui.parser.print_help() def create_subparser(self): """Create 'help' CLI subparser.""" - p = self.UI.add_subparser( + p = self.ui.add_subparser( 'help', help="""Print help for a command""", usage=""" diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 1756dd4..3aefaa8 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -208,7 +208,7 @@ def __bool__(self): _provider_package = {} """Mapping of package manager name to package name to install with it.""" - UI = None + USER_INTERFACE = None """User interface (if any) available to helpers.""" @property @@ -282,7 +282,7 @@ def call(self, args, installed, and the user declines to install it at this time. """ if not self.path: - if self.UI and not self.UI.confirm( + if self.USER_INTERFACE and not self.USER_INTERFACE.confirm( "{0} does not appear to be installed.\nTry to install it?" .format(self.name)): raise HelperNotFoundError( diff --git a/COT/helpers/tests/test_helper.py b/COT/helpers/tests/test_helper.py index ed14b02..5b7f9ac 100644 --- a/COT/helpers/tests/test_helper.py +++ b/COT/helpers/tests/test_helper.py @@ -330,14 +330,14 @@ def test_call_install(self): def test_call_no_install(self): """If not installed, and user declines, raise HelperNotFoundError.""" - _ui = Helper.UI - Helper.UI = UI() - Helper.UI.default_confirm_response = False + _ui = Helper.USER_INTERFACE + Helper.USER_INTERFACE = UI() + Helper.USER_INTERFACE.default_confirm_response = False try: self.assertRaises(HelperNotFoundError, self.helper.call, ["Hello!"]) finally: - Helper.UI = _ui + Helper.USER_INTERFACE = _ui def test_download_and_expand_tgz(self): """Validate the download_and_expand_tgz() context_manager.""" diff --git a/COT/info.py b/COT/info.py index 078630b..3ae3921 100644 --- a/COT/info.py +++ b/COT/info.py @@ -32,7 +32,7 @@ class COTInfo(COTGenericSubmodule): """Display VM information string. Inherited attributes: - :attr:`~COTGenericSubmodule.UI` + :attr:`~COTGenericSubmodule.ui` Attributes: :attr:`package_list`, @@ -42,8 +42,8 @@ class COTInfo(COTGenericSubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTInfo, self).__init__(ui) self._package_list = None @@ -77,7 +77,8 @@ def verbosity(self, value): def ready_to_run(self): """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if not self.package_list: return False, "At least one package must be specified" @@ -86,7 +87,8 @@ def ready_to_run(self): def run(self): """Do the actual work of this submodule. - :raises InvalidInputError: if :func:`ready_to_run` reports ``False`` + Raises: + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTInfo, self).run() @@ -97,13 +99,13 @@ def run(self): if not first: print("") with VMContextManager(package, None) as vm: - print(vm.info_string(self.UI.terminal_width - 1, + print(vm.info_string(self.ui.terminal_width - 1, self.verbosity)) first = False def create_subparser(self): """Create 'info' CLI subparser.""" - p = self.UI.add_subparser( + p = self.ui.add_subparser( 'info', aliases=['describe'], help="""Generate a description of an OVF package""", diff --git a/COT/inject_config.py b/COT/inject_config.py index 59db7f9..9872d54 100644 --- a/COT/inject_config.py +++ b/COT/inject_config.py @@ -32,7 +32,7 @@ class COTInjectConfig(COTSubmodule): """Wrap configuration file(s) into a disk image embedded into the VM. Inherited attributes: - :attr:`~COTGenericSubmodule.UI`, + :attr:`~COTGenericSubmodule.ui`, :attr:`~COTSubmodule.package`, :attr:`~COTSubmodule.output` @@ -45,8 +45,8 @@ class COTInjectConfig(COTSubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTInjectConfig, self).__init__(ui) self._config_file = None @@ -57,9 +57,10 @@ def __init__(self, ui): def config_file(self): """Primary configuration file. - :raises InvalidInputError: if the file does not exist - :raises InvalidInputError: if the `platform described by - :attr:`package` doesn't support configuration files. + Raises: + InvalidInputError: if the file does not exist + InvalidInputError: if the `platform described by + :attr:`package` doesn't support configuration files. """ return self._config_file @@ -80,9 +81,10 @@ def config_file(self, value): def secondary_config_file(self): """Secondary configuration file. - :raises InvalidInputError: if the file does not exist - :raises InvalidInputError: if the platform described by - :attr:`package` doesn't support secondary configuration files. + Raises: + InvalidInputError: if the file does not exist + InvalidInputError: if the platform described by + :attr:`package` doesn't support secondary configuration files. """ return self._secondary_config_file @@ -103,7 +105,8 @@ def secondary_config_file(self, value): def extra_files(self): """Additional files to be embedded as-is. - :raise InvalidInputError: if any file in the list does not exist + Raises: + InvalidInputError: if any file in the list does not exist """ return self._extra_files @@ -117,7 +120,8 @@ def extra_files(self, values): def ready_to_run(self): """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if not (self.config_file or self.secondary_config_file or @@ -128,14 +132,15 @@ def ready_to_run(self): def run(self): """Do the actual work of this submodule. - :raises InvalidInputError: if :func:`ready_to_run` reports ``False`` - :raises ValueUnsupportedError: if the - :const:`~COT.platforms.GenericPlatform.BOOTSTRAP_DISK_TYPE` of - the associated VM's - :attr:`~COT.vm_description.VMDescription.platform` is not - 'cdrom' or 'harddisk' - :raises LookupError: if unable to find a disk drive device to inject - the configuration into. + Raises: + InvalidInputError: if :func:`ready_to_run` reports ``False`` + ValueUnsupportedError: if the + :const:`~COT.platforms.GenericPlatform.BOOTSTRAP_DISK_TYPE` of + the associated VM's + :attr:`~COT.vm_description.VMDescription.platform` is not + 'cdrom' or 'harddisk' + LookupError: if unable to find a disk drive device to inject + the configuration into. """ super(COTInjectConfig, self).run() @@ -155,7 +160,7 @@ def run(self): "'cdrom' or 'harddisk'") if f is not None: file_id = vm.get_id_from_file(f) - self.UI.confirm_or_die( + self.ui.confirm_or_die( "Existing configuration disk '{0}' found.\n" "Continue and overwrite it?".format(file_id)) logger.warning("Overwriting existing config disk '%s'", file_id) @@ -204,7 +209,7 @@ def run(self): # Inject the disk image into the OVA, using "add-disk" functionality add_disk_worker( - ui=self.UI, + ui=self.ui, vm=vm, disk_image=disk_image, drive_type=platform.BOOTSTRAP_DISK_TYPE, @@ -218,11 +223,11 @@ def run(self): def create_subparser(self): """Create 'inject-config' CLI subparser.""" - p = self.UI.add_subparser( + p = self.ui.add_subparser( 'inject-config', aliases=['add-bootstrap'], help="Inject a configuration file into an OVF package", - usage=self.UI.fill_usage("inject-config", [ + usage=self.ui.fill_usage("inject-config", [ "PACKAGE [-o OUTPUT] [-c CONFIG_FILE] " "[-s SECONDARY_CONFIG_FILE] [-e EXTRA_FILE [EXTRA_FILE2 ...]]", ]), diff --git a/COT/install_helpers.py b/COT/install_helpers.py index c233986..3a6be8d 100644 --- a/COT/install_helpers.py +++ b/COT/install_helpers.py @@ -52,8 +52,10 @@ def guess_manpath(): def verify_manpages(man_dir): """Verify installation of COT's manual pages. - :param str man_dir: Base directory where manpages should be found. - :return: (result, message) + Args: + man_dir (str): Base directory where manpages should be found. + Returns: + tuple: (result, message) """ for f in resource_listdir("COT", "docs/man"): src_path = resource_filename("COT", os.path.join("docs/man", f)) @@ -79,11 +81,14 @@ def verify_manpages(man_dir): def _install_manpage(src_path, man_dir): """Install the given manual page for COT. - :param str src_path: Path to manual page file. - :param str man_dir: Base directory where page should be installed. - :return: (page_previously_installed, page_updated) - :raises IOError: if installation fails under some circumstances - :raises OSError: if installation fails under other circumstances + Args: + src_path (str): Path to manual page file. + man_dir (str): Base directory where page should be installed. + Returns: + tuple: (page_previously_installed, page_updated) + Raises: + IOError: if installation fails under some circumstances + OSError: if installation fails under other circumstances """ # Which man section does this belong in? f = os.path.basename(src_path) @@ -106,8 +111,10 @@ def _install_manpage(src_path, man_dir): def install_manpages(man_dir): """Install COT's manual pages. - :param str man_dir: Base directory where manpages should be installed. - :return: (result, message) + Args: + man_dir (str): Base directory where manpages should be installed. + Returns: + tuple: (result, message) """ installed_any = False some_preinstalled = False @@ -136,7 +143,7 @@ class COTInstallHelpers(COTGenericSubmodule): """Install all helper tools that COT requires. Inherited attributes: - :attr:`~COTGenericSubmodule.UI`, + :attr:`~COTGenericSubmodule.ui`, :attr:`~COTSubmodule.package`, :attr:`~COTSubmodule.output` """ @@ -144,8 +151,8 @@ class COTInstallHelpers(COTGenericSubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTInstallHelpers, self).__init__(ui) self.ignore_errors = False @@ -154,9 +161,11 @@ def __init__(self, ui): def install_helper(self, helper): """Install the given helper module. - :param helper: Helper module to install. - :type helper: :class:`~COT.helpers.helper.Helper` - :return: (result, message) + Args: + helper (Helper): Helper module to install. + + Returns: + tuple: (result, message) """ if helper.installed: return (True, @@ -178,7 +187,8 @@ def install_helper(self, helper): def manpages_helper(self): """Verify or install COT's manual pages. - :return: (result, message) + Returns: + tuple: (result, message) """ try: resource_listdir("COT", "docs/man") @@ -216,7 +226,7 @@ def run(self): print("Results:") print("-------------") - wrapper = textwrap.TextWrapper(width=self.UI.terminal_width, + wrapper = textwrap.TextWrapper(width=self.ui.terminal_width, initial_indent="", subsequent_indent=(" " * 14)) for k in sorted(results): @@ -227,11 +237,11 @@ def run(self): def create_subparser(self): """Create 'install-helpers' CLI subparser.""" - p = self.UI.add_subparser( + p = self.ui.add_subparser( 'install-helpers', help=("Install/verify COT manual pages and any third-party helper " "programs that COT may require"), - usage=self.UI.fill_usage('install-helpers', + usage=self.ui.fill_usage('install-helpers', ["--verify-only", "[--ignore-errors]"]), description=""" @@ -243,7 +253,7 @@ def create_subparser(self): * ovftool (https://www.vmware.com/support/developer/ovf/) * fatdisk (http://github.com/goblinhack/fatdisk) * vmdktool (http://www.freshports.org/sysutils/vmdktool/)""", - epilog=self.UI.fill_examples([ + epilog=self.ui.fill_examples([ ("Verify whether COT can find all expected helper programs", """ > cot install-helpers --verify-only diff --git a/COT/remove_file.py b/COT/remove_file.py index 0ef791f..766cf13 100644 --- a/COT/remove_file.py +++ b/COT/remove_file.py @@ -34,7 +34,7 @@ class COTRemoveFile(COTSubmodule): Inherited attributes: - :attr:`~COTGenericSubmodule.UI`, + :attr:`~COTGenericSubmodule.ui`, :attr:`~COTSubmodule.package`, :attr:`~COTSubmodule.output` @@ -46,8 +46,8 @@ class COTRemoveFile(COTSubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTRemoveFile, self).__init__(ui) self.file_path = None @@ -58,7 +58,8 @@ def __init__(self, ui): def ready_to_run(self): """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.file_path is None and self.file_id is None: return False, "No file information provided!" @@ -67,7 +68,8 @@ def ready_to_run(self): def run(self): """Do the actual work of this submodule. - :raises InvalidInputError: if :func:`ready_to_run` reports ``False`` + Raises: + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTRemoveFile, self).run() @@ -107,18 +109,18 @@ def run(self): prompt_info += " and device '{0}'".format( vm.device_info_str(disk_drive)) - self.UI.confirm_or_die("Remove {0}?".format(prompt_info)) + self.ui.confirm_or_die("Remove {0}?".format(prompt_info)) vm.remove_file(file_obj, disk=disk, disk_drive=disk_drive) def create_subparser(self): """Create 'remove-file' CLI subparser.""" - p = self.UI.add_subparser( + p = self.ui.add_subparser( 'remove-file', aliases=['delete-file'], add_help=False, - usage=self.UI.fill_usage("remove-file", [ + usage=self.ui.fill_usage("remove-file", [ "[-f FILE_PATH] [-i FILE_ID] PACKAGE [-o OUTPUT]", ]), help="Remove a file from an OVF package", diff --git a/COT/submodule.py b/COT/submodule.py index d30fb69..83a2c00 100644 --- a/COT/submodule.py +++ b/COT/submodule.py @@ -40,7 +40,7 @@ class COTGenericSubmodule(object): Attributes: :attr:`vm`, - :attr:`UI` + :attr:`ui` .. note :: Generally a command should not inherit directly from this class, but should instead subclass :class:`COTReadOnlySubmodule` or @@ -50,25 +50,27 @@ class COTGenericSubmodule(object): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ self.vm = None """Virtual machine description (:class:`VMDescription`).""" - self.UI = ui + self.ui = ui """User interface instance (:class:`~ui_shared.UI` or subclass).""" def ready_to_run(self): # pylint: disable=no-self-use """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ return True, "Ready to go!" def run(self): """Do the actual work of this submodule. - :raises InvalidInputError: if :meth:`ready_to_run` reports ``False`` + Raises: + InvalidInputError: if :meth:`ready_to_run` reports ``False`` """ (ready, reason) = self.ready_to_run() if not ready: @@ -99,7 +101,7 @@ class COTReadOnlySubmodule(COTGenericSubmodule): Inherited attributes: :attr:`vm`, - :attr:`UI` + :attr:`ui` Attributes: :attr:`package` @@ -108,8 +110,8 @@ class COTReadOnlySubmodule(COTGenericSubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTReadOnlySubmodule, self).__init__(ui) self._package = None @@ -121,7 +123,8 @@ def package(self): Calls :meth:`COT.vm_factory.VMFactory.create` to instantiate :attr:`self.vm` from the provided file. - :raises InvalidInputError: if the file does not exist. + Raises: + InvalidInputError: if the file does not exist. """ return self._package @@ -140,7 +143,8 @@ def package(self, value): def ready_to_run(self): """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.package is None: return False, "PACKAGE is a mandatory argument!" @@ -152,7 +156,7 @@ class COTSubmodule(COTGenericSubmodule): Inherited attributes: :attr:`vm`, - :attr:`UI` + :attr:`ui` Attributes: :attr:`package`, @@ -162,8 +166,8 @@ class COTSubmodule(COTGenericSubmodule): def __init__(self, ui): """Instantiate this submodule with the given UI. - :param ui: User interface instance. - :type ui: :class:`~COT.ui_shared.UI` + Args: + ui (UI): User interface instance. """ super(COTSubmodule, self).__init__(ui) self._package = None @@ -177,7 +181,8 @@ def package(self): Calls :meth:`COT.vm_factory.VMFactory.create` to instantiate :attr:`self.vm` from the provided file. - :raises InvalidInputError: if the file does not exist. + Raises: + InvalidInputError: if the file does not exist. """ return self._package @@ -206,7 +211,7 @@ def output(self): @output.setter def output(self, value): if value and value != self._output and os.path.exists(value): - self.UI.confirm_or_die("Overwrite existing file {0}?" + self.ui.confirm_or_die("Overwrite existing file {0}?" .format(value)) self._output = value if self.vm is not None: @@ -215,7 +220,8 @@ def output(self, value): def ready_to_run(self): """Check whether the module is ready to :meth:`run`. - :returns: ``(True, ready_message)`` or ``(False, reason_why_not)`` + Returns: + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.package is None: return False, "PACKAGE is a mandatory argument!" @@ -227,7 +233,8 @@ def run(self): If :attr:`output` was not previously set, automatically sets it to the value of :attr:`PACKAGE`. - :raises InvalidInputError: if :meth:`ready_to_run` reports ``False`` + Raises: + InvalidInputError: if :meth:`ready_to_run` reports ``False`` """ super(COTSubmodule, self).run() diff --git a/COT/tests/test_edit_properties.py b/COT/tests/test_edit_properties.py index fb25571..389359d 100644 --- a/COT/tests/test_edit_properties.py +++ b/COT/tests/test_edit_properties.py @@ -524,16 +524,16 @@ def custom_input(prompt, self.counter += 1 return canned_input - _input = self.instance.UI.get_input + _input = self.instance.ui.get_input try: - self.instance.UI.get_input = custom_input + self.instance.ui.get_input = custom_input self.instance.package = self.input_ovf self.instance.run() log = expected[self.counter - 1][msgs_idx] if log is not None: self.assertLogged(**log) # pylint: disable=not-a-mapping finally: - self.instance.UI.get_input = _input + self.instance.ui.get_input = _input self.instance.finished() self.check_diff(""" 1. Bootstrap Properties diff --git a/COT/tests/test_info.py b/COT/tests/test_info.py index 62789db..d1f0672 100644 --- a/COT/tests/test_info.py +++ b/COT/tests/test_info.py @@ -699,7 +699,7 @@ def test_invalid_ovf(self): def test_wrapping(self): """Test info string on a narrower-than-usual terminal.""" # pylint: disable=protected-access - self.instance.UI._terminal_width = 60 + self.instance.ui._terminal_width = 60 self.instance.package_list = [self.invalid_ovf] self.check_cot_output(""" diff --git a/COT/ui_shared.py b/COT/ui_shared.py index e11d32a..74b78cd 100644 --- a/COT/ui_shared.py +++ b/COT/ui_shared.py @@ -36,7 +36,8 @@ class UI(object): def __init__(self, force=False): """Constructor. - :param bool force: See :attr:`force`. + Args: + force (bool): See :attr:`force`. """ self.force = force """Whether to automatically select the default value in all cases. @@ -47,7 +48,7 @@ def __init__(self, force=False): """Knob for API testing, sets the default response to confirm().""" self._terminal_width = 80 from COT.helpers import Helper - Helper.UI = self + Helper.USER_INTERFACE = self @property def terminal_width(self): @@ -58,10 +59,12 @@ def fill_usage(self, # pylint: disable=no-self-use subcommand, usage_list): """Pretty-print a list of usage strings. - :param str subcommand: Subcommand name/keyword - :param list usage_list: List of usage strings for this subcommand. - :returns: String containing all usage strings, each appropriately - wrapped to the :attr:`terminal_width` value. + Args: + subcommand (str): Subcommand name/keyword + usage_list (list): List of usage strings for this subcommand. + Returns: + str: Concatenation of all usage strings, each appropriately + wrapped to the :attr:`terminal_width` value. """ return "\n".join(["{0} {1}".format(subcommand, usage) for usage in usage_list]) @@ -69,8 +72,10 @@ def fill_usage(self, # pylint: disable=no-self-use def fill_examples(self, example_list): """Pretty-print a set of usage examples. - :param list example_list: List of (example, description) tuples. - :raises NotImplementedError: Must be implemented by a subclass. + Args: + example_list (list): List of (example, description) tuples. + Raises: + NotImplementedError: Must be implemented by a subclass. """ raise NotImplementedError("No implementation for fill_examples()") @@ -84,9 +89,11 @@ def confirm(self, prompt): but instead returns :attr:`default_confirm_response`. Subclasses should override this method. - :param str prompt: Message to prompt the user with - :return: ``True`` (user confirms acceptance) or ``False`` - (user declines) + Args: + prompt (str): Message to prompt the user with + Returns: + bool: ``True`` (user confirms acceptance) or ``False`` + (user declines) """ if self.force: logger.warning("Automatically agreeing to '%s'", prompt) @@ -99,7 +106,10 @@ def confirm_or_die(self, prompt): A simple wrapper for :meth:`confirm` that calls :func:`sys.exit` if :meth:`confirm` returns ``False``. - :param str prompt: Message to prompt the user with + Args: + prompt (str): Message to prompt the user with + Raises: + SystemExit: if user declines """ if not self.confirm(prompt): sys.exit("Aborting.") @@ -108,13 +118,16 @@ def choose_from_list(self, footer, option_list, default_value, header="", info_list=None): """Prompt the user to choose from a list. - :param str footer: Prompt string to display following the list - :param list option_list: List of strings to choose amongst - :param str default_value: Default value to select if user declines - :param str header: String to display prior to the list - :param list info_list: Verbose strings to display in place of - :attr:`option_list` - :return: :attr:`default_value` or an item from :attr:`option_list`. + Args: + footer (str): Prompt string to display following the list + option_list (list): List of strings to choose amongst + default_value (str): Default value to select if user declines + header (str): String to display prior to the list + info_list (list): Verbose strings to display in place of + :attr:`option_list` + + Returns: + str: :attr:`default_value` or an item from :attr:`option_list`. """ if not info_list: info_list = option_list @@ -151,12 +164,13 @@ def get_input(self, prompt, default_value): but instead always returns :attr:`default_value`. Subclasses should override this method. - :param str prompt: Message to prompt the user with - :param str default_value: Default value to input if the user simply - hits Enter without entering a value, or if :attr:`force`. + Args: + prompt (str): Message to prompt the user with + default_value (str): Default value to input if the user simply + hits Enter without entering a value, or if :attr:`force`. - :return: Input value - :rtype: str + Returns: + str: Input value """ if self.force: logger.warning("Automatically entering %s in response to '%s'", @@ -167,8 +181,10 @@ def get_input(self, prompt, default_value): def get_password(self, username, host): """Get password string from the user. - :param str username: Username the password is associated with - :param str host: Host the password is associated with - :raises NotImplementedError: Must be implemented by a subclass. + Args: + username (str): Username the password is associated with + host (str): Host the password is associated with + Raises: + NotImplementedError: Must be implemented by a subclass. """ raise NotImplementedError("No implementation of get_password()") diff --git a/COT/vm_context_manager.py b/COT/vm_context_manager.py index 6ec6ae1..b5cb941 100644 --- a/COT/vm_context_manager.py +++ b/COT/vm_context_manager.py @@ -55,7 +55,7 @@ def __exit__(self, exc_type, exc_value, trace): """If the block exited cleanly, write the VM out to disk. In any case, destroy the VM. - For the parameters, see :module:`contextlib`. + For the parameters, see :mod:`contextlib`. """ # Did we exit cleanly? try: diff --git a/COT/vm_description.py b/COT/vm_description.py index 0ad31d9..aadf883 100644 --- a/COT/vm_description.py +++ b/COT/vm_description.py @@ -76,10 +76,13 @@ def detect_type_from_name(cls, filename): Does not check file contents, as the given filename may not yet exist. - :param str filename: File name or path - :return: A string representing a recognized and supported type of file - :raises ValueUnsupportedError: if COT can't recognize the file type or - doesn't know how to handle this file type. + Args: + filename (str): File name or path + Returns: + str: A string representing a recognized and supported type of file + Raises: + ValueUnsupportedError: if COT can't recognize the file type or + doesn't know how to handle this file type. """ raise ValueUnsupportedError("filename", filename, ("none implemented")) @@ -88,11 +91,14 @@ def __init__(self, input_file, output_file=None): Also creates a temporary directory as a working directory. - :param str input_file: Data file to read in. - :param str output_file: File name to write to. If this VM is read-only, - (there will never be an output file) this value should be ``None``; - if the output filename is not yet known, use ``""`` and subsequently - set :attr:`output` when it is determined. + Args: + input_file (str): Data file to read in. + output_file (str): File name to write to. + + * If this VM is read-only, (there will never be an output file) + this value should be ``None`` + * If the output filename is not yet known, use ``""`` and + subsequently set :attr:`output` when it is determined. """ self._input_file = input_file self._product_class = None @@ -160,7 +166,8 @@ def platform(self): def validate_hardware(self): """Check sanity of hardware properties for this VM/product/platform. - :return: ``True`` if hardware is sane, ``False`` if not. + Returns: + bool: ``True`` if hardware is sane, ``False`` if not. """ raise NotImplementedError("validate_hardware not implemented!") @@ -177,7 +184,8 @@ def config_profiles(self): def default_config_profile(self): """The name of the default configuration profile. - :return: Profile name or ``None`` if none are defined. + Returns: + str: Profile name or ``None`` if none are defined. """ if self.config_profiles: return self.config_profiles[0] @@ -187,17 +195,16 @@ def default_config_profile(self): def environment_properties(self): """The array of environment properties. - :return: Array of dicts (one per property) with the keys - ``"key"``, ``"value"``, ``"qualifiers"``, ``"type"``, - ``"label"``, and ``"description"``. + Returns: + list: Array of dicts (one per property) with the keys + ``"key"``, ``"value"``, ``"qualifiers"``, ``"type"``, + ``"label"``, and ``"description"``. """ raise NotImplementedError("environment_properties not implemented") @property def environment_transports(self): """The list of environment transport methods. - - :rtype: list[str] """ raise NotImplementedError("environment_transports not implemented") @@ -208,16 +215,12 @@ def environment_transports(self, value): @property def networks(self): """The list of network names currently defined in this VM. - - :rtype: list[str] """ raise NotImplementedError("networks property not implemented!") @property def network_descriptions(self): """The list of network descriptions currently defined in this VM. - - :rtype: list[str] """ raise NotImplementedError( "network_descriptions property not implemented!") @@ -255,15 +258,16 @@ def convert_disk_if_needed(self, # pylint: disable=no-self-use kind): # pylint: disable=unused-argument """Convert the disk to a more appropriate format if needed. - :param disk_image: Image to inspect and possibly convert - :type disk_image: instance of :class:`~COT.disks.DiskRepresentation` - or subclass - :param str kind: Image type (harddisk/cdrom). - :return: - * :attr:`disk_image`, if no conversion was required - * or a new :class:`~COT.disks.DiskRepresentation` instance - representing a converted image that has been created in - :attr:`output_dir`. + Args: + disk_image (DiskRepresentation): Image to inspect and possibly + convert + kind (str): Image type (harddisk/cdrom). + Returns: + DiskRepresentation: + * :attr:`disk_image`, if no conversion was required + * or a new :class:`~COT.disks.disk.DiskRepresentation` instance + representing a converted image that has been created in + :attr:`output_dir`. """ # Some VMs may not need this, so default to do nothing, not error return disk_image @@ -271,79 +275,97 @@ def convert_disk_if_needed(self, # pylint: disable=no-self-use def search_from_filename(self, filename): """From the given filename, try to find any existing objects. - :param str filename: Filename to search from - :return: ``(file, disk, controller_device, disk_device)``, - opaque objects of which any or all may be ``None`` + Args: + filename (str): Filename to search from + Returns: + tuple: ``(file, disk, controller_device, disk_device)``, + opaque objects of which any or all may be ``None`` """ raise NotImplementedError("search_from_filename not implemented") def search_from_file_id(self, file_id): """From the given file ID, try to find any existing objects. - :param str file_id: File ID to search from - :return: ``(file, disk, controller_device, disk_device)``, - opaque objects of which any or all may be ``None`` + Args: + file_id (str): File ID to search from + Returns: + tuple: ``(file, disk, controller_device, disk_device)``, + opaque objects of which any or all may be ``None`` """ raise NotImplementedError("search_from_file_id not implemented") def search_from_controller(self, controller, address): """From the controller type and device address, look for existing disk. - :param str controller: ``'ide'`` or ``'scsi'`` - :param str address: Device address such as ``'1:0'`` - :return: ``(file, disk, controller_device, disk_device)``, - opaque objects of which any or all may be ``None`` + Args: + controller (str): ``'ide'`` or ``'scsi'`` + address (str): Device address such as ``'1:0'`` + Returns: + tuple: ``(file, disk, controller_device, disk_device)``, + opaque objects of which any or all may be ``None`` """ raise NotImplementedError("search_from_controller not implemented") def find_open_controller(self, controller_type): """Find the first open slot on a controller of the given type. - :param str controller_type: ``'ide'`` or ``'scsi'`` - :return: ``(controller_device, address_string)`` or ``(None, None)`` + Args: + controller_type (str): ``'ide'`` or ``'scsi'`` + Returns: + tuple: ``(controller_device, address_string)`` or ``(None, None)`` """ raise NotImplementedError("find_open_controller not implemented") def get_id_from_file(self, file_obj): """Get the file ID from the given opaque file object. - :param object file_obj: File object to query - :return: Identifier string associated with this object + Args: + file_obj (object): File object to query + Returns: + str: Identifier string associated with this object """ raise NotImplementedError("get_id_from_file not implemented") def get_path_from_file(self, file_obj): """Get the file path from the given opaque file object. - :param object file_obj: File object to query - :return: Relative path to the file associated with this object + Args: + file_obj (object): File object to query + Returns: + str: Relative path to the file associated with this object """ raise NotImplementedError("get_path_from_file not implemented") def get_file_ref_from_disk(self, disk): """Get the file reference from the given opaque disk object. - :param object disk: Disk object to query - :return: String that can be used to identify the file associated - with this disk + Args: + disk (object): Disk object to query + Returns: + str: String that can be used to identify the file associated + with this disk """ raise NotImplementedError("get_file_ref_from_disk not implemented") def get_id_from_disk(self, disk): """Get the identifier string associated with the given Disk object. - :param object disk: Disk object - :rtype: string + Args: + disk (object): Disk object + Returns: + str: Identifier string associated with this object """ raise NotImplementedError("get_id_from_disk not implemented") def get_common_subtype(self, device_type): """Get the sub-type common to all devices of the given type. - :param str device_type: Device type such as ``'ide'`` or ``'memory'``. - :return: ``None``, if multiple such devices exist and they do not all - have the same sub-type. - :return: Subtype string common to all devices of the type. + Args: + device_type (str): Device type such as ``'ide'`` or ``'memory'``. + Returns: + str: Subtype string common to all devices of this type, or ``None``, + if multiple such devices exist and they do not all + have the same sub-type. """ raise NotImplementedError("get_common_subtype not implemented") @@ -351,14 +373,16 @@ def check_sanity_of_disk_device(self, disk, file_obj, disk_item, ctrl_item): """Check if the given disk is linked properly to the other objects. - :param object disk: Disk object to validate - :param object file_obj: File object which this disk should be linked to - (optional) - :param object disk_item: Disk device object which should link to - this disk (optional) - :param object ctrl_item: Controller device object which should link to - the :attr:`disk_item` - :raises ValueMismatchError: if the given items are not linked properly. + Args: + disk (object): Disk object to validate + file_obj (object): File object which this disk should be linked to + (optional) + disk_item (object): Disk device object which should link to + this disk (optional) + ctrl_item (object): Controller device object which should link to + the :attr:`disk_item` + Raises: + ValueMismatchError: if the given items are not linked properly. """ raise NotImplementedError( "check_sanity_of_disk_device not implemented") @@ -366,34 +390,38 @@ def check_sanity_of_disk_device(self, disk, file_obj, def add_file(self, file_path, file_id, file_obj=None, disk=None): """Add a new file object to the VM or overwrite the provided one. - :param str file_path: Path to file to add - :param str file_id: Identifier string for the file in the VM - :param object file_obj: Existing file object to overwrite - :param object disk: Existing disk object referencing :attr:`file`. + Args: + file_path (str): Path to file to add + file_id (str): Identifier string for the file in the VM + file_obj (object): Existing file object to overwrite + disk (object): Existing disk object referencing :attr:`file`. - :return: New or updated file object + Returns: + object: New or updated file object """ raise NotImplementedError("add_file not implemented") def remove_file(self, file_obj, disk=None, disk_drive=None): """Remove the given file object from the VM. - :param object file_obj: File object to remove - :param object disk: Disk object referencing :attr:`file` - :param object disk_drive: Disk drive mapping :attr:`file` to a device + Args: + file_obj (object): File object to remove + disk (object): Disk object referencing :attr:`file` + disk_drive (object): Disk drive mapping :attr:`file` to a device """ raise NotImplementedError("remove_file not implemented") def add_disk(self, disk_repr, file_id, drive_type, disk=None): """Add a new disk object to the VM or overwrite the provided one. - :param str disk_repr: Disk file representation - :type disk_repr: COT.disks.DiskRepresentation or subclass - :param str file_id: Identifier string for the file/disk mapping - :param str drive_type: 'harddisk' or 'cdrom' - :param object disk: Existing disk object to overwrite + Args: + disk_repr (DiskRepresentation): Disk file representation + file_id (str): Identifier string for the file/disk mapping + drive_type (str): 'harddisk' or 'cdrom' + disk (object): Existing disk object to overwrite - :return: New or updated disk object + Returns: + object: New or updated disk object """ raise NotImplementedError("add_disk not implemented") @@ -401,13 +429,14 @@ def add_controller_device(self, device_type, subtype, address, ctrl_item=None): """Create a new IDE or SCSI controller, or update existing one. - :param str device_type: ``'ide'`` or ``'scsi'`` - :param str subtype: Subtype such as ``'virtio'`` (optional) - :param int address: Controller address such as 0 or 1 (optional) - :param object ctrl_item: Existing controller device to - update (optional) + Args: + device_type (str): ``'ide'`` or ``'scsi'`` + subtype (str): Subtype such as ``'virtio'`` (optional) + address (int): Controller address such as 0 or 1 (optional) + ctrl_item (object): Existing controller device to update (optional) - :return: New or updated controller device object + Returns: + object: New or updated controller device object """ raise NotImplementedError("add_controller_device not implemented") @@ -415,17 +444,19 @@ def add_disk_device(self, drive_type, address, name, description, disk, file_obj, ctrl_item, disk_item=None): """Add a new disk device to the VM or update the provided one. - :param str drive_type: ``'harddisk'`` or ``'cdrom'`` - :param str address: Address on controller, such as "1:0" (optional) - :param str name: Device name string (optional) - :param str description: Description string (optional) - :param object disk: Disk object to map to this device - :param object file_obj: File object to map to this device - :param object ctrl_item: Controller object to serve as parent - :param object disk_item: Existing disk device to update instead of - making a new device. + Args: + drive_type (str): ``'harddisk'`` or ``'cdrom'`` + address (str): Address on controller, such as "1:0" (optional) + name (str): Device name string (optional) + description (str): Description string (optional) + disk (object): Disk object to map to this device + file_obj (object): File object to map to this device + ctrl_item (object): Controller object to serve as parent + disk_item (object): Existing disk device to update instead of + making a new device. - :return: New or updated disk device object. + Returns: + object: New or updated disk device object. """ raise NotImplementedError("add_disk_device not implemented") @@ -433,9 +464,10 @@ def add_disk_device(self, drive_type, address, name, description, def create_configuration_profile(self, pid, label, description): """Create/update a configuration profile with the given ID. - :param str pid: Profile identifier - :param str label: Brief descriptive label for the profile - :param str description: Verbose description of the profile + Args: + pid (str): Profile identifier + label (str): Brief descriptive label for the profile + description (str): Verbose description of the profile """ raise NotImplementedError("create_configuration_profile " "not implemented!") @@ -443,7 +475,8 @@ def create_configuration_profile(self, pid, label, description): def delete_configuration_profile(self, profile): """Delete the configuration profile with the given ID. - :param str profile: Profile identifier + Args: + profile (str): Profile identifier """ raise NotImplementedError("delete_configuration_profile " "not implemented") @@ -471,16 +504,18 @@ def delete_configuration_profile(self, profile): def set_cpu_count(self, cpus, profile_list): """Set the number of CPUs. - :param int cpus: Number of CPUs - :param list profile_list: Change only the given profiles + Args: + cpus (int): Number of CPUs + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_cpu_count not implemented!") def set_memory(self, megabytes, profile_list): """Set the amount of RAM, in megabytes. - :param int megabytes: Memory value, in megabytes - :param list profile_list: Change only the given profiles + Args: + megabytes (int): Memory value, in megabytes + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_memory not implemented!") @@ -490,8 +525,9 @@ def set_nic_type(self, nic_type, profile_list): .. deprecated:: 1.5 Use :func:`set_nic_types` instead. - :param str nic_type: NIC hardware type - :param list profile_list: Change only the given profiles. + Args: + nic_type (str): NIC hardware type + profile_list (list): Change only the given profiles. """ warnings.warn("Use set_nic_types() instead", DeprecationWarning) self.set_nic_types([nic_type], profile_list) @@ -499,25 +535,28 @@ def set_nic_type(self, nic_type, profile_list): def set_nic_types(self, type_list, profile_list): """Set the hardware type(s) for NICs. - :param list type_list: NIC hardware type(s) - :param list profile_list: Change only the given profiles. + Args: + type_list (list): NIC hardware type(s) + profile_list (list): Change only the given profiles. """ raise NotImplementedError("set_nic_types not implemented!") def get_nic_count(self, profile_list): """Get the number of NICs under the given profile(s). - :param list profile_list: Profile(s) of interest. - :rtype: dict - :return: ``{ profile_name : nic_count }`` + Args: + profile_list (list): Profile(s) of interest. + Returns: + dict: ``{ profile_name : nic_count }`` """ raise NotImplementedError("get_nic_count not implemented!") def set_nic_count(self, count, profile_list): """Set the given profile(s) to have the given number of NICs. - :param int count: number of NICs - :param list profile_list: Change only the given profiles + Args: + count (int): number of NICs + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_nic_count not implemented!") @@ -526,8 +565,9 @@ def create_network(self, label, description): Also serves to update the description of an existing network label. - :param str label: Brief label for the network - :param str description: Verbose description of the network + Args: + label (str): Brief label for the network + description (str): Verbose description of the network """ raise NotImplementedError("create_network not implemented!") @@ -538,8 +578,9 @@ def set_nic_networks(self, network_list, profile_list): If the length of :attr:`network_list` is less than the number of NICs, will use the last entry in the list for all remaining NICs. - :param list network_list: List of networks to map NICs to - :param list profile_list: Change only the given profiles + Args: + network_list (list): List of networks to map NICs to + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_nic_networks not implemented!") @@ -550,8 +591,9 @@ def set_nic_mac_addresses(self, mac_list, profile_list): If the length of :attr:`mac_list` is less than the number of NICs, will use the last entry in the list for all remaining NICs. - :param list mac_list: List of MAC addresses to assign to NICs - :param list profile_list: Change only the given profiles + Args: + mac_list (list): List of MAC addresses to assign to NICs + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_nic_mac_addresses not implemented!") @@ -570,41 +612,47 @@ def set_nic_names(self, name_list, profile_list): ``["mgmt0" "eth{10}"]`` Expands to ``["mgmt0", "eth10", "eth11", "eth12", ...]`` - :param list name_list: List of names to assign. - :param list profile_list: Change only the given profiles + Args: + name_list (list): List of names to assign. + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_nic_names not implemented!") def get_serial_count(self, profile_list): """Get the number of serial ports under the given profile(s). - :param list profile_list: Change only the given profiles - :rtype: dict - :return: ``{ profile_name : serial_count }`` + Args: + profile_list (list): Change only the given profiles + Returns: + dict: ``{ profile_name : serial_count }`` """ raise NotImplementedError("get_serial_count not implemented!") def set_serial_count(self, count, profile_list): """Set the given profile(s) to have the given number of NICs. - :param int count: Number of serial ports - :param list profile_list: Change only the given profiles + Args: + count (int): Number of serial ports + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_serial_count not implemented!") def set_serial_connectivity(self, conn_list, profile_list): """Set the serial port connectivity under the given profile(s). - :param list conn_list: List of connectivity strings - :param list profile_list: Change only the given profiles + Args: + conn_list (list): List of connectivity strings + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_serial_connectivity not implemented!") def get_serial_connectivity(self, profile): """Get the serial port connectivity strings under the given profile. - :param str profile: Profile of interest. - :return: List of connectivity strings + Args: + profile (str): Profile of interest. + Returns: + list: List of connectivity strings """ raise NotImplementedError("get_serial_connectivity not implemented!") @@ -614,8 +662,9 @@ def set_scsi_subtype(self, subtype, profile_list): .. deprecated:: 1.5 Use :func:`set_scsi_subtypes` instead. - :param str subtype: SCSI subtype string - :param list profile_list: Change only the given profiles + Args: + subtype (str): SCSI subtype string + profile_list (list): Change only the given profiles """ warnings.warn("Use set_scsi_subtypes() instead", DeprecationWarning) self.set_scsi_subtypes([subtype], profile_list) @@ -623,8 +672,9 @@ def set_scsi_subtype(self, subtype, profile_list): def set_scsi_subtypes(self, type_list, profile_list): """Set the device subtype list for the SCSI controller(s). - :param list type_list: SCSI subtype string list - :param list profile_list: Change only the given profiles + Args: + type_list (list): SCSI subtype string list + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_scsi_subtypes not implemented!") @@ -634,8 +684,9 @@ def set_ide_subtype(self, subtype, profile_list): .. deprecated:: 1.5 Use :func:`set_ide_subtypes` instead. - :param str subtype: IDE subtype string - :param list profile_list: Change only the given profiles + Args: + subtype (str): IDE subtype string + profile_list (list): Change only the given profiles """ warnings.warn("Use set_ide_subtypes() instead", DeprecationWarning) self.set_ide_subtypes([subtype], profile_list) @@ -643,8 +694,9 @@ def set_ide_subtype(self, subtype, profile_list): def set_ide_subtypes(self, type_list, profile_list): """Set the device subtype list for the IDE controller(s). - :param list type_list: IDE subtype string list - :param list profile_list: Change only the given profiles + Args: + type_list (list): IDE subtype string list + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_ide_subtypes not implemented!") @@ -653,8 +705,10 @@ def set_ide_subtypes(self, type_list, profile_list): def get_property_value(self, key): """Get the value of the given property. - :param str key: Property identifier - :return: Value of this property, or ``None`` + Args: + key (str): Property identifier + Returns: + str: Value of this property, or ``None`` """ raise NotImplementedError("get_property_value not implemented") @@ -663,23 +717,27 @@ def set_property_value(self, key, value, label=None, description=None): """Set the value of the given property (converting value if needed). - :param str key: Property identifier - :param object value: Value to set for this property - :param bool user_configurable: Should this property be configurable at - deployment time by the user? - :param str property_type: Value type - 'string' or 'boolean' - :param str label: Brief explanatory label for this property - :param str description: Detailed description of this property - :return: the (converted) value that was set. + Args: + key (str): Property identifier + value (object): Value to set for this property + user_configurable (bool): Should this property be configurable at + deployment time by the user? + property_type (str): Value type - 'string' or 'boolean' + label (str): Brief explanatory label for this property + description (str): Detailed description of this property + + Returns: + str: the (converted) value that was set. """ raise NotImplementedError("set_property_value not implemented") def config_file_to_properties(self, file_path, user_configurable=None): """Import each line of a text file into a configuration property. - :param str file_path: File name to import. - :param bool user_configurable: Should the properties be configurable at - deployment time by the user? + Args: + file_path (str): File name to import. + user_configurable (bool): Should the properties be configurable at + deployment time by the user? """ raise NotImplementedError("config_file_to_properties not implemented") @@ -693,22 +751,26 @@ def config_file_to_properties(self, file_path, user_configurable=None): def info_string(self, width=79, verbosity_option=None): """Get a descriptive string summarizing the contents of this VM. - :param int width: Line length to wrap to where possible. - :param str verbosity_option: ``'brief'``, ``None`` (default), - or ``'verbose'`` + Args: + width (int): Line length to wrap to where possible. + verbosity_option (str): ``'brief'``, ``None`` (default), + or ``'verbose'`` - :return: Wrapped, appropriately verbose string. + Returns: + str: Wrapped, appropriately verbose string. """ raise NotImplementedError("info_string not implemented") def profile_info_string(self, width=79, verbosity_option=None): """Get a string summarizing available configuration profiles. - :param int width: Line length to wrap to if possible - :param str verbosity_option: ``'brief'``, ``None`` (default), - or ``'verbose'`` + Args: + width (int): Line length to wrap to if possible + verbosity_option (str): ``'brief'``, ``None`` (default), + or ``'verbose'`` - :return: Appropriately formatted and verbose string. + Returns: + str: Appropriately formatted and verbose string. """ raise NotImplementedError("profile_info_string not implemented") @@ -716,15 +778,19 @@ def profile_info_string(self, width=79, verbosity_option=None): def find_empty_drive(self, drive_type): """Find a disk device that exists but contains no data. - :param str drive_type: Disk drive type, such as 'cdrom' or 'harddisk' - :return: Hardware device object, or None. + Args: + drive_type (str): Disk drive type, such as 'cdrom' or 'harddisk' + Returns: + object: Hardware device object, or None. """ raise NotImplementedError("find_empty_drive not implemented") def find_device_location(self, device): """Find the controller type and address of a given device object. - :param object device: Hardware device object. - :returns: ``(type, address)``, such as ``("ide", "1:0")``. + Args: + device (object): Hardware device object. + Returns: + tuple: ``(type, address)``, such as ``("ide", "1:0")``. """ raise NotImplementedError("find_device_location not implemented") diff --git a/COT/vm_factory.py b/COT/vm_factory.py index 21bd6a2..63580f4 100644 --- a/COT/vm_factory.py +++ b/COT/vm_factory.py @@ -32,13 +32,17 @@ class VMFactory(object): def create(cls, input_file, output_file): """Create an appropriate VMDescription subclass instance from a file. - :raises VMInitError: if no appropriate class is identified - :raises VMInitError: if the selected subclass raises a - ValueUnsupportedError while loading the file. - :param str input_file: File to read VM description from - :param str output_file: File to write to when finished (optional) - :return: Created object - :rtype: instance of :class:`VMDescription` or appropriate subclass + Args: + input_file (str): File to read VM description from + output_file (str): File to write to when finished (optional) + + Raises: + VMInitError: if no appropriate class is identified + VMInitError: if the selected subclass raises a + ValueUnsupportedError while loading the file. + + Returns: + VMDescription: Created object """ vm_class = None diff --git a/COT/xml_file.py b/COT/xml_file.py index b8c8597..65b8f1d 100644 --- a/COT/xml_file.py +++ b/COT/xml_file.py @@ -27,9 +27,10 @@ def register_namespace(prefix, uri): """Record a particular mapping between a namespace prefix and URI. - :param str prefix: Namespace prefix such as "ovf" - :param str uri: Namespace URI such as - "http://schemas.dmtf.org/ovf/envelope/1" + Args: + prefix (str): Namespace prefix such as "ovf" + uri (str): Namespace URI such as + "http://schemas.dmtf.org/ovf/envelope/1" """ try: ET.register_namespace(prefix, uri) @@ -45,10 +46,13 @@ class XML(object): def get_ns(cls, text): """Get the namespace prefix from an XML element or attribute name. - :param str text: Element name or attribute name, such as - "{http://schemas.dmtf.org/ovf/envelope/1}Element". - :return: Namespace prefix, such as - "http://schemas.dmtf.org/ovf/envelope/1", or "" if no prefix present. + Args: + text (str): Element name or attribute name, such as + "{http://schemas.dmtf.org/ovf/envelope/1}Element". + Returns: + str: Namespace prefix, such as + "http://schemas.dmtf.org/ovf/envelope/1", or "" if no prefix + is present. """ match = re.match(r"\{(.*)\}", str(text)) if not match: @@ -60,9 +64,11 @@ def get_ns(cls, text): def strip_ns(cls, text): """Remove a namespace prefix from an XML element or attribute name. - :param str text: Element name or attribute name, such as - "{http://schemas.dmtf.org/ovf/envelope/1}Element". - :return: Bare name, such as "Element". + Args: + text (str): Element name or attribute name, such as + "{http://schemas.dmtf.org/ovf/envelope/1}Element". + Returns: + str: Bare name, such as "Element". """ match = re.match(r"\{.*\}(.*)", str(text)) if match is None: @@ -77,11 +83,13 @@ def __init__(self, xml_file): The memory representation is available as properties :attr:`tree` and :attr:`root`. - :param str xml_file: File path to read. + Args: + xml_file (str): File path to read. - :raises xml.etree.ElementTree.ParseError: if parsing fails under Python - 2.7 or later - :raises xml.parsers.expat.ExpatError: if parsing fails under Python 2.6 + Raises: + xml.etree.ElementTree.ParseError: if parsing fails under Python + 2.7 or later + xml.parsers.expat.ExpatError: if parsing fails under Python 2.6 """ # Parse the XML into memory self.tree = ET.parse(xml_file) @@ -92,7 +100,8 @@ def __init__(self, xml_file): def write_xml(self, xml_file): """Write pretty XML out to the given file. - :param str xml_file: Filename to write to + Args: + xml_file (str): Filename to write to """ logger.debug("Writing XML to %s", xml_file) @@ -118,10 +127,10 @@ def write_xml(self, xml_file): def xml_reindent(self, parent, depth): """Recursively add indentation to XML to make it look nice. - :param parent: Current parent element - :type parent: :class:`xml.etree.ElementTree.Element` - :param int depth: How far down the rabbit hole we have recursed. - Increments by 2 for each successive level of nesting. + Args: + parent (xml.etree.ElementTree.Element): Current parent element + depth (int): How far down the rabbit hole we have recursed. + Increments by 2 for each successive level of nesting. """ depth += 2 last = None @@ -145,16 +154,19 @@ def xml_reindent(self, parent, depth): def find_child(cls, parent, tag, attrib=None, required=False): """Find the unique child element under the specified parent element. - :raises LookupError: if more than one matching child is found - :raises KeyError: if no matching child is found and :attr:`required` - is True - :param parent: Parent element - :type parent: :class:`xml.etree.ElementTree.Element` - :param str tag: Child tag to match on - :param dict attrib: Child attributes to match on - :param boolean required: Whether to raise an error if no child exists - :return: Child element found, or None - :rtype: :class:`xml.etree.ElementTree.Element` + Args: + parent (xml.etree.ElementTree.Element): Parent element + tag (str): Child tag to match on + attrib (dict): Child attributes to match on + required (boolean): Whether to raise an error if no child exists + + Raises: + LookupError: if more than one matching child is found + KeyError: if no matching child is found and :attr:`required` + is True + + Returns: + xml.etree.ElementTree.Element: Child element found, or None """ matches = cls.find_all_children(parent, tag, attrib) if len(matches) > 1: @@ -178,13 +190,13 @@ def find_child(cls, parent, tag, attrib=None, required=False): def find_all_children(cls, parent, tag, attrib=None): """Find all matching child elements under the specified parent element. - :param parent: Parent element - :type parent: :class:`xml.etree.ElementTree.Element` - :param tag: Child tag (or list of tags) to match on - :type tag: string or iterable - :param dict attrib: Child attributes to match on - :return: (Possibly empty) list of matching child elements - :rtype: list of :class:`xml.etree.ElementTree.Element` instances + Args: + parent (xml.etree.ElementTree.Element): Parent element + tag (iterable): Child tag string (or list of tags) to match on + attrib (dict): Child attributes to match on + + Returns: + list: (Possibly empty) list of matching child Elements """ assert parent is not None if isinstance(tag, str): @@ -221,18 +233,19 @@ def add_child(cls, parent, new_child, ordering=None, known_namespaces=None): """Add the given child element under the given parent element. - :param parent: Parent element - :type parent: :class:`xml.etree.ElementTree.Element` - :param new_child: Child element to attach - :type new_child: :class:`xml.etree.ElementTree.Element` - :param list ordering: (Optional) List describing the expected ordering - of child tags under the parent; if a new child element is created, - its placement under the parent will respect this sequence. - :param list known_namespaces: (Optional) List of well-understood XML - namespaces. If a new child is created, and ``ordering`` is given, - any tag (new or existing) that is encountered but not accounted for - in ``ordering`` will result in COT logging a warning **iff** the - unaccounted-for tag is in a known namespace. + Args: + parent (xml.etree.ElementTree.Element): Parent element + new_child (xml.etree.ElementTree.Element): Child element to attach + ordering (list): (Optional) List describing the expected ordering + of child tags under the parent; if a new child element is + created, its placement under the parent will respect this + sequence. + known_namespaces (list): (Optional) List of well-understood XML + namespaces. If a new child is created, and ``ordering`` is + given, any tag (new or existing) that is encountered but not + accounted for in ``ordering`` will result in COT logging a + warning **if and only if** the unaccounted-for tag is in a + known namespace. """ if ordering and new_child.tag not in ordering: if (known_namespaces and @@ -279,16 +292,17 @@ def set_or_make_child(cls, parent, tag, text=None, attrib=None, ordering=None, known_namespaces=None): """Update or create a child element under the specified parent element. - :param parent: Parent element - :type parent: :class:`xml.etree.ElementTree.Element` - :param str tag: Child element text tag to find or create - :param str text: Value to set the child's text attribute to - :param dict attrib: Dict of child attributes to match on - while searching and set in the final child element - :param list ordering: See :meth:`add_child` - :param list known_namespaces: See :meth:`add_child` - :return: New or updated child Element. - :rtype: :class:`xml.etree.ElementTree.Element` + Args: + parent (xml.etree.ElementTree.Element): Parent element + tag (str): Child element text tag to find or create + text (str): Value to set the child's text attribute to + attrib (dict): Dict of child attributes to match on while + searching and set in the final child element + ordering (list): See :meth:`add_child` + known_namespaces (list): See :meth:`add_child` + + Returns: + xml.etree.ElementTree.Element: New or updated child Element. """ assert parent is not None if attrib is None: diff --git a/docs/conf.py b/docs/conf.py index 9aba237..4312cd9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -233,7 +233,7 @@ def help_text_to_rst(help, dirpath): # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' +needs_sphinx = '1.3.1' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -244,7 +244,7 @@ def help_text_to_rst(help, dirpath): 'sphinx.ext.ifconfig', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', - 'sphinxcontrib.napoleon', + 'sphinx.ext.napoleon', ] # -- Autodoc configuration -------------------- diff --git a/setup.py b/setup.py index 88852f3..5a942b3 100644 --- a/setup.py +++ b/setup.py @@ -49,9 +49,9 @@ if sys.version_info < (2, 7) or (sys.version_info >= (3, 0) and sys.version_info < (3, 4)): # Sphinx 1.5 and later requires 2.7 or 3.4 - setup_requires = install_requires + ['sphinx>=1.3,<1.5'] + setup_requires = install_requires + ['sphinx>=1.3.1,<1.5'] else: - setup_requires = install_requires + ['sphinx>=1.3'] + setup_requires = install_requires + ['sphinx>=1.3.1'] setup_requires.append('sphinx_rtd_theme') From 47de57f1e4761530f88f58e9e9fd0154f18edb2a Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 31 Oct 2016 15:38:56 -0400 Subject: [PATCH 43/59] Change COT.platforms to use google style docstrings --- COT/platforms/__init__.py | 14 +++-- COT/platforms/cisco_csr1000v.py | 58 ++++++++++++------- COT/platforms/cisco_iosv.py | 48 ++++++++++------ COT/platforms/cisco_iosxrv.py | 89 +++++++++++++++++++----------- COT/platforms/cisco_iosxrv_9000.py | 44 +++++++++------ COT/platforms/cisco_nxosv.py | 50 ++++++++++------- COT/platforms/generic.py | 84 ++++++++++++++++++---------- 7 files changed, 249 insertions(+), 138 deletions(-) diff --git a/COT/platforms/__init__.py b/COT/platforms/__init__.py index dba680e..b90024a 100644 --- a/COT/platforms/__init__.py +++ b/COT/platforms/__init__.py @@ -73,8 +73,11 @@ def is_known_product_class(product_class): """Determine if the given product class string is a known one. - :param str product_class: String such as 'com.cisco.iosv' - :rtype: boolean + Args: + product_class (str): String such as 'com.cisco.iosv' + + Returns: + bool: Whether product_class is known. """ return product_class in PRODUCT_PLATFORM_MAP @@ -82,8 +85,11 @@ def is_known_product_class(product_class): def platform_from_product_class(product_class): """Get the class of Platform corresponding to a product class string. - :param str product_class: String such as 'com.cisco.iosv' - :return: Class object - GenericPlatform or a subclass of it + Args: + product_class (str): String such as 'com.cisco.iosv' + + Returns: + class: GenericPlatform or a subclass of it """ if product_class is None: return GenericPlatform diff --git a/COT/platforms/cisco_csr1000v.py b/COT/platforms/cisco_csr1000v.py index 0d6a89d..3f92e7b 100644 --- a/COT/platforms/cisco_csr1000v.py +++ b/COT/platforms/cisco_csr1000v.py @@ -37,8 +37,10 @@ class CSR1000V(GenericPlatform): def controller_type_for_device(cls, device_type): """CSR1000V uses SCSI for hard disks and IDE for CD-ROMs. - :param str device_type: 'harddisk' or 'cdrom' - :return: 'ide' for CD-ROM, 'scsi' for hard disk + Args: + device_type (str): 'harddisk' or 'cdrom' + Returns: + str: 'ide' for CD-ROM, 'scsi' for hard disk """ if device_type == 'harddisk': return 'scsi' @@ -56,11 +58,13 @@ def guess_nic_name(cls, nic_number): Some early versions started at "GigabitEthernet0" but we don't support that. - :param int nic_number: Nth NIC to name. - :return: - * "GigabitEthernet1" - * "GigabitEthernet2" - * etc. + Args: + nic_number (int): Nth NIC to name. + Returns: + str: + * "GigabitEthernet1" + * "GigabitEthernet2" + * etc. """ return "GigabitEthernet" + str(nic_number) @@ -68,11 +72,14 @@ def guess_nic_name(cls, nic_number): def validate_cpu_count(cls, cpus): """CSR1000V supports 1, 2, 4, or 8 CPUs. - :param int cpus: Number of CPUs. - :raises ValueTooLowError: if ``cpus`` is less than 1 - :raises ValueTooHighError: if ``cpus`` is more than 8 - :raises ValueUnsupportedError: if ``cpus`` is an unsupported value - between 1 and 8 + Args: + cpus (int): Number of CPUs. + + Raises: + ValueTooLowError: if ``cpus`` is less than 1 + ValueTooHighError: if ``cpus`` is more than 8 + ValueUnsupportedError: if ``cpus`` is an unsupported value + between 1 and 8 """ validate_int(cpus, 1, 8, "CPUs") if cpus not in [1, 2, 4, 8]: @@ -82,9 +89,12 @@ def validate_cpu_count(cls, cpus): def validate_memory_amount(cls, mebibytes): """Minimum 2.5 GiB, max 8 GiB. - :param int mebibytes: RAM, in MiB. - :raises ValueTooLowError: if``mebibytes`` is less than 2560 - :raises ValueTooHighError: if ``mebibytes`` is more than 8192 + 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") @@ -95,9 +105,12 @@ def validate_memory_amount(cls, mebibytes): def validate_nic_count(cls, count): """CSR1000V requires 3 NICs and supports up to 26. - :param int count: Number of NICs. - :raises ValueTooLowError: if ``count`` is less than 3 - :raises ValueTooHighError: if ``count`` is more than 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") @@ -105,8 +118,11 @@ def validate_nic_count(cls, count): def validate_serial_count(cls, count): """CSR1000V supports 0-2 serial ports. - :param int count: Number of serial ports. - :raises ValueTooLowError: if ``count`` is less than 0 - :raises ValueTooHighError: if ``count`` is more than 2 + 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") diff --git a/COT/platforms/cisco_iosv.py b/COT/platforms/cisco_iosv.py index d44f321..fb152ac 100644 --- a/COT/platforms/cisco_iosv.py +++ b/COT/platforms/cisco_iosv.py @@ -37,11 +37,13 @@ class IOSv(GenericPlatform): def guess_nic_name(cls, nic_number): """GigabitEthernet0/0, GigabitEthernet0/1, etc. - :param int nic_number: Nth NIC to name. - :return: - * "GigabitEthernet0/0" - * "GigabitEthernet0/1" - * etc. + Args: + nic_number (int): Nth NIC to name. + Returns: + str: + * "GigabitEthernet0/0" + * "GigabitEthernet0/1" + * etc. """ return "GigabitEthernet0/" + str(nic_number - 1) @@ -49,9 +51,12 @@ def guess_nic_name(cls, nic_number): def validate_cpu_count(cls, cpus): """IOSv only supports a single CPU. - :param int cpus: Number of CPUs. - :raises ValueTooLowError: if ``cpus`` is less than 1 - :raises ValueTooHighError: if ``cpus`` is more than 1 + 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") @@ -59,9 +64,12 @@ def validate_cpu_count(cls, cpus): def validate_memory_amount(cls, mebibytes): """IOSv has minimum 192 MiB (with minimal feature set), max 3 GiB. - :param int mebibytes: RAM, in MiB. - :raises ValueTooLowError: if``mebibytes`` is less than 192 - :raises ValueTooHighError: if ``mebibytes`` is more than 3072 + Args: + mebibytes (int): RAM, in MiB. + + Raises: + 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") @@ -76,9 +84,12 @@ def validate_memory_amount(cls, mebibytes): def validate_nic_count(cls, count): """IOSv supports up to 16 NICs. - :param int count: Number of NICs. - :raises ValueTooLowError: if ``count`` is less than 0 - :raises ValueTooHighError: if ``count`` is more than 16 + 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") @@ -86,8 +97,11 @@ def validate_nic_count(cls, count): def validate_serial_count(cls, count): """IOSv requires 1-2 serial ports. - :param int count: Number of serial ports. - :raises ValueTooLowError: if ``count`` is less than 1 - :raises ValueTooHighError: if ``count`` is more than 2 + 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") diff --git a/COT/platforms/cisco_iosxrv.py b/COT/platforms/cisco_iosxrv.py index 43a90c4..6b53f7a 100644 --- a/COT/platforms/cisco_iosxrv.py +++ b/COT/platforms/cisco_iosxrv.py @@ -46,12 +46,15 @@ class IOSXRv(GenericPlatform): def guess_nic_name(cls, nic_number): """MgmtEth0/0/CPU0/0, GigabitEthernet0/0/0/0, Gig0/0/0/1, etc. - :param int nic_number: Nth NIC to name. - :return: - * "MgmtEth0/0/CPU0/0" - * "GigabitEthernet0/0/0/0" - * "GigabitEthernet0/0/0/1" - * etc. + Args: + nic_number (int): Nth NIC to name. + + Returns: + str: + * "MgmtEth0/0/CPU0/0" + * "GigabitEthernet0/0/0/0" + * "GigabitEthernet0/0/0/1" + * etc. """ if nic_number == 1: return "MgmtEth0/0/CPU0/0" @@ -62,9 +65,12 @@ def guess_nic_name(cls, nic_number): def validate_cpu_count(cls, cpus): """IOS XRv supports 1-8 CPUs. - :param int cpus: Number of CPUs - :raises ValueTooLowError: if ``cpus`` is less than 1 - :raises ValueTooHighError: if ``cpus`` is more than 8 + 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") @@ -72,9 +78,12 @@ def validate_cpu_count(cls, cpus): def validate_memory_amount(cls, mebibytes): """Minimum 3 GiB, max 8 GiB of RAM. - :param int mebibytes: RAM, in MiB. - :raises ValueTooLowError: if``mebibytes`` is less than 3072 - :raises ValueTooHighError: if ``mebibytes`` is more than 8192 + 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") @@ -85,8 +94,11 @@ def validate_memory_amount(cls, mebibytes): def validate_nic_count(cls, count): """IOS XRv requires at least one NIC. - :param int count: Number of NICs. - :raises ValueTooLowError: if ``count`` is less than 1 + Args: + count (int): Number of NICs. + + Raises: + ValueTooLowError: if ``count`` is less than 1 """ validate_int(count, 1, None, "NIC count") @@ -94,9 +106,12 @@ def validate_nic_count(cls, count): def validate_serial_count(cls, count): """IOS XRv supports 1-4 serial ports. - :param int count: Number of serial ports. - :raises ValueTooLowError: if ``count`` is less than 1 - :raises ValueTooHighError: if ``count`` is more than 4 + 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") @@ -110,8 +125,11 @@ class IOSXRvRP(IOSXRv): def guess_nic_name(cls, nic_number): """Fabric and management only. - :param int nic_number: Nth NIC to name. - :return: "fabric" or "MgmtEth0/{SLOT}/CPU0/0" only + Args: + nic_number (int): Nth NIC to name. + + Returns: + str: "fabric" or "MgmtEth0/{SLOT}/CPU0/0" only """ if nic_number == 1: return "fabric" @@ -122,9 +140,12 @@ def guess_nic_name(cls, nic_number): def validate_nic_count(cls, count): """Fabric plus an optional management NIC. - :param int count: Number of NICs. - :raises ValueTooLowError: if ``count`` is less than 1 - :raises ValueTooHighError: if ``count`` is more than 2 + 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") @@ -142,12 +163,15 @@ class IOSXRvLC(IOSXRv): def guess_nic_name(cls, nic_number): """Fabric interface plus slot-appropriate GigabitEthernet interfaces. - :param int nic_number: Nth NIC to name. - :return: - * "fabric" - * "GigabitEthernet0/{SLOT}/0/0" - * "GigabitEthernet0/{SLOT}/0/1" - * etc. + Args: + nic_number (int): Nth NIC to name. + + Returns: + str: + * "fabric" + * "GigabitEthernet0/{SLOT}/0/0" + * "GigabitEthernet0/{SLOT}/0/1" + * etc. """ if nic_number == 1: return "fabric" @@ -158,8 +182,11 @@ def guess_nic_name(cls, nic_number): def validate_serial_count(cls, count): """No serial ports are needed but up to 4 can be used for debugging. - :param int count: Number of serial ports. - :raises ValueTooLowError: if ``count`` is less than 0 - :raises ValueTooHighError: if ``count`` is more than 4 + 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") diff --git a/COT/platforms/cisco_iosxrv_9000.py b/COT/platforms/cisco_iosxrv_9000.py index ae387bc..e26ab5f 100644 --- a/COT/platforms/cisco_iosxrv_9000.py +++ b/COT/platforms/cisco_iosxrv_9000.py @@ -32,14 +32,17 @@ class IOSXRv9000(IOSXRv): def guess_nic_name(cls, nic_number): """MgmtEth0/0/CPU0/0, CtrlEth, DevEth, GigabitEthernet0/0/0/0, etc. - :param int nic_number: Nth NIC to name. - :return: - * "MgmtEth0/0/CPU0/0" - * "CtrlEth" - * "DevEth" - * "GigabitEthernet0/0/0/0" - * "GigabitEthernet0/0/0/1" - * etc. + Args: + nic_number (int): Nth NIC to name. + + Returns: + str: + * "MgmtEth0/0/CPU0/0" + * "CtrlEth" + * "DevEth" + * "GigabitEthernet0/0/0/0" + * "GigabitEthernet0/0/0/1" + * etc. """ if nic_number == 1: return "MgmtEth0/0/CPU0/0" @@ -54,9 +57,12 @@ def guess_nic_name(cls, nic_number): def validate_cpu_count(cls, cpus): """Minimum 1, maximum 32 CPUs. - :param int cpus: Number of CPUs - :raises ValueTooLowError: if ``cpus`` is less than 1 - :raises ValueTooHighError: if ``cpus`` is more than 32 + 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") @@ -64,9 +70,12 @@ def validate_cpu_count(cls, cpus): def validate_memory_amount(cls, mebibytes): """Minimum 8 GiB, maximum 32 GiB. - :param int mebibytes: RAM, in MiB. - :raises ValueTooLowError: if``mebibytes`` is less than 8192 - :raises ValueTooHighError: if ``mebibytes`` is more than 32768 + Args: + mebibytes (int): RAM, in MiB. + + Raises: + ValueTooLowError: if ``mebibytes`` is less than 8192 + ValueTooHighError: if ``mebibytes`` is more than 32768 """ if mebibytes < 8192: raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "8 GiB") @@ -77,7 +86,10 @@ def validate_memory_amount(cls, mebibytes): def validate_nic_count(cls, count): """IOS XRv 9000 requires at least 4 NICs. - :param int count: Number of NICs. - :raises ValueTooLowError: if ``count`` is less than 4 + Args: + count (int): Number of NICs. + + Raises: + ValueTooLowError: if ``count`` is less than 4 """ validate_int(count, 4, None, "NIC count") diff --git a/COT/platforms/cisco_nxosv.py b/COT/platforms/cisco_nxosv.py index 979a43d..fc795f7 100644 --- a/COT/platforms/cisco_nxosv.py +++ b/COT/platforms/cisco_nxosv.py @@ -35,16 +35,19 @@ class NXOSv(GenericPlatform): def guess_nic_name(cls, nic_number): """NX-OSv names its NICs a bit interestingly... - :param int nic_number: Nth NIC to name. - :return: - * mgmt0 - * Ethernet2/1 - * Ethernet2/2 - * ... - * Ethernet2/48 - * Ethernet3/1 - * Ethernet3/2 - * ... + Args: + nic_number (int): Nth NIC to name. + + Returns: + str: + * mgmt0 + * Ethernet2/1 + * Ethernet2/2 + * ... + * Ethernet2/48 + * Ethernet3/1 + * Ethernet3/2 + * ... """ if nic_number == 1: return "mgmt0" @@ -56,9 +59,12 @@ def guess_nic_name(cls, nic_number): def validate_cpu_count(cls, cpus): """NX-OSv requires 1-8 CPUs. - :param int cpus: Number of CPUs - :raises ValueTooLowError: if ``cpus`` is less than 1 - :raises ValueTooHighError: if ``cpus`` is more than 8 + 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") @@ -66,9 +72,12 @@ def validate_cpu_count(cls, cpus): def validate_memory_amount(cls, mebibytes): """NX-OSv requires 2-8 GiB of RAM. - :param int mebibytes: RAM, in MiB. - :raises ValueTooLowError: if``mebibytes`` is less than 2048 - :raises ValueTooHighError: if ``mebibytes`` is more than 8192 + 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") @@ -79,8 +88,11 @@ def validate_memory_amount(cls, mebibytes): def validate_serial_count(cls, count): """NX-OSv requires 1-2 serial ports. - :param int count: Number of serial ports. - :raises ValueTooLowError: if ``count`` is less than 1 - :raises ValueTooHighError: if ``count`` is more than 2 + 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") diff --git a/COT/platforms/generic.py b/COT/platforms/generic.py index d42ec79..aaad1bc 100644 --- a/COT/platforms/generic.py +++ b/COT/platforms/generic.py @@ -45,8 +45,11 @@ class GenericPlatform(object): def controller_type_for_device(cls, device_type): """Get the default controller type for the given device type. - :param str device_type: 'harddisk', 'cdrom', etc. - :return: 'ide' unless overridden by subclass. + Args: + device_type (str): 'harddisk', 'cdrom', etc. + + Returns: + str: 'ide' unless overridden by subclass. """ # For most platforms IDE is the correct default. return 'ide' @@ -57,8 +60,11 @@ def guess_nic_name(cls, nic_number): .. note:: This method counts from 1, not from 0! - :param int nic_number: Nth NIC to name. - :return: "Ethernet1", "Ethernet2", etc. unless overridden by subclass. + Args: + nic_number (int): Nth NIC to name. + + Returns: + str: "Ethernet1", "Ethernet2", etc. unless overridden by subclass. """ return "Ethernet" + str(nic_number) @@ -66,11 +72,14 @@ def guess_nic_name(cls, nic_number): def validate_cpu_count(cls, cpus): """Throw an error if the number of CPUs is not a supported value. - :param int cpus: Number of CPUs - :raises ValueTooLowError: if ``cpus`` is less than the minimum required - by this platform - :raises ValueTooHighError: if ``cpus`` exceeds the maximum supported - by this platform + 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") @@ -78,11 +87,14 @@ def validate_cpu_count(cls, cpus): def validate_memory_amount(cls, mebibytes): """Throw an error if the amount of RAM is not supported. - :param int mebibytes: RAM, in MiB. - :raises ValueTooLowError: if``mebibytes`` is less than the minimum - required by this platform - :raises ValueTooHighError: if ``mebibytes`` is more than the maximum - supported by this platform + 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") @@ -91,11 +103,14 @@ def validate_memory_amount(cls, mebibytes): def validate_nic_count(cls, count): """Throw an error if the number of NICs is not supported. - :param int count: Number of NICs. - :raises ValueTooLowError: if ``count`` is less than the minimum - required by this platform - :raises ValueTooHighError: if ``count`` is more than the maximum - supported by this platform + 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") @@ -107,9 +122,12 @@ def validate_nic_type(cls, type_string): - :func:`COT.data_validation.canonicalize_nic_subtype` - :data:`COT.data_validation.NIC_TYPES` - :param str type_string: See :data:`COT.data_validation.NIC_TYPES` - :raises ValueUnsupportedError: if ``type_string`` is not in - :const:`SUPPORTED_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, @@ -119,9 +137,12 @@ def validate_nic_type(cls, type_string): def validate_nic_types(cls, type_list): """Throw an error if any NIC type string in the list is unsupported. - :param list type_list: See :data:`COT.data_validation.NIC_TYPES` - :raises ValueUnsupportedError: if any value in ``type_list`` is not in - :const:`SUPPORTED_NIC_TYPES` + 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) @@ -130,10 +151,13 @@ def validate_nic_types(cls, type_list): def validate_serial_count(cls, count): """Throw an error if the number of serial ports is not supported. - :param int count: Number of serial ports. - :raises ValueTooLowError: if ``count`` is less than the minimum - required by this platform - :raises ValueTooHighError: if ``count`` is more than the maximum - supported by this platform + 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") From e466e9dadafc59012d17f75b54a8d5184fb95fc6 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 31 Oct 2016 16:30:29 -0400 Subject: [PATCH 44/59] Convert COT.disks docstrings to Google style --- COT/disks/__init__.py | 26 ++++++++++++++++---------- COT/disks/disk.py | 34 ++++++++++++++++++++++------------ COT/disks/iso.py | 22 +++++++++++++++------- COT/disks/qcow2.py | 11 +++++++---- COT/disks/raw.py | 11 +++++++---- COT/disks/vmdk.py | 13 ++++++++----- 6 files changed, 75 insertions(+), 42 deletions(-) diff --git a/COT/disks/__init__.py b/COT/disks/__init__.py index 503eb60..4d5b401 100644 --- a/COT/disks/__init__.py +++ b/COT/disks/__init__.py @@ -58,12 +58,14 @@ def convert_disk(disk_image, new_directory, new_format, new_subformat=None): """Convert a disk representation into a new format. - :param disk_image: Existing disk image as input. - :type disk_image: :class:`~COT.disks.disk.DiskRepresentation` or subclass. - :param str new_directory: Directory to create new image under - :param str new_format: Format to convert to. - :param str new_subformat: (optional) Sub-format to convert to. - :return: instance of :class:`~COT.disks.disk.DiskRepresentation` subclass. + 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}'" @@ -76,11 +78,13 @@ def convert_disk(disk_image, new_directory, new_format, new_subformat=None): def create_disk(disk_format, *args, **kwargs): """Create a disk of the requested format. - :param str disk_format: Disk format such as 'iso' or 'vmdk'. + Args: + disk_format (str): Disk format such as 'iso' or 'vmdk'. For the other parameters, see :class:`~COT.disks.disk.DiskRepresentation`. - :return: instance of :class:`~COT.disks.disk.DiskRepresentation` subclass. + Returns: + DiskRepresentation: Created disk """ if disk_format in _class_for_format: return _class_for_format[disk_format](*args, **kwargs) @@ -91,9 +95,11 @@ def create_disk(disk_format, *args, **kwargs): def disk_representation_from_file(file_path): """Get a DiskRepresentation appropriate to the given file. - :param str file_path: Path of existing file to represent. + Args: + file_path (str): Path of existing file to represent. - :return: instance of :class:`~COT.disks.disk.DiskRepresentation` subclass. + 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)) diff --git a/COT/disks/disk.py b/COT/disks/disk.py index e3a42d2..095eb73 100644 --- a/COT/disks/disk.py +++ b/COT/disks/disk.py @@ -33,11 +33,13 @@ def __init__(self, path, files=None): """Create a representation of an existing disk or create a new disk. - :param str path: Path to existing file or path to create new file at. - :param str disk_subformat: Subformat option(s) of the disk to create - (e.g., 'rockridge' for ISO, 'streamOptimized' for VMDK), if any. - :param int capacity: Capacity of disk to create - :param int files: Files to place in the filesystem of this disk. + 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. """ if not path: raise ValueError("Path must be set to a valid value, but got {0}" @@ -86,10 +88,13 @@ def files(self): def from_other_image(cls, input_image, output_dir, output_subformat=None): """Convert the other disk image into an image of this type. - :param DiskRepresentation input_image: Existing image representation. - :param str output_dir: Output directory to store the new image in. - :param str output_subformat: Any relevant subformat information. - :rtype: instance of DiskRepresentation or subclass + Args: + input_image (DiskRepresentation): Existing image representation. + output_dir (str): Output directory to store the new image in. + output_subformat (str): Any relevant subformat information. + + Raises: + NotImplementedError: Subclasses may implement this. """ raise NotImplementedError("Not a valid target for conversion") @@ -97,9 +102,14 @@ def from_other_image(cls, input_image, output_dir, output_subformat=None): def file_is_this_type(cls, path): """Check if the given file is image type represented by this class. - :param str path: Path to file to check. - :return: True (file matches this type) or False (file does not match) - :raises HelperError: if no file exists at ``path``. + Args: + path (str): Path to file to check. + + Returns: + bool: True (file matches this type) or False (file does not match) + + Raises: + HelperError: if no file exists at ``path``. """ if not os.path.exists(path): raise HelperError(2, "No such file or directory: '{0}'" diff --git a/COT/disks/iso.py b/COT/disks/iso.py index 85e8042..4cb04af 100644 --- a/COT/disks/iso.py +++ b/COT/disks/iso.py @@ -94,9 +94,14 @@ def _create_file(self): def file_is_this_type(cls, path): """Detect whether the given file is an ISO image. - :param str path: Path to file - :return: True (file is an ISO) or False (file is not an ISO) - :raises HelperError: if ``path`` is not a file at all. + Args: + path (str): Path to file + + Returns: + bool: True (file is an ISO) or False (file is not an ISO) + + Raises: + HelperError: if ``path`` is not a file at all. """ if not os.path.exists(path): raise HelperError(2, "No such file or directory: '{0}'" @@ -122,9 +127,12 @@ def file_is_this_type(cls, path): def from_other_image(cls, input_image, output_dir, output_subformat=None): """Convert the other disk image into an image of this type. - :param DiskRepresentation input_image: Existing image representation. - :param str output_dir: Output directory to store the new image in. - :param str output_subformat: Any relevant subformat information. - :rtype: instance of DiskRepresentation or subclass + Args: + input_image (DiskRepresentation): Existing image representation. + output_dir (str): Output directory to store the new image in. + output_subformat (str): Any relevant subformat information. + + Raises: + NotImplementedError: non-trivial to convert other types to ISO """ raise NotImplementedError("Not a valid target for conversion") diff --git a/COT/disks/qcow2.py b/COT/disks/qcow2.py index 78d0110..d726f5c 100644 --- a/COT/disks/qcow2.py +++ b/COT/disks/qcow2.py @@ -27,10 +27,13 @@ class QCOW2(DiskRepresentation): def from_other_image(cls, input_image, output_dir, output_subformat=None): """Convert the other disk image into an image of this type. - :param DiskRepresentation input_image: Existing image representation. - :param str output_dir: Output directory to store the new image in. - :param str output_subformat: Any relevant subformat information. - :rtype: instance of DiskRepresentation or subclass + Args: + input_image (DiskRepresentation): Existing image representation. + output_dir (str): Output directory to store the new image in. + output_subformat (str): Any relevant subformat information. + + Returns: + QCOW2: representation of newly created qcow2 image file """ file_name = os.path.basename(input_image.path) (file_prefix, _) = os.path.splitext(file_name) diff --git a/COT/disks/raw.py b/COT/disks/raw.py index e76e74b..5fb7f04 100644 --- a/COT/disks/raw.py +++ b/COT/disks/raw.py @@ -91,10 +91,13 @@ def _create_file(self): def from_other_image(cls, input_image, output_dir, output_subformat=None): """Convert the other disk image into an image of this type. - :param DiskRepresentation input_image: Existing image representation. - :param str output_dir: Output directory to store the new image in. - :param str output_subformat: Any relevant subformat information. - :rtype: instance of DiskRepresentation or subclass + Args: + input_image (DiskRepresentation): Existing image representation. + output_dir (str): Output directory to store the new image in. + output_subformat (str): Any relevant subformat information. + + Returns: + RAW: representation of newly created raw image. """ file_name = os.path.basename(input_image.path) file_prefix, _ = os.path.splitext(file_name) diff --git a/COT/disks/vmdk.py b/COT/disks/vmdk.py index 0f0d2b0..bc6ea73 100644 --- a/COT/disks/vmdk.py +++ b/COT/disks/vmdk.py @@ -52,11 +52,14 @@ def from_other_image(cls, input_image, output_dir, output_subformat="streamOptimized"): """Convert the other disk image into an image of this type. - :param DiskRepresentation input_image: Existing image representation. - :param str output_dir: Output directory to store the new image in. - :param str output_subformat: VMDK subformat string. - Defaults to "streamOptimized" if unset. - :rtype: :class:`~COT.disks.vmdk.VMDK` + Args: + input_image (DiskRepresentation): Existing image representation. + output_dir (str): Output directory to store the new image in. + output_subformat (str): VMDK subformat string. + Defaults to "streamOptimized" if unset. + + Returns: + VMDK: representation of newly created VMDK file. """ file_name = os.path.basename(input_image.path) (file_prefix, _) = os.path.splitext(file_name) From 98bcc2fdc2145f769963dca2c0f0abba08dd98e6 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 1 Nov 2016 09:34:54 -0400 Subject: [PATCH 45/59] Move COT.helpers to Google style docstrings --- COT/helpers/apt_get.py | 3 +- COT/helpers/helper.py | 150 ++++++++++++++++++++++++----------------- COT/helpers/port.py | 3 +- COT/helpers/yum.py | 3 +- 4 files changed, 96 insertions(+), 63 deletions(-) diff --git a/COT/helpers/apt_get.py b/COT/helpers/apt_get.py index 3949f09..29f134b 100644 --- a/COT/helpers/apt_get.py +++ b/COT/helpers/apt_get.py @@ -36,7 +36,8 @@ def __init__(self): def install_package(self, package): """Install the requested package if needed. - :param str package: Name of the package to install. + Args: + package (str): Name of the package to install. """ # Check whether it's already installed if re.search(r"install ok installed", diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 3aefaa8..60097fc 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -123,8 +123,9 @@ class HelperDict(dict): def __init__(self, factory, *args, **kwargs): """Create the given dictionary with the given factory class/method. - :param object factory: Factory class or method to be called to populate - a new entry in response to :meth:`__missing__`. + Args: + factory (object): Factory class or method to be called to populate + a new entry in response to :meth:`__missing__`. For the other parameters, see :class:`dict`. """ @@ -136,8 +137,11 @@ def __missing__(self, key): Automatically populate the given key with an instance of the factory. - :param object key: Key that was not yet defined in this dictionary. - :return: Result of calling ``self.factory(key)`` + Args: + key (object): Key that was not yet defined in this dictionary. + + Returns: + object: Result of calling ``self.factory(key)`` """ self[key] = self.factory(key) return self[key] @@ -181,12 +185,13 @@ def __init__(self, name, version_regexp="([0-9.]+)"): """Initializer. - :param str name: Name of helper executable - :param str info_uri: URI to refer to for more info about this helper. - :param list version_args: Args to pass to the helper to - get its version. Defaults to ``['--version']`` if unset. - :param str version_regexp: Regexp to get the version number from - the output of the command. + Args: + name (str): Name of helper executable + info_uri (str): URI to refer to for more info about this helper. + version_args (list): Args to pass to the helper to + get its version. Defaults to ``['--version']`` if unset. + version_regexp (str): Regexp to get the version number from + the output of the command. """ self._name = name self._info_uri = info_uri @@ -265,21 +270,23 @@ def call(self, args, capture_output=True, **kwargs): """Call the helper program with the given arguments. - :param list args: List of arguments to the helper program. - :param boolean capture_output: If ``True``, stdout/stderr will be - redirected to a buffer and returned, instead of being displayed - to the user. (I.e., :func:`check_output` will be invoked instead of - :func:`check_call`) - :return: Captured stdout/stderr (if :attr:`capture_output`), - else ``None``. + Args: + args (list): List of arguments to the helper program. + capture_output (boolean): If ``True``, stdout/stderr will be + redirected to a buffer and returned, instead of being displayed + to the user. (I.e., :func:`check_output` will be invoked + instead of :func:`check_call`) + + Returns: + str: Captured stdout/stderr if :attr:`capture_output` is True, + else ``None``. For the other parameters, see :func:`check_call` and :func:`check_output`. - :return: Captured stdout/stderr (if :attr:`capture_output`), - else ``None``. - :raises HelperNotFoundError: if the helper was not previously - installed, and the user declines to install it at this time. + Raises: + HelperNotFoundError: if the helper was not previously + installed, and the user declines to install it at this time. """ if not self.path: if self.USER_INTERFACE and not self.USER_INTERFACE.confirm( @@ -301,8 +308,9 @@ def call(self, args, def install(self): """Install the helper program. - :raise: :exc:`NotImplementedError` if not ``installable`` - :raise: :exc:`HelperError` if installation is attempted but fails. + Raises: + NotImplementedError: if not :attr:`installable` + HelperError: if installation is attempted but fails. Subclasses should not override this method but instead should provide an appropriate implementation of the :meth:`_install` method. @@ -357,7 +365,11 @@ def download_and_expand_tgz(url): ... # d is automatically cleaned up. - :param str url: URL of a .tgz or .tar.gz file to download. + Args: + url (str): URL of a .tgz or .tar.gz file to download. + + Yields: + str: Temporary directory path where the archive has been extracted. """ with TemporaryDirectory(prefix="cot_helper") as d: logger.debug("Temporary directory is %s", d) @@ -383,9 +395,10 @@ def download_and_expand_tgz(url): def mkdir(directory, permissions=493): # 493 == 0o755 """Check whether the given target directory exists, and create if not. - :param str directory: Directory to check/create. - :param int permissions: Permission mask to set when creating a - directory. Default is ``0o755``. + Args: + directory (str): Directory to check/create. + permissions (int): Permission mask to set when creating a + directory. Default is ``0o755``. """ if os.path.isdir(directory): # TODO: permissions check, update permissions if needed @@ -413,9 +426,15 @@ def mkdir(directory, permissions=493): # 493 == 0o755 def cp(src, dest): """Copy the given src to the given dest, using sudo if needed. - :param str src: Source path. - :param str dest: Destination path. - :return: True + Args: + src (str): Source path. + dest (str): Destination path. + + Returns: + bool: True + + Raises: + HelperError: if file copying fails """ logger.verbose("Copying %s to %s", src, dest) try: @@ -440,7 +459,8 @@ class PackageManager(Helper): def install_package(self, package): """Install the requested package if needed. - :param str package: Name of the package to install. + Args: + package (str): Name of the package to install. """ raise NotImplementedError("install_package not implemented!") @@ -452,20 +472,22 @@ def check_call(args, require_success=True, retry_with_sudo=False, **kwargs): or stderr; all output from the subprocess will be sent to the system stdout/stderr as normal. - :param list args: Command to invoke and its associated args - :param boolean require_success: If ``False``, do not raise an error - when the command exits with a return code other than 0 - :param boolean retry_with_sudo: If ``True``, if the command gets - an exception, prepend ``sudo`` to the command and try again. + Args: + args (list): Command to invoke and its associated args + require_success (boolean): If ``False``, do not raise an error + when the command exits with a return code other than 0 + retry_with_sudo (boolean): If ``True``, if the command gets + an exception, prepend ``sudo`` to the command and try again. For the other parameters, see :func:`subprocess.check_call`. - :raise HelperNotFoundError: if the command doesn't exist - (instead of a :class:`OSError`) - :raise HelperError: if :attr:`require_success` is not ``False`` and - the command returns a value other than 0 (instead of a - :class:`CalledProcessError`). - :raise OSError: as :func:`subprocess.check_call`. + Raises: + HelperNotFoundError: if the command doesn't exist (instead of a + :class:`OSError`) + HelperError: if :attr:`require_success` is not ``False`` and + the command returns a value other than 0 (instead of a + :class:`subprocess.CalledProcessError`). + OSError: as :func:`subprocess.check_call`. """ cmd = args[0] logger.info("Calling '%s'...", " ".join(args)) @@ -505,22 +527,25 @@ def check_output(args, require_success=True, retry_with_sudo=False, **kwargs): Automatically redirects stderr to stdout, captures both to a buffer, and generates a debug message with the stdout contents. - :param list args: Command to invoke and its associated args - :param boolean require_success: If ``False``, do not raise an error - when the command exits with a return code other than 0 - :param boolean retry_with_sudo: If ``True``, if the command gets - an exception, prepend ``sudo`` to the command and try again. + Args: + args (list): Command to invoke and its associated args + require_success (boolean): If ``False``, do not raise an error + when the command exits with a return code other than 0 + retry_with_sudo (boolean): If ``True``, if the command gets + an exception, prepend ``sudo`` to the command and try again. For the other parameters, see :func:`subprocess.check_output`. - :return: Captured stdout/stderr from the command + Returns: + str: Captured stdout/stderr from the command - :raise HelperNotFoundError: if the command doesn't exist - (instead of a :class:`OSError`) - :raise HelperError: if :attr:`require_success` is not ``False`` and - the command returns a value other than 0 (instead of a - :class:`CalledProcessError`). - :raise OSError: as :func:`subprocess.check_call`. + Raises: + HelperNotFoundError: if the command doesn't exist (instead of a + :class:`OSError`) + HelperError: if :attr:`require_success` is not ``False`` and + the command returns a value other than 0 (instead of a + :class:`subprocess.CalledProcessError`). + OSError: as :func:`subprocess.check_output`. """ cmd = args[0] logger.info("Calling '%s' and capturing its output...", " ".join(args)) @@ -558,13 +583,18 @@ def helper_select(choices): If no helper in the list is currently installed, will install the first installable helper from the list. - :raise HelperNotFoundError: if no valid helper is available or installable. + Raises: + HelperNotFoundError: if no valid helper is available or installable. + + Args: + choices (list): List of helpers, in order from most preferred to + least preferred. Each choice in this list can be either: + + * a string (the helper name, such as "mkisofs") + * a tuple of (name, minimum version) such as ("qemu-img", "2.1.0"). - :param list choices: List of helpers, in order from most preferred to - least preferred. Each choice in this list can be a string (the helper - name, such as "mkisofs") or a tuple of (name, minimum version) such as - ("qemu-img", "2.1.0"). - :return: The selected helper class instance. + Returns: + Helper: The selected helper class instance. """ for choice in choices: if isinstance(choice, str): diff --git a/COT/helpers/port.py b/COT/helpers/port.py index cc246ff..b73d072 100644 --- a/COT/helpers/port.py +++ b/COT/helpers/port.py @@ -38,7 +38,8 @@ def __init__(self): def install_package(self, package): """Install the requested package if needed. - :param str package: Name of the package to install. + Args: + package (str): Name of the package to install. """ # Check for updates if not Port._updated: diff --git a/COT/helpers/yum.py b/COT/helpers/yum.py index 8684e24..07ade22 100644 --- a/COT/helpers/yum.py +++ b/COT/helpers/yum.py @@ -33,7 +33,8 @@ def __init__(self): def install_package(self, package): """Install the requested package if needed. - :param str package: Name of the package to install. + Args: + package (str): Name of the package to install. """ self.call(['--quiet', 'install', package], capture_output=False, retry_with_sudo=True) From c688a70eb53c0d55af36647224e4cabfe48f1738 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 1 Nov 2016 09:35:03 -0400 Subject: [PATCH 46/59] Docstring fixup --- COT/ovf/ovf.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index d7f8449..962d182 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -643,7 +643,8 @@ def networks(self): def network_descriptions(self): """The list of network descriptions currently defined in this VM. - :rtype: list[str] + Returns: + list: List of network description strings """ if self.network_section is None: return [] @@ -2035,15 +2036,14 @@ def search_from_file_id(self, file_id): file_id (str): File ID to search from Returns: - ``(file, disk, ctrl_item, disk_item)``, any or all of which - may be ``None`` + tuple: ``(file, disk, ctrl_item, disk_item)``, any or all of which + may be ``None`` Raises: - LookupError: - * If the ``disk`` entry is found but no corresponding - ``file`` is found. - * If the ``disk_item`` is found but no ``ctrl_item`` - is found to be its parent. + LookupError: If the ``disk`` entry is found but no corresponding + ``file`` is found. + LookupError: If the ``disk_item`` is found but no ``ctrl_item`` + is found to be its parent. """ if file_id is None: return (None, None, None, None) From e7fafe9336f8c0edbf09655426207f8cc7a3879a Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 1 Nov 2016 09:57:04 -0400 Subject: [PATCH 47/59] Convert remaining docstrings to Google style --- COT/helpers/helper.py | 3 + COT/helpers/tests/test_helper.py | 12 ++-- COT/platforms/tests/test_cisco_iosv.py | 3 +- COT/tests/test_install_helpers.py | 15 +++-- COT/tests/ut.py | 79 +++++++++++++++++--------- 5 files changed, 75 insertions(+), 37 deletions(-) diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 60097fc..1bc3af9 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -97,6 +97,9 @@ def TemporaryDirectory(suffix='', # noqa: N802 Reimplementation of Python 3's ``tempfile.TemporaryDirectory``. For the parameters, see :class:`tempfile.TemporaryDirectory`. + + Yields: + str: Path to temporary directory """ tempdir = tempfile.mkdtemp(suffix, prefix, dirpath) try: diff --git a/COT/helpers/tests/test_helper.py b/COT/helpers/tests/test_helper.py index 5b7f9ac..10f7ee6 100644 --- a/COT/helpers/tests/test_helper.py +++ b/COT/helpers/tests/test_helper.py @@ -100,10 +100,11 @@ def assertAptUpdated(self): # noqa: N802 def apt_install_test(self, pkgname, helpername, *_): """Test installation with 'dpkg' and 'apt-get'. - :param str pkgname: Apt package to test installation for. - :param str helpername: Expected value of - :attr:`~COT.helpers.helper.Helper.name`, if different from - ``pkgname``. + Args: + pkgname (str): Apt package to test installation for. + helpername (str): Expected value of + :attr:`~COT.helpers.helper.Helper.name`, if different from + ``pkgname``. """ helpers['dpkg']._installed = True # Python 2.6 doesn't let us do multiple mocks in one 'with' @@ -140,7 +141,8 @@ def apt_install_test(self, pkgname, helpername, *_): def port_install_test(self, portname, *_): """Test installation with 'port'. - :param str portname: MacPorts package name to test. + Args: + portname (str): MacPorts package name to test. """ self.select_package_manager('port') Port._updated = False diff --git a/COT/platforms/tests/test_cisco_iosv.py b/COT/platforms/tests/test_cisco_iosv.py index c9459c6..74b0962 100644 --- a/COT/platforms/tests/test_cisco_iosv.py +++ b/COT/platforms/tests/test_cisco_iosv.py @@ -26,7 +26,8 @@ class NullHandler(logging.Handler): def emit(self, record): """Do nothing. - :param object record: Ignored. + Args: + record (object): Ignored. """ pass diff --git a/COT/tests/test_install_helpers.py b/COT/tests/test_install_helpers.py index fc21ce7..b064a6d 100644 --- a/COT/tests/test_install_helpers.py +++ b/COT/tests/test_install_helpers.py @@ -35,9 +35,12 @@ def stub_check_output(arg_list, *_args, **_kwargs): """Stub to ensure fixed version number strings. - :param list arg_list: arg_list[0] is script being called, - others are ignored. - :return: Canned output line, or "" + Args: + arg_list (list): arg_list[0] is script being called, + others are ignored. + + Returns: + str: Canned output line, or "" """ versions = { "fatdisk": "fatdisk, version 1.0.0-beta", @@ -55,8 +58,10 @@ def stub_check_output(arg_list, *_args, **_kwargs): def stub_dir_exists_but_not_file(path): """Stub for :func:`os.path.exists`. - :param str path: Path to check. - :return: True for man dir, False for man file. + Args: + path (str): Path to check. + Returns: + bool: True for man dir, False for man file. """ return os.path.basename(path) != "cot.1" diff --git a/COT/tests/ut.py b/COT/tests/ut.py index 5a5d454..8fd559f 100644 --- a/COT/tests/ut.py +++ b/COT/tests/ut.py @@ -39,7 +39,8 @@ class NullHandler(logging.Handler): def emit(self, record): """Do nothing. - :param LogRecord record: Record to ignore. + Args: + record (LogRecord): Record to ignore. """ pass @@ -73,8 +74,8 @@ class UTLoggingHandler(BufferingHandler): def __init__(self, testcase): """Create a logging handler for the given test case. - :param testcase: Test case owning this logging handler. - :type testcase: :class:`unittest.TestCase` + Args: + testcase (unittest.TestCase): Owner of this logging handler. """ BufferingHandler.__init__(self, capacity=0) self.setLevel(logging.DEBUG) @@ -83,23 +84,28 @@ def __init__(self, testcase): def emit(self, record): """Add the given log record to our internal buffer. - :param LogRecord record: Record to store. + Args: + record (LogRecord): Record to store. """ self.buffer.append(record.__dict__) def shouldFlush(self, record): # noqa: N802 """Return False - we only flush manually. - :param LogRecord record: Record to ignore. - :return: False + Args: + record (LogRecord): Record to ignore. + Returns: + bool: always False """ return False def logs(self, **kwargs): """Look for log entries matching the given dict. - :param kwargs: logging arguments to match against. - :return: List of record(s) that matched. + Args: + kwargs (dict): logging arguments to match against. + Returns: + list: List of record(s) that matched. """ matches = [] for record in self.buffer: @@ -125,8 +131,13 @@ def logs(self, **kwargs): def assertLogged(self, info='', **kwargs): # noqa: N802 """Fail unless the given log messages were each seen exactly once. - :param str info: Optional string to prepend to any failure messages. - :param kwargs: logging arguments to match against. + Args: + info (str): Optional string to prepend to any failure messages. + kwargs (dict): logging arguments to match against. + + Raises: + AssertionError: if an expected log message was not seen + AssertionError: if an expected log message was seen more than once """ matches = self.logs(**kwargs) if not matches: @@ -143,8 +154,11 @@ def assertLogged(self, info='', **kwargs): # noqa: N802 def assertNoLogsOver(self, max_level, info=''): # noqa: N802 """Fail if any logs are logged higher than the given level. - :param int max_level: Highest logging level to permit. - :param str info: Optional string to prepend to any failure messages. + Args: + max_level (int): Highest logging level to permit. + info (str): Optional string to prepend to any failure messages. + Raises: + AssertionError: if any messages higher than max_level were seen """ for level in (logging.CRITICAL, logging.ERROR, logging.WARNING, logging.INFO, logging.VERBOSE, logging.DEBUG): @@ -243,8 +257,10 @@ class COT_UT(unittest.TestCase): # noqa: N801 def localfile(name): """Get the absolute path to a local resource file. - :param str name: File name. - :return: Absolute file path. + Args: + name (str): File name. + Returns: + str: Absolute file path. """ return os.path.abspath(resource_filename(__name__, name)) @@ -252,10 +268,12 @@ def localfile(name): def invalid_hardware_warning(profile, value, kind): """Warning log message for invalid hardware. - :param str profile: Config profile, or "". - :param object value: Invalid value - :param str kind: Label for this hardware kind. - :return: dict of kwargs suitable for passing into :meth:`assertLogged` + Args: + profile (str): Config profile, or "". + value (object): Invalid value + kind (str): Label for this hardware kind. + Returns: + dict: kwargs suitable for passing into :meth:`assertLogged` """ msg = "" if profile: @@ -278,8 +296,8 @@ def __init__(self, method_name='runTest'): def set_vm_platform(self, plat): """Force the VM under test to use a particular Platform class. - :param plat: Platform class to use - :type plat: :class:`~COT.platforms.GenericPlatform` + Args: + plat (COT.platforms.GenericPlatform): Platform class to use """ # pylint: disable=protected-access self.instance.vm._platform = plat @@ -287,7 +305,11 @@ def set_vm_platform(self, plat): def check_cot_output(self, expected): """Grab the output from COT and check it against expected output. - :param str expected: Expected output + Args: + expected (str): Expected output + Raises: + AssertionError: if an error is raised by COT when run + AssertionError: if the output returned does not match expected. """ with mock.patch('sys.stdout', new_callable=StringIO.StringIO) as so: try: @@ -308,9 +330,13 @@ def check_diff(self, expected, file1=None, file2=None): running under Python 2.6, as it produces different XML output than later Python versions. - :param str expected: Expected diff output - :param str file1: File path to compare - :param str file2: File path to compare + Args: + expected (str): Expected diff output + file1 (str): File path to compare (default: input.ovf file) + file2 (str): File path to compare (default: output.ovf file) + + Raises: + AssertionError: if the two files do not have identical contents. """ if file1 is None: file1 = self.input_ovf @@ -428,8 +454,9 @@ def tearDown(self): def validate_with_ovftool(self, filename=None): """Use OVFtool to validate the given OVF/OVA file. - :param str filename: File name to validate (optional, default is - :attr:`temp_file`). + Args: + filename (str): File name to validate (optional, default is + :attr:`temp_file`). """ if filename is None: filename = self.temp_file From ae72b5423b9f148835aaa2d81e66514e886d4dad Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 1 Nov 2016 11:21:47 -0400 Subject: [PATCH 48/59] Changelog updates --- CHANGELOG.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3cfe322..76e31c4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,11 +6,17 @@ This project adheres to `Semantic Versioning`_. `Unreleased`_ ------------- +**Fixed** + +- Cisco CSR1000v platform now supports 8 CPUs as a valid option. + **Added** - ``cot inject-config --extra-files`` parameter (`#53`_). - Helper class for ``isoinfo`` (a companion to ``mkisofs``). - Added glossary of terms to COT documentation. +- Inline documentation (docstrings) are now validated using the `Pylint`_ + `docparams`_ extension. **Changed** @@ -21,6 +27,10 @@ This project adheres to `Semantic Versioning`_. (now just for handling helper programs such as ``apt-get`` and ``mkisofs``) and :mod:`COT.disks` (which uses the helpers to handle ISO/VMDK/QCOW2/RAW image files). +- Inline documentation (docstrings) have been converted to "`Google style`_" + for better readability in the code. Sphinx rendering of documentation + (for readthedocs.org, etc) now makes use of the `napoleon`_ extension to + handle this style. **Removed** @@ -104,7 +114,6 @@ under these Python versions. **Fixed** -- CSR1000v platform supports 8 CPUs as a valid option. - ``ValueMismatchError`` exceptions are properly caught by the CLI wrapper so as to result in a graceful exit rather than a stack trace. - ``cot remove-file`` now errors if the user specifies both file-id and @@ -113,6 +122,7 @@ under these Python versions. - Better handling of exceptions and usage of ``sudo`` when installing helpers. - Manual pages are now correctly included in the distribution. Oops! + `1.6.0`_ - 2016-06-30 --------------------- @@ -572,9 +582,12 @@ Initial public release. .. _pydocstyle: https://pypi.python.org/pypi/pydocstyle .. _`flake8-docstrings`: https://pypi.python.org/pypi/flake8-docstrings .. _Pylint: https://www.pylint.org/ +.. _docparams: https://docs.pylint.org/en/1.6.0/extensions.html#parameter-documentation-checker .. _`pep8-naming`: https://pypi.python.org/pypi/pep8-naming .. _mccabe: https://pypi.python.org/pypi/mccabe .. _Codecov: https://codecov.io +.. _`Google style`: https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments +.. _napoleon: http://www.sphinx-doc.org/en/latest/ext/napoleon.html .. _Unreleased: https://github.com/glennmatthews/cot/compare/master...develop .. _1.7.4: https://github.com/glennmatthews/cot/compare/v1.7.3...v1.7.4 From b56335d3b42fe01926468f490d03814d25f38290 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 1 Nov 2016 14:52:05 -0400 Subject: [PATCH 49/59] More compact docstrings --- COT/add_disk.py | 124 +++--- COT/add_file.py | 8 +- COT/cli.py | 94 ++--- COT/data_validation.py | 160 +++---- COT/deploy.py | 51 +-- COT/deploy_esxi.py | 46 +- COT/disks/__init__.py | 18 +- COT/disks/disk.py | 25 +- COT/disks/iso.py | 14 +- COT/disks/qcow2.py | 8 +- COT/disks/raw.py | 8 +- COT/disks/vmdk.py | 10 +- COT/edit_hardware.py | 18 +- COT/edit_product.py | 6 +- COT/edit_properties.py | 8 +- COT/file_reference.py | 52 +-- COT/help.py | 2 +- COT/helpers/apt_get.py | 2 +- COT/helpers/helper.py | 122 +++--- COT/helpers/port.py | 2 +- COT/helpers/tests/test_helper.py | 10 +- COT/helpers/yum.py | 2 +- COT/info.py | 6 +- COT/inject_config.py | 34 +- COT/install_helpers.py | 26 +- COT/ovf/hardware.py | 134 +++--- COT/ovf/item.py | 148 +++---- COT/ovf/name_helper.py | 30 +- COT/ovf/ovf.py | 561 ++++++++++++------------- COT/platforms/__init__.py | 8 +- COT/platforms/cisco_csr1000v.py | 41 +- COT/platforms/cisco_iosv.py | 33 +- COT/platforms/cisco_iosxrv.py | 43 +- COT/platforms/cisco_iosxrv_9000.py | 31 +- COT/platforms/cisco_nxosv.py | 35 +- COT/platforms/generic.py | 56 +-- COT/platforms/tests/test_cisco_iosv.py | 2 +- COT/remove_file.py | 6 +- COT/submodule.py | 20 +- COT/tests/test_install_helpers.py | 9 +- COT/tests/ut.py | 60 +-- COT/ui_shared.py | 52 +-- COT/vm_description.py | 292 +++++++------ COT/vm_factory.py | 12 +- COT/xml_file.py | 106 +++-- 45 files changed, 1254 insertions(+), 1281 deletions(-) diff --git a/COT/add_disk.py b/COT/add_disk.py index 1f6237e..dad4de1 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -54,11 +54,11 @@ def validate_controller_address(controller, address): Helper method for the :attr:`controller`/:attr:`address` setters. Args: - controller (str): ``'ide'`` or ``'scsi'`` - address (str): A string like '0:0' or '2:10' + controller (str): ``'ide'`` or ``'scsi'`` + address (str): A string like '0:0' or '2:10' Raises: - InvalidInputError: if the address/controller combo is invalid. + InvalidInputError: if the address/controller combo is invalid. """ logger.info("validate_controller_address: %s, %s", controller, address) if controller is not None and address is not None: @@ -98,7 +98,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTAddDisk, self).__init__(ui) self._disk_image = None @@ -120,7 +120,7 @@ def disk_image(self): """Disk image file to add to the VM. Raises: - InvalidInputError: if the file does not exist. + InvalidInputError: if the file does not exist. """ return self._disk_image @@ -133,7 +133,7 @@ def address(self): """Disk device address on controller (``1:0``, etc.). Raises: - InvalidInputError: see :meth:`validate_controller_address` + InvalidInputError: see :meth:`validate_controller_address` """ return self._address @@ -148,7 +148,7 @@ def controller(self): """Disk controller type (``ide``, ``scsi``). Raises: - InvalidInputError: see :meth:`validate_controller_address` + InvalidInputError: see :meth:`validate_controller_address` """ return self._controller @@ -162,7 +162,7 @@ def ready_to_run(self): """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.disk_image is None: return False, "DISK_IMAGE is a mandatory argument!" @@ -175,7 +175,7 @@ def run(self): """Do the actual work of this submodule. Raises: - InvalidInputError: if :meth:`ready_to_run` reports ``False`` + InvalidInputError: if :meth:`ready_to_run` reports ``False`` """ super(COTAddDisk, self).run() @@ -264,11 +264,11 @@ def guess_drive_type_from_extension(disk_file_name): """Guess the disk type (harddisk/cdrom) from the disk file name. Args: - disk_file_name (str): File name or file path. + disk_file_name (str): File name or file path. Returns: - str: "cdrom" or "harddisk" + str: "cdrom" or "harddisk" Raises: - InvalidInputError: if the disk type cannot be guessed. + InvalidInputError: if the disk type cannot be guessed. """ disk_extension = os.path.splitext(disk_file_name)[1] ext_type_map = { @@ -315,17 +315,17 @@ def search_for_elements(vm, disk_file, file_id, controller, address): all relevant approaches agree on what sections we're talking about... Args: - vm (VMDescription): Virtual machine object - disk_file (str): Disk file name or path - file_id (str): File identifier - controller (str): controller type, "ide" or "scsi" - address (str): device address, such as "1:0" + vm (VMDescription): Virtual machine object + disk_file (str): Disk file name or path + file_id (str): File identifier + controller (str): controller type, "ide" or "scsi" + address (str): device address, such as "1:0" Raises: - ValueMismatchError: if the criteria select a non-unique set. + ValueMismatchError: if the criteria select a non-unique set. Returns: - tuple: (file_object, disk_object, controller_item, disk_item) + tuple: (file_object, disk_object, controller_item, disk_item) """ # 1) Check whether the DISK_IMAGE file name matches an existing File # in the OVF (and from there, find the associated Disk and Items) @@ -359,14 +359,14 @@ def guess_controller_type(vm, ctrl_item, drive_type): """If a controller type wasn't specified, try to guess from context. Args: - vm (VMDescription): Virtual machine object - ctrl_item (object): Any known controller object - drive_type (str): "cdrom" or "harddisk" + vm (VMDescription): Virtual machine object + ctrl_item (object): Any known controller object + drive_type (str): "cdrom" or "harddisk" Returns: - str: 'ide' or 'scsi' + str: 'ide' or 'scsi' Raises: - ValueUnsupportedError: if ``ctrl_item`` is not an IDE or SCSI - controller device. + ValueUnsupportedError: if ``ctrl_item`` is not an IDE or SCSI + controller device. """ if ctrl_item is None: # If the user didn't tell us which controller type they wanted, @@ -393,16 +393,16 @@ def validate_elements(vm, file_obj, disk_obj, disk_item, ctrl_item, """Validate any existing file, disk, controller item, and disk item. Raises: - ValueMismatchError: if the search criteria select a non-unique set. + ValueMismatchError: if the search criteria select a non-unique set. Args: - vm (VMDescription): Virtual machine object - file_obj (object): Known file object - disk_obj (object): Known disk object - disk_item (object): Known disk device object - ctrl_item (object): Known controller device object - file_id (str): File identifier string - ctrl_type (str): Controller type ("ide" or "scsi") + vm (VMDescription): Virtual machine object + file_obj (object): Known file object + disk_obj (object): Known disk object + disk_item (object): Known disk device object + ctrl_item (object): Known controller device object + file_id (str): File identifier string + ctrl_type (str): Controller type ("ide" or "scsi") """ # Ok, we now have confirmed that we have at most one of each of these # four objects. Now it's time for some sanity checking... @@ -443,16 +443,16 @@ def confirm_elements(vm, ui, file_obj, disk_image, disk_obj, disk_item, """Get user confirmation of any risky or unusual operations. Args: - vm (VMDescription): Virtual machine object - ui (UI): User interface object - file_obj (object): Known file object - disk_image (str): Filename or path for disk file - disk_obj (object): Known disk object - disk_item (object): Known disk device object - drive_type (str): "harddisk" or "cdrom" - controller (str): Controller type ("ide" or "scsi") - ctrl_item (object): Known controller device object - subtype (str): Controller subtype (such as "virtio") + vm (VMDescription): Virtual machine object + ui (UI): User interface object + file_obj (object): Known file object + disk_image (str): Filename or path for disk file + disk_obj (object): Known disk object + disk_item (object): Known disk device object + drive_type (str): "harddisk" or "cdrom" + controller (str): Controller type ("ide" or "scsi") + ctrl_item (object): Known controller device object + subtype (str): Controller subtype (such as "virtio") """ # TODO: more refactoring! if file_obj is not None: @@ -507,25 +507,25 @@ def add_disk_worker(vm, and will be automatically determined by COT if unspecified. Args: - vm (VMDescription): The virtual machine being edited. - ui (UI): User interface in effect. - disk_image (DiskRepresentation): Disk image to add to the VM. - drive_type (str): Disk drive type: ``'cdrom'`` or ``'harddisk'``. - If not specified, will be derived automatically from the - disk_image file name extension. - file_id (str): Identifier of the disk file in the VM. If not - specified, the VM will automatically derive an appropriate value. - controller (str): Disk controller type: ``'ide'`` or ``'scsi'``. - If not specified, will be derived from the `type` and the - `platform` of the given `vm`. - subtype (str): Controller subtype ('virtio', 'lsilogic', etc.) - address (str): Disk device address on its controller - (such as ``'1:0'``). If this matches an existing disk device, - that device will be overwritten. If not specified, the first - available address not already occupied by an existing device - will be selected. - diskname (str): Name for disk device - description (str): Description of disk device + vm (VMDescription): The virtual machine being edited. + ui (UI): User interface in effect. + disk_image (DiskRepresentation): Disk image to add to the VM. + drive_type (str): Disk drive type: ``'cdrom'`` or ``'harddisk'``. + If not specified, will be derived automatically from the + disk_image file name extension. + file_id (str): Identifier of the disk file in the VM. If not + specified, the VM will automatically derive an appropriate value. + controller (str): Disk controller type: ``'ide'`` or ``'scsi'``. + If not specified, will be derived from the `type` and the + `platform` of the given `vm`. + subtype (str): Controller subtype ('virtio', 'lsilogic', etc.) + address (str): Disk device address on its controller + (such as ``'1:0'``). If this matches an existing disk device, + that device will be overwritten. If not specified, the first + available address not already occupied by an existing device + will be selected. + diskname (str): Name for disk device + description (str): Description of disk device """ if drive_type is None: drive_type = guess_drive_type_from_extension(disk_image.path) diff --git a/COT/add_file.py b/COT/add_file.py index 15a7c85..75d826d 100644 --- a/COT/add_file.py +++ b/COT/add_file.py @@ -46,7 +46,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTAddFile, self).__init__(ui) self._file = None @@ -58,7 +58,7 @@ def file(self): """File to be added to the package. Raises: - InvalidInputError: if the file does not exist. + InvalidInputError: if the file does not exist. """ return self._file @@ -73,7 +73,7 @@ def ready_to_run(self): """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.file is None: return False, "FILE is a mandatory argument!" @@ -83,7 +83,7 @@ def run(self): """Do the actual work of this submodule. Raises: - InvalidInputError: if :func:`ready_to_run` reports ``False`` + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTAddFile, self).run() diff --git a/COT/cli.py b/COT/cli.py index 38859fb..51e2d33 100644 --- a/COT/cli.py +++ b/COT/cli.py @@ -69,10 +69,10 @@ def formatter(verbosity=logging.INFO): hence this need. Args: - verbosity (int): Logging level as defined by :mod:`logging`. + verbosity (int): Logging level as defined by :mod:`logging`. Returns: - colorlog.ColoredFormatter: Formatter object to use with :mod:`logging`. + colorlog.ColoredFormatter: Formatter object to use with :mod:`logging`. """ from colorlog import ColoredFormatter log_colors = { @@ -121,8 +121,8 @@ def __init__(self, terminal_width=None): """Create CLI handler instance. Args: - terminal_width (int): (optional) Set the terminal width for this - CLI, independent of the actual terminal in use. + terminal_width (int): (optional) Set the terminal width for this + CLI, independent of the actual terminal in use. """ super(CLI, self).__init__(force=True) # In python 2.7, we want raw_input, but in python 3 we want input. @@ -179,11 +179,11 @@ def fill_usage(self, subcommand, usage_list): [-f FILE_ID] Args: - subcommand (str): Subcommand name/keyword - usage_list (list): List of usage strings for this subcommand. + subcommand (str): Subcommand name/keyword + usage_list (list): List of usage strings for this subcommand. Returns: - string: All usage strings, each appropriately wrapped to the - :func:`terminal_width` value. + string: All usage strings, each appropriately wrapped to the + :func:`terminal_width` value. """ # Automatically add a line for --help to the usage output_lines = ["\n cot "+subcommand+" --help"] @@ -265,12 +265,12 @@ def fill_examples(self, example_list): cot deploy foo.ova esxi 192.0.2.100 -u admin -c 1CPU-2.5GB Args: - example_list (list): List of (description, CLI example) tuples. + example_list (list): List of (description, CLI example) tuples. Returns: - str: Examples wrapped appropriately to the :func:`terminal_width` - value. CLI examples will be wrapped with backslashes and - a hanging indent. + str: Concatenation of examples, each wrapped appropriately to the + :func:`terminal_width` value. CLI examples will be wrapped with + backslashes and a hanging indent. """ output_lines = ["Examples:"] # Just as in fill_usage, the default textwrap behavior @@ -314,7 +314,7 @@ def set_verbosity(self, level): with logging. Args: - level (int): Logging level as defined by :mod:`logging` + level (int): Logging level as defined by :mod:`logging` """ if not self.handler: self.handler = logging.StreamHandler() @@ -331,9 +331,9 @@ def run(self, argv): Calls :func:`parse_args` followed by :func:`main`. Args: - argv (list): The CLI argv value (not including argv[0]) + argv (list): The CLI argv value (not including argv[0]) Returns: - Return code from :func:`main` + int: Return code from :func:`main` """ args = self.parse_args(argv) return self.main(args) @@ -344,9 +344,9 @@ def confirm(self, prompt): Auto-accepts if :attr:`force` is set to ``True``. Args: - prompt (str): Message to prompt the user with + prompt (str): Message to prompt the user with Returns: - bool: ``True`` (user accepts) or ``False`` (user declines) + bool: ``True`` (user accepts) or ``False`` (user declines) """ if self.force: logger.warning("Automatically agreeing to '%s'", prompt) @@ -378,12 +378,12 @@ def get_input(self, prompt, default_value): ``True``. Args: - prompt (str): Message to prompt the user with - default_value (str): Default value to input if the user simply - hits Enter without entering a value, or if :attr:`force`. + prompt (str): Message to prompt the user with + default_value (str): Default value to input if the user simply + hits Enter without entering a value, or if :attr:`force`. Returns: - str: Input value + str: Input value """ if self.force: logger.warning("Automatically entering '%s' in response to '%s'", @@ -399,14 +399,14 @@ def get_password(self, username, host): """Get password string from the user. Args: - username (str): Username the password is associated with - host (str): Host the password is associated with + username (str): Username the password is associated with + host (str): Host the password is associated with Raises: - InvalidInputError: if :attr:`force` is ``True`` - (as there is no "default" password value) + InvalidInputError: if :attr:`force` is ``True`` + (as there is no "default" password value) Returns: - str: Password string + str: Password string """ if self.force: raise InvalidInputError("No password specified for {0}@{1}" @@ -514,16 +514,16 @@ def add_subparser(self, title, """Create a subparser under the specified parent. Args: - title (str): Canonical keyword for this subparser - parent (object): Subparser grouping object returned by - :meth:`ArgumentParser.add_subparsers` - aliases (list): Aliases for ``title``. Only used in Python 3.x. - lookup_prefix (str): String to prepend to ``title`` and - each alias in ``aliases`` for lookup purposes. - kwargs (dict): Passed through to :meth:`parent.add_parser` + title (str): Canonical keyword for this subparser + parent (object): Subparser grouping object returned by + :meth:`ArgumentParser.add_subparsers` + aliases (list): Aliases for ``title``. Only used in Python 3.x. + lookup_prefix (str): String to prepend to ``title`` and + each alias in ``aliases`` for lookup purposes. + kwargs (dict): Passed through to :meth:`parent.add_parser` Returns: - object: Subparser object + object: Subparser object """ # Subparser aliases are only supported by argparse in Python 3.2+ if sys.hexversion >= 0x03020000 and aliases: @@ -545,9 +545,9 @@ def parse_args(self, argv): """Parse the given CLI arguments into a namespace object. Args: - argv (list): List of CLI arguments, not including argv0 + argv (list): List of CLI arguments, not including argv0 Returns: - argparse.Namespace: Parser namespace object + argparse.Namespace: Parser namespace object """ # Parse the user input args = self.parser.parse_args(argv) @@ -564,9 +564,9 @@ def args_to_dict(args): """Convert args to a dict and perform any needed cleanup. Args: - args (argparse.Namespace): Namespace from :meth:`parse_args`. + args (argparse.Namespace): Namespace from :meth:`parse_args`. Returns: - dict: Dictionary of arg to value + dict: Dictionary of arg to value """ arg_dict = vars(args) del arg_dict["_verbosity"] @@ -588,9 +588,9 @@ def set_instance_attributes(arg_dict): """Set attributes of the :attr:`instance` based on the given arg_dict. Args: - arg_dict (dict): Dictionary of (attribute, value). + arg_dict (dict): Dictionary of (attribute, value). Raises: - InvalidInputError: if attributes are not validly set. + InvalidInputError: if attributes are not validly set. """ # Set mandatory (CAPITALIZED) args first, then optional args for (arg, value) in arg_dict.items(): @@ -617,16 +617,16 @@ def main(self, args): * Catches various exceptions and handles them appropriately. Args: - args (argparse.Namespace): Parser namespace object returned from - :func:`parse_args`. + args (argparse.Namespace): Parser namespace object returned from + :func:`parse_args`. Returns: - int: Exit code for the COT executable. + int: Exit code for the COT executable. - * 0 on successful completion - * 1 on runtime error - * 2 on input error (parser error, - :class:`~COT.data_validation.InvalidInputError`, etc.) + * 0 on successful completion + * 1 on runtime error + * 2 on input error (parser error, + :class:`~COT.data_validation.InvalidInputError`, etc.) """ # pylint: disable=protected-access self.force = args._force diff --git a/COT/data_validation.py b/COT/data_validation.py index 6d69fc8..221a7c2 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -66,9 +66,9 @@ def to_string(obj): """Get string representation of an object, special-case for XML Element. Args: - obj (object): Object to represent as a string. + obj (object): Object to represent as a string. Returns: - str: string representation + str: string representation """ if ET.iselement(obj): return ET.tostring(obj) @@ -80,17 +80,17 @@ def alphanum_split(key): """Split the key into a list of [text, int, text, int, ...]. Args: - key (str): String to split. + key (str): String to split. Returns: - List of tokens + list: List of tokens """ def text_to_int(text): """Convert number strings to ints, leave other strings as text. Args: - text (object): Input to convert (str or int) + text (object): Input to convert (str or int) Returns: - object: Converted value (str or int) + object: Converted value (str or int) """ return int(text) if text.isdigit() else text @@ -105,9 +105,9 @@ def natural_sort(l): See also http://nedbatchelder.com/blog/200712/human_sorting.html Args: - l (list): List to sort + l (list): List to sort Returns: - list: Sorted list + list: Sorted list """ # Sort based on alphanum_split return value return sorted(l, key=alphanum_split) @@ -117,12 +117,12 @@ def match_or_die(first_label, first, second_label, second): """Make sure "first" and "second" are equal or raise an error. Args: - first_label (str): Descriptive label for :attr:`first` - first (object): First object to compare - second_label (str): Descriptive label for :attr:`second` - second (object): Second object to compare + first_label (str): Descriptive label for :attr:`first` + first (object): First object to compare + second_label (str): Descriptive label for :attr:`second` + second (object): Second object to compare Raises: - ValueMismatchError: if ``first != second`` + ValueMismatchError: if ``first != second`` """ if first != second: raise ValueMismatchError("{0} {1} does not match {2} {3}" @@ -136,15 +136,15 @@ def canonicalize_helper(label, user_input, mappings, re_flags=0): """Try to find a mapping of input to output. Args: - label (str): Label to use in any error raised - user_input (str): User-provided string - mappings (list): List of ``(expr, canonical)`` pairs for mapping. - re_flags (int): ``re.IGNORECASE``, etc. if desired + label (str): Label to use in any error raised + user_input (str): User-provided string + mappings (list): List of ``(expr, canonical)`` pairs for mapping. + re_flags (int): ``re.IGNORECASE``, etc. if desired Returns: - str: The canonical string + str: The canonical string Raises: - ValueUnsupportedError: If no ``expr`` in ``mappings`` matches the given - ``user_input``. + ValueUnsupportedError: If no ``expr`` in ``mappings`` matches the given + ``user_input``. """ if user_input is None or user_input == "": return None @@ -158,15 +158,15 @@ def canonicalize_ide_subtype(subtype): """Try to convert the given IDE controller string to a canonical form. Args: - subtype (str): User-provided string + subtype (str): User-provided string Returns: - str: The canonical string, one of: + str: The canonical string, one of: - - ``PIIX4`` - - ``virtio`` + - ``PIIX4`` + - ``virtio`` Raises: - ValueUnsupportedError: If the canonical string cannot be determined + ValueUnsupportedError: If the canonical string cannot be determined """ return canonicalize_helper("IDE controller subtype", subtype, [ @@ -191,11 +191,11 @@ def canonicalize_nic_subtype(subtype): """Try to convert the given NIC subtype string to a canonical form. Args: - subtype (str): User-provided string + subtype (str): User-provided string Returns: - str: The canonical string, one of :data:`NIC_TYPES` + str: The canonical string, one of :data:`NIC_TYPES` Raises: - ValueUnsupportedError: If the canonical string cannot be determined + ValueUnsupportedError: If the canonical string cannot be determined .. seealso:: :meth:`COT.platforms.GenericPlatform.validate_nic_type` @@ -208,18 +208,18 @@ def canonicalize_scsi_subtype(subtype): """Try to convert the given SCSI controller string to a canonical form. Args: - subtype (str): User-provided string + subtype (str): User-provided string Returns: - str: The canonical string, one of: + str: The canonical string, one of: - - ``buslogic`` - - ``lsilogic`` - - ``lsilogicsas`` - - ``virtio`` - - ``VirtualSCSI`` + - ``buslogic`` + - ``lsilogic`` + - ``lsilogicsas`` + - ``virtio`` + - ``VirtualSCSI`` Raises: - ValueUnsupportedError: If the canonical string cannot be determined + ValueUnsupportedError: If the canonical string cannot be determined """ return canonicalize_helper("SCSI controller subtype", subtype, [ @@ -236,12 +236,12 @@ def check_for_conflict(label, li): """Make sure the list does not contain references to more than one object. Args: - label (str): Descriptive label to be used if an error is raised - li (list): List of object references (which may include ``None``) + label (str): Descriptive label to be used if an error is raised + li (list): List of object references (which may include ``None``) Raises: - ValueMismatchError: if references differ + ValueMismatchError: if references differ Returns: - object: the object or ``None`` + object: the object or ``None`` """ obj = None for i, obj1 in enumerate(li): @@ -262,10 +262,10 @@ def file_checksum(path_or_obj, checksum_type): """Get the checksum of the given file. Args: - path_or_obj (str): File path to checksum OR an opened file object - checksum_type (str): Supported values are 'md5' and 'sha1'. + path_or_obj (str): File path to checksum OR an opened file object + checksum_type (str): Supported values are 'md5' and 'sha1'. Returns: - str: Hexadecimal file checksum + str: Hexadecimal file checksum """ # pylint: disable=redefined-variable-type if checksum_type == 'md5': @@ -310,11 +310,11 @@ def mac_address(string): * xxxx.xxxx.xxxx Args: - string (str): String to validate + string (str): String to validate Raises: - InvalidInputError: if string is not a valid MAC address + InvalidInputError: if string is not a valid MAC address Returns: - str: Validated string(with leading/trailing whitespace stripped) + str: Validated string(with leading/trailing whitespace stripped) """ string = string.strip() if not (re.match(r"([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$", string) or @@ -332,11 +332,11 @@ def device_address(string): Validate string is an appropriately formed device address such as '1:0'. Args: - string (str): String to validate + string (str): String to validate Raises: - InvalidInputError: if string is not a well-formatted device address + InvalidInputError: if string is not a well-formatted device address Returns: - str: Validated string (with leading/trailing whitespace stripped) + str: Validated string (with leading/trailing whitespace stripped) """ string = string.strip() if not re.match(r"\d+:\d+$", string): @@ -349,11 +349,11 @@ def no_whitespace(string): """Parser helper function for arguments not allowed to contain whitespace. Args: - string (str): String to validate + string (str): String to validate Raises: - InvalidInputError: if string contains internal whitespace + InvalidInputError: if string contains internal whitespace Returns: - str: Validated string (with leading/trailing whitespace stripped) + str: Validated string (with leading/trailing whitespace stripped) """ string = string.strip() if len(string.split()) > 1: @@ -368,18 +368,18 @@ def validate_int(string, """Parser helper function for validating integer arguments in a range. Args: - string (str): String to convert to an integer and validate - minimum (int): Minimum valid value (optional) - maximum (int): Maximum valid value (optional) - label (str): Label to include in any errors raised + string (str): String to convert to an integer and validate + minimum (int): Minimum valid value (optional) + maximum (int): Maximum valid value (optional) + label (str): Label to include in any errors raised Returns: - int: Validated integer value + int: Validated integer value Raises: - ValueUnsupportedError: if :attr:`string` can't be converted to int - ValueTooLowError: if value is less than :attr:`minimum` - ValueTooHighError: if value is more than :attr:`maximum` + ValueUnsupportedError: if :attr:`string` can't be converted to int + ValueTooLowError: if value is less than :attr:`minimum` + ValueTooHighError: if value is more than :attr:`maximum` """ try: i = int(string) @@ -398,12 +398,12 @@ def non_negative_int(string): Alias for :func:`validate_int` setting :attr:`minimum` to 0. Args: - string (str): String to validate. + string (str): String to validate. Returns: - int: Validated integer value + int: Validated integer value Raises: - ValueUnsupportedError: if :attr:`string` can't be converted to int - ValueTooLowError: if value is less than 0 + ValueUnsupportedError: if :attr:`string` can't be converted to int + ValueTooLowError: if value is less than 0 """ return validate_int(string, minimum=0) @@ -414,12 +414,12 @@ def positive_int(string): Alias for :func:`validate_int` setting :attr:`minimum` to 1. Args: - string (str): String to validate. + string (str): String to validate. Returns: - int: Validated integer value + int: Validated integer value Raises: - ValueUnsupportedError: if :attr:`string` can't be converted to int - ValueTooLowError: if value is less than 1 + ValueUnsupportedError: if :attr:`string` can't be converted to int + ValueTooLowError: if value is less than 1 """ return validate_int(string, minimum=1) @@ -430,11 +430,11 @@ def truth_value(value): Wrapper for :func:`distutils.util.strtobool` Args: - value (str): String to parse/validate + value (str): String to parse/validate Returns: - bool: True or False + bool: True or False Raises: - ValueUnsupportedError: if the value can't be parsed to a boolean. + ValueUnsupportedError: if the value can't be parsed to a boolean. """ if isinstance(value, bool): return value @@ -466,9 +466,9 @@ class ValueUnsupportedError(InvalidInputError): """An unsupported value was provided. Args: - value_type (str): descriptive string - actual_value (str): invalid value that was provided - expected_value (object): expected/valid value(s) (item or list) + value_type (str): descriptive string + actual_value (str): invalid value that was provided + expected_value (object): expected/valid value(s) (item or list) """ def __init__(self, value_type, actual_value, expected_value): @@ -489,9 +489,9 @@ class ValueTooLowError(ValueUnsupportedError): """A numerical input was less than the lowest supported value. Args: - value_type (str): descriptive string - actual_value (int): invalid value that was provided - expected_value (int): minimum supported value + value_type (str): descriptive string + actual_value (int): invalid value that was provided + expected_value (int): minimum supported value """ def __str__(self): @@ -505,9 +505,9 @@ class ValueTooHighError(ValueUnsupportedError): """A numerical input was higher than the highest supported value. Args: - value_type (str): descriptive string - actual_value (int): invalid value that was provided - expected_value (int): maximum supported value + value_type (str): descriptive string + actual_value (int): invalid value that was provided + expected_value (int): maximum supported value """ def __str__(self): diff --git a/COT/deploy.py b/COT/deploy.py index 3e65d9a..bb9c20b 100644 --- a/COT/deploy.py +++ b/COT/deploy.py @@ -42,7 +42,7 @@ def from_cli_string(cls, cli_string): Based on the QEMU CLI for serial ports. Args: - cli_string (str): String of the form 'kind:value[,opts]' + cli_string (str): String of the form 'kind:value[,opts]' :: @@ -54,9 +54,9 @@ def from_cli_string(cls, cli_string): '' Returns: - SerialConnection: Created instance or None. + SerialConnection: Created instance or None. Raises: - InvalidInputError: if ``cli_string`` cannot be parsed + InvalidInputError: if ``cli_string`` cannot be parsed """ if cli_string is None: return None @@ -88,11 +88,11 @@ def validate_kind(cls, kind): """Validate the connection type string and munge it as needed. Args: - kind (str): Connection type string, possibly in need of munging. + kind (str): Connection type string, possibly in need of munging. Returns: - str: A valid type string + str: A valid type string Raises: - ValueUnsupportedError: if ``kind`` is not recognized as valid + ValueUnsupportedError: if ``kind`` is not recognized as valid """ kind = kind.lower() if kind == '': @@ -114,13 +114,13 @@ def validate_value(cls, kind, value): """Check that the given value is valid for the given connection kind. Args: - kind (str): Connection type, valid per :func:`validate_kind`. - value (str): Connection value such as '/dev/ttyS0' or '1.1.1.1:80' + kind (str): Connection type, valid per :func:`validate_kind`. + value (str): Connection value such as '/dev/ttyS0' or '1.1.1.1:80' Returns: - str: Munged value string. + str: Munged value string. Raises: - InvalidInputError: if value string is not recognized as valid - NotImplementedError: if ``kind`` is not valid + InvalidInputError: if value string is not recognized as valid + NotImplementedError: if ``kind`` is not valid """ if kind == 'device' or kind == 'file' or kind == 'pipe': # TODO: Validate that device path exists on target? @@ -143,17 +143,20 @@ def validate_value(cls, kind, value): .format(kind)) @classmethod - def validate_options(cls, kind, _value, options): + def validate_options(cls, + kind, + value, # pylint: disable=unused-argument + options): """Check that the given set of options are valid for this connection. Args: - kind (str): Validated 'kind' string. - _value (str): Validated 'value' string. Currently unused. - options (dict): Input options dictionary. + kind (str): Validated 'kind' string. + value (str): Validated 'value' string. Currently unused. + options (dict): Input options dictionary. Returns: - dict: Validated options + dict: Validated options Raises: - InvalidInputError: if options are not valid. + InvalidInputError: if options are not valid. """ if kind == 'file': if 'datastore' not in options: @@ -165,9 +168,9 @@ def __init__(self, kind, value, options): """Construct a SerialConnection object of the given kind and value. Args: - kind (str): Connection type string, possibly in need of munging. - value (str): Connection value such as '/dev/ttyS0' or '1.1.1.1:80' - options (dict): Input options dictionary. + kind (str): Connection type string, possibly in need of munging. + value (str): Connection value such as '/dev/ttyS0' or '1.1.1.1:80' + options (dict): Input options dictionary. """ logger.debug("Creating SerialConnection: " "kind: %s, value: %s, options: %s", @@ -212,7 +215,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTDeploy, self).__init__(ui) # User inputs @@ -248,7 +251,7 @@ def hypervisor(self): """Hypervisor to deploy to. Raises: - InvalidInputError: if not a recognized value. + InvalidInputError: if not a recognized value. """ return self._hypervisor @@ -264,7 +267,7 @@ def configuration(self): """VM configuration profile to use for deployment. Raises: - InvalidInputError: if not a profile defined in the VM. + InvalidInputError: if not a profile defined in the VM. """ return self._configuration @@ -329,7 +332,7 @@ def ready_to_run(self): """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.hypervisor is None: return False, "HYPERVISOR is a mandatory argument" diff --git a/COT/deploy_esxi.py b/COT/deploy_esxi.py index e392d1f..5a539bd 100644 --- a/COT/deploy_esxi.py +++ b/COT/deploy_esxi.py @@ -58,7 +58,7 @@ def __init__(self, ui, host, user, pwd, port=443): """Create a connection to the given server. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. For the other parameters, see :class:`pyVim.connect.SmartConnection` """ @@ -78,8 +78,8 @@ def __enter__(self): more meaningful error messages on failure. Raises: - vim.fault.HostConnectFault: TODO - requests.exceptions.ConnectionError: TODO + vim.fault.HostConnectFault: TODO + requests.exceptions.ConnectionError: TODO """ logger.verbose("Establishing connection to %s:%s...", self.server, self.port) @@ -127,9 +127,9 @@ def unwrap_connection_error(outer_e): this function dives inside the ConnectionError to find that context. Args: - outer_e (ConnectionError): ConnectionError to unwrap + outer_e (ConnectionError): ConnectionError to unwrap Returns: - tuple: extracted (errno, inner_message) + tuple: extracted (errno, inner_message) """ errno = None inner_message = None @@ -160,11 +160,11 @@ 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. + conn (SmarterConnection): Connection to ESXi. + vimtype (object): currently only ``vim.VirtualMachine`` + name (str): Name of the object to look up. Returns: - object: Located object + object: Located object """ obj = None content = conn.RetrieveContent() @@ -184,8 +184,8 @@ def __init__(self, conn, vm_name): """Use the given name to look up a VM using the given connection. Args: - conn (SmarterConnection): Connection to ESXi. - vm_name (str): Virtual machine name. + conn (SmarterConnection): Connection to ESXi. + vm_name (str): Virtual machine name. """ self.vm = get_object_from_connection(conn, vim.VirtualMachine, vm_name) if not self.vm: @@ -235,7 +235,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTDeployESXi, self).__init__(ui) self.datastore = None @@ -290,7 +290,7 @@ def ready_to_run(self): """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.locator is None: return False, "LOCATOR is a mandatory argument" @@ -300,10 +300,10 @@ def fixup_ovftool_args(self, ovftool_args, target): """Make any needed modifications to the ovftool arguments. Args: - ovftool_args (list): Any existing ovftool arguments to begin with. - target (str): deployment target URI + ovftool_args (list): Any existing ovftool arguments to begin with. + target (str): deployment target URI Returns: - list: Updated ovftool arguments + list: Updated ovftool arguments """ # pass selected configuration profile to ovftool if self.configuration is not None: @@ -341,7 +341,7 @@ def run(self): """Do the actual work of this submodule - deploying to ESXi. Raises: - InvalidInputError: if :func:`ready_to_run` reports ``False`` + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTDeployESXi, self).run() @@ -415,10 +415,10 @@ def fixup_serial_ports(self): """Use PyVmomi to create and configure serial ports for the new VM. Raises: - NotImplementedError: If any :class:`~COT.deploy.SerialConnection` - in :attr:`serial_connection` has a - :attr:`~COT.deploy.SerialConnection.kind` other than - 'tcp', 'telnet', or 'device' + NotImplementedError: If any :class:`~COT.deploy.SerialConnection` + in :attr:`serial_connection` has a + :attr:`~COT.deploy.SerialConnection.kind` other than + 'tcp', 'telnet', or 'device' """ logger.info("Fixing up serial ports...") with SmarterConnection(self.ui, self.server, @@ -437,8 +437,8 @@ def _create_serial_port(s, spec): """Use PyVmomi to create a serial connection on a VM. Args: - s (SerialConnection): Serial connection to create - spec (PyVmomiVMReconfigSpec): PyVmomi VM spec object + s (SerialConnection): Serial connection to create + spec (PyVmomiVMReconfigSpec): PyVmomi VM spec object """ logger.verbose(s) serial_spec = vim.vm.device.VirtualDeviceSpec() diff --git a/COT/disks/__init__.py b/COT/disks/__init__.py index 4d5b401..92cc7f2 100644 --- a/COT/disks/__init__.py +++ b/COT/disks/__init__.py @@ -59,13 +59,13 @@ 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. + 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. + DiskRepresentation: Converted disk. """ if new_format not in _class_for_format: raise NotImplementedError("No support for converting to type '{0}'" @@ -79,12 +79,12 @@ 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'. + disk_format (str): Disk format such as 'iso' or 'vmdk'. For the other parameters, see :class:`~COT.disks.disk.DiskRepresentation`. Returns: - DiskRepresentation: Created disk + DiskRepresentation: Created disk """ if disk_format in _class_for_format: return _class_for_format[disk_format](*args, **kwargs) @@ -96,10 +96,10 @@ 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. + file_path (str): Path of existing file to represent. Returns: - DiskRepresentation: Representation of this file. + DiskRepresentation: Representation of this file. """ if not os.path.exists(file_path): raise IOError(2, "No such file or directory: {0}".format(file_path)) diff --git a/COT/disks/disk.py b/COT/disks/disk.py index 095eb73..45842c4 100644 --- a/COT/disks/disk.py +++ b/COT/disks/disk.py @@ -34,12 +34,11 @@ def __init__(self, path, """Create a representation of an existing disk or create a new disk. 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. + 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. """ if not path: raise ValueError("Path must be set to a valid value, but got {0}" @@ -89,12 +88,12 @@ def from_other_image(cls, input_image, output_dir, output_subformat=None): """Convert the other disk image into an image of this type. Args: - input_image (DiskRepresentation): Existing image representation. - output_dir (str): Output directory to store the new image in. - output_subformat (str): Any relevant subformat information. + input_image (DiskRepresentation): Existing image representation. + output_dir (str): Output directory to store the new image in. + output_subformat (str): Any relevant subformat information. Raises: - NotImplementedError: Subclasses may implement this. + NotImplementedError: Subclasses may implement this. """ raise NotImplementedError("Not a valid target for conversion") @@ -103,13 +102,13 @@ def file_is_this_type(cls, path): """Check if the given file is image type represented by this class. Args: - path (str): Path to file to check. + path (str): Path to file to check. Returns: - bool: True (file matches this type) or False (file does not match) + bool: True (file matches this type) or False (file does not match) Raises: - HelperError: if no file exists at ``path``. + HelperError: if no file exists at ``path``. """ if not os.path.exists(path): raise HelperError(2, "No such file or directory: '{0}'" diff --git a/COT/disks/iso.py b/COT/disks/iso.py index 4cb04af..c8433e7 100644 --- a/COT/disks/iso.py +++ b/COT/disks/iso.py @@ -95,13 +95,13 @@ def file_is_this_type(cls, path): """Detect whether the given file is an ISO image. Args: - path (str): Path to file + path (str): Path to file Returns: - bool: True (file is an ISO) or False (file is not an ISO) + bool: True (file is an ISO) or False (file is not an ISO) Raises: - HelperError: if ``path`` is not a file at all. + HelperError: if ``path`` is not a file at all. """ if not os.path.exists(path): raise HelperError(2, "No such file or directory: '{0}'" @@ -128,11 +128,11 @@ def from_other_image(cls, input_image, output_dir, output_subformat=None): """Convert the other disk image into an image of this type. Args: - input_image (DiskRepresentation): Existing image representation. - output_dir (str): Output directory to store the new image in. - output_subformat (str): Any relevant subformat information. + input_image (DiskRepresentation): Existing image representation. + output_dir (str): Output directory to store the new image in. + output_subformat (str): Any relevant subformat information. Raises: - NotImplementedError: non-trivial to convert other types to ISO + NotImplementedError: non-trivial to convert other types to ISO """ raise NotImplementedError("Not a valid target for conversion") diff --git a/COT/disks/qcow2.py b/COT/disks/qcow2.py index d726f5c..ee192ed 100644 --- a/COT/disks/qcow2.py +++ b/COT/disks/qcow2.py @@ -28,12 +28,12 @@ def from_other_image(cls, input_image, output_dir, output_subformat=None): """Convert the other disk image into an image of this type. Args: - input_image (DiskRepresentation): Existing image representation. - output_dir (str): Output directory to store the new image in. - output_subformat (str): Any relevant subformat information. + input_image (DiskRepresentation): Existing image representation. + output_dir (str): Output directory to store the new image in. + output_subformat (str): Any relevant subformat information. Returns: - QCOW2: representation of newly created qcow2 image file + QCOW2: representation of newly created qcow2 image file """ file_name = os.path.basename(input_image.path) (file_prefix, _) = os.path.splitext(file_name) diff --git a/COT/disks/raw.py b/COT/disks/raw.py index 5fb7f04..0b2b0d3 100644 --- a/COT/disks/raw.py +++ b/COT/disks/raw.py @@ -92,12 +92,12 @@ def from_other_image(cls, input_image, output_dir, output_subformat=None): """Convert the other disk image into an image of this type. Args: - input_image (DiskRepresentation): Existing image representation. - output_dir (str): Output directory to store the new image in. - output_subformat (str): Any relevant subformat information. + input_image (DiskRepresentation): Existing image representation. + output_dir (str): Output directory to store the new image in. + output_subformat (str): Any relevant subformat information. Returns: - RAW: representation of newly created raw image. + RAW: representation of newly created raw image. """ file_name = os.path.basename(input_image.path) file_prefix, _ = os.path.splitext(file_name) diff --git a/COT/disks/vmdk.py b/COT/disks/vmdk.py index bc6ea73..2e5adb9 100644 --- a/COT/disks/vmdk.py +++ b/COT/disks/vmdk.py @@ -53,13 +53,13 @@ def from_other_image(cls, input_image, output_dir, """Convert the other disk image into an image of this type. Args: - input_image (DiskRepresentation): Existing image representation. - output_dir (str): Output directory to store the new image in. - output_subformat (str): VMDK subformat string. - Defaults to "streamOptimized" if unset. + input_image (DiskRepresentation): Existing image representation. + output_dir (str): Output directory to store the new image in. + output_subformat (str): VMDK subformat string. + Defaults to "streamOptimized" if unset. Returns: - VMDK: representation of newly created VMDK file. + VMDK: representation of newly created VMDK file. """ file_name = os.path.basename(input_image.path) (file_prefix, _) = os.path.splitext(file_name) diff --git a/COT/edit_hardware.py b/COT/edit_hardware.py index 9d97740..24d6b82 100644 --- a/COT/edit_hardware.py +++ b/COT/edit_hardware.py @@ -81,7 +81,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTEditHardware, self).__init__(ui) self.profiles = None @@ -302,7 +302,7 @@ def ready_to_run(self): """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ # Need some work to do! if not any([x is not None and x is not False for x in [ @@ -550,7 +550,7 @@ def run(self): """Do the actual work of this submodule. Raises: - InvalidInputError: if :func:`ready_to_run` reports ``False`` + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTEditHardware, self).run() @@ -737,12 +737,12 @@ def expand_list_wildcard(name_list, length, quiet=False): ['mgmt0', 'eth10', 'eth11', 'eth12'] Args: - name_list (list): List of names to assign, or None - length (list): Length to expand to - quiet (bool): Silence usual log messages generated by this function. + name_list (list): List of names to assign, or None + length (list): Length to expand to + quiet (bool): Silence usual log messages generated by this function. Returns: - list: Expanded list, or empty list if ``name_list`` is None or empty. + list: Expanded list, or empty list if ``name_list`` is None or empty. """ if not name_list: return [] @@ -785,10 +785,10 @@ def guess_list_wildcard(known_values): ['fake1', 'fake2', 'real{4}'] Args: - known_values (list): Values to guess from + known_values (list): Values to guess from Returns: - list: Guessed wildcard list, or None if unable to guess + list: Guessed wildcard list, or None if unable to guess """ logger.debug("Attempting to infer a pattern from %s", known_values) # Guess sequences ending with simple N, N+1, N+2 diff --git a/COT/edit_product.py b/COT/edit_product.py index 5f87694..a22afd5 100644 --- a/COT/edit_product.py +++ b/COT/edit_product.py @@ -54,7 +54,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTEditProduct, self).__init__(ui) self.product_class = None @@ -78,7 +78,7 @@ def ready_to_run(self): """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if not any([ self.product_class, @@ -98,7 +98,7 @@ def run(self): """Do the actual work of this submodule. Raises: - InvalidInputError: if :func:`ready_to_run` reports ``False`` + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTEditProduct, self).run() diff --git a/COT/edit_properties.py b/COT/edit_properties.py index 09a9dcc..d9106af 100644 --- a/COT/edit_properties.py +++ b/COT/edit_properties.py @@ -57,7 +57,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTEditProperties, self).__init__(ui) self._config_file = None @@ -75,7 +75,7 @@ def config_file(self): """Path to plaintext file to read configuration lines from. Raises: - InvalidInputError: if the file does not exist. + InvalidInputError: if the file does not exist. """ return self._config_file @@ -139,7 +139,7 @@ def ready_to_run(self): """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.labels and not self.properties: return False, ("The --label option requires also specifying " @@ -163,7 +163,7 @@ def run(self): """Do the actual work of this submodule. Raises: - InvalidInputError: if :func:`ready_to_run` reports ``False`` + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTEditProperties, self).run() diff --git a/COT/file_reference.py b/COT/file_reference.py index 7d3ff81..83d52f9 100644 --- a/COT/file_reference.py +++ b/COT/file_reference.py @@ -33,10 +33,10 @@ def __init__(self, file_path, filename=None): """Create a reference to a file on disk. Args: - file_path (str): File path or directory path - filename (str): If specified, file_path is considered to be - a directory containing this filename. If not specified, the - final element in file_path is considered the filename. + file_path (str): File path or directory path + filename (str): If specified, file_path is considered to be a + directory containing this filename. If not specified, the + final element in file_path is considered the filename. :: @@ -46,7 +46,7 @@ def __init__(self, file_path, filename=None): True Raises: - IOError: if no such file exists + IOError: if no such file exists """ if filename is None: self.file_path = file_path @@ -64,9 +64,9 @@ def __eq__(self, other): No attempt is made to check file equivalence, symlinks, etc. Args: - other (object): Other object to compare against + other (object): Other object to compare against Returns: - bool: True if the paths are the same, else False + bool: True if the paths are the same, else False """ return type(other) is type(self) and self.file_path == other.file_path @@ -74,9 +74,9 @@ def __ne__(self, other): """FileOnDisk instances are not equal if they have different paths. Args: - other (object): Other object to compare against + other (object): Other object to compare against Returns: - bool: False if the paths are the same, else True + bool: False if the paths are the same, else True """ return not self.__eq__(other) @@ -94,9 +94,9 @@ def open(self, mode): """Open the file and return a reference to the file object. Args: - mode (str): Mode such as 'r', 'w', 'a', 'w+', etc. + mode (str): Mode such as 'r', 'w', 'a', 'w+', etc. Returns: - file: File object + file: File object """ self.obj = open(self.file_path, mode) return self.obj @@ -109,7 +109,7 @@ def copy_to(self, dest_dir): """Copy this file to the given destination directory. Args: - dest_dir (str): Destination directory or filename. + dest_dir (str): Destination directory or filename. """ if self.file_path == os.path.join(dest_dir, self.filename): return @@ -120,7 +120,7 @@ def add_to_archive(self, tarf): """Copy this file into the given tarfile object. Args: - tarf (tarfile.TarFile): Add this file to that archive. + tarf (tarfile.TarFile): Add this file to that archive. """ logger.info("Adding %s to TAR file as %s", self.file_path, self.filename) @@ -134,12 +134,12 @@ def __init__(self, tarfile_path, filename): """Create a reference to a file contained in a TAR archive. Args: - tarfile_path (str): Path to TAR archive to read - filename (str): File name in the TAR archive. + tarfile_path (str): Path to TAR archive to read + filename (str): File name in the TAR archive. Raises: - IOError: if ``tarfile_path`` doesn't reference a TAR file, - or the TAR file does not contain ``filename``. + IOError: if ``tarfile_path`` doesn't reference a TAR file, + or the TAR file does not contain ``filename``. """ if not tarfile.is_tarfile(tarfile_path): raise IOError("{0} is not a valid TAR file.".format(tarfile_path)) @@ -158,9 +158,9 @@ def __eq__(self, other): No attempt is made to check file equivalence, symlinks, etc. Args: - other (object): Other object to compare against + other (object): Other object to compare against Returns: - bool: True if filename and tarfile_path are the same, else False + bool: True if filename and tarfile_path are the same, else False """ if type(other) is type(self): return (self.tarfile_path == other.tarfile_path and @@ -171,9 +171,9 @@ def __ne__(self, other): """FileInTar are not equal if they have different paths or names. Args: - other (object): Other object to compare against + other (object): Other object to compare against Returns: - bool: False if filename and tarfile_path are the same, else True + bool: False if filename and tarfile_path are the same, else True """ return not self.__eq__(other) @@ -197,11 +197,11 @@ def open(self, mode): """Open the TAR and return a reference to the relevant file object. Args: - mode (str): Only 'r' and 'rb' modes are supported. + mode (str): Only 'r' and 'rb' modes are supported. Returns: - file: File object + file: File object Raises: - ValueError: if ``mode`` is not valid. + ValueError: if ``mode`` is not valid. """ # We can only extract a file object from a TAR file in read mode. if mode != 'r' and mode != 'rb': @@ -223,7 +223,7 @@ def copy_to(self, dest_dir): """Extract this file to the given destination directory. Args: - dest_dir (str): Destination directory or filename. + dest_dir (str): Destination directory or filename. """ with closing(tarfile.open(self.tarfile_path, 'r')) as tarf: logger.info("Extracting %s from %s to %s", @@ -234,7 +234,7 @@ def add_to_archive(self, tarf): """Copy this file into the given tarfile object. Args: - tarf (tarfile.TarFile): Add this file to that archive. + tarf (tarfile.TarFile): Add this file to that archive. """ self.open('r') try: diff --git a/COT/help.py b/COT/help.py index 6c41bd7..5f557d4 100644 --- a/COT/help.py +++ b/COT/help.py @@ -38,7 +38,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTHelp, self).__init__(ui) self._subcommand = None diff --git a/COT/helpers/apt_get.py b/COT/helpers/apt_get.py index 29f134b..2a69c40 100644 --- a/COT/helpers/apt_get.py +++ b/COT/helpers/apt_get.py @@ -37,7 +37,7 @@ def install_package(self, package): """Install the requested package if needed. Args: - package (str): Name of the package to install. + package (str): Name of the package to install. """ # Check whether it's already installed if re.search(r"install ok installed", diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 1bc3af9..d3727a5 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -99,7 +99,7 @@ def TemporaryDirectory(suffix='', # noqa: N802 For the parameters, see :class:`tempfile.TemporaryDirectory`. Yields: - str: Path to temporary directory + str: Path to temporary directory """ tempdir = tempfile.mkdtemp(suffix, prefix, dirpath) try: @@ -127,8 +127,8 @@ def __init__(self, factory, *args, **kwargs): """Create the given dictionary with the given factory class/method. Args: - factory (object): Factory class or method to be called to populate - a new entry in response to :meth:`__missing__`. + factory (object): Factory class or method to be called to populate + a new entry in response to :meth:`__missing__`. For the other parameters, see :class:`dict`. """ @@ -141,10 +141,10 @@ def __missing__(self, key): Automatically populate the given key with an instance of the factory. Args: - key (object): Key that was not yet defined in this dictionary. + key (object): Key that was not yet defined in this dictionary. Returns: - object: Result of calling ``self.factory(key)`` + object: Result of calling ``self.factory(key)`` """ self[key] = self.factory(key) return self[key] @@ -189,12 +189,12 @@ def __init__(self, name, """Initializer. Args: - name (str): Name of helper executable - info_uri (str): URI to refer to for more info about this helper. - version_args (list): Args to pass to the helper to - get its version. Defaults to ``['--version']`` if unset. - version_regexp (str): Regexp to get the version number from - the output of the command. + name (str): Name of helper executable + info_uri (str): URI to refer to for more info about this helper. + version_args (list): Args to pass to the helper to + get its version. Defaults to ``['--version']`` if unset. + version_regexp (str): Regexp to get the version number from + the output of the command. """ self._name = name self._info_uri = info_uri @@ -274,22 +274,22 @@ def call(self, args, """Call the helper program with the given arguments. Args: - args (list): List of arguments to the helper program. - capture_output (boolean): If ``True``, stdout/stderr will be - redirected to a buffer and returned, instead of being displayed - to the user. (I.e., :func:`check_output` will be invoked - instead of :func:`check_call`) + args (list): List of arguments to the helper program. + capture_output (boolean): If ``True``, stdout/stderr will be + redirected to a buffer and returned, instead of being displayed + to the user. (I.e., :func:`check_output` will be invoked + instead of :func:`check_call`) Returns: - str: Captured stdout/stderr if :attr:`capture_output` is True, - else ``None``. + str: Captured stdout/stderr if :attr:`capture_output` is True, + else ``None``. For the other parameters, see :func:`check_call` and :func:`check_output`. Raises: - HelperNotFoundError: if the helper was not previously - installed, and the user declines to install it at this time. + HelperNotFoundError: if the helper was not previously + installed, and the user declines to install it at this time. """ if not self.path: if self.USER_INTERFACE and not self.USER_INTERFACE.confirm( @@ -312,8 +312,8 @@ def install(self): """Install the helper program. Raises: - NotImplementedError: if not :attr:`installable` - HelperError: if installation is attempted but fails. + NotImplementedError: if not :attr:`installable` + HelperError: if installation is attempted but fails. Subclasses should not override this method but instead should provide an appropriate implementation of the :meth:`_install` method. @@ -369,10 +369,10 @@ def download_and_expand_tgz(url): # d is automatically cleaned up. Args: - url (str): URL of a .tgz or .tar.gz file to download. + url (str): URL of a .tgz or .tar.gz file to download. Yields: - str: Temporary directory path where the archive has been extracted. + str: Temporary directory path where the archive has been extracted. """ with TemporaryDirectory(prefix="cot_helper") as d: logger.debug("Temporary directory is %s", d) @@ -399,9 +399,9 @@ def mkdir(directory, permissions=493): # 493 == 0o755 """Check whether the given target directory exists, and create if not. Args: - directory (str): Directory to check/create. - permissions (int): Permission mask to set when creating a - directory. Default is ``0o755``. + directory (str): Directory to check/create. + permissions (int): Permission mask to set when creating a directory. + Default is ``0o755``. """ if os.path.isdir(directory): # TODO: permissions check, update permissions if needed @@ -430,14 +430,14 @@ def cp(src, dest): """Copy the given src to the given dest, using sudo if needed. Args: - src (str): Source path. - dest (str): Destination path. + src (str): Source path. + dest (str): Destination path. Returns: - bool: True + bool: True Raises: - HelperError: if file copying fails + HelperError: if file copying fails """ logger.verbose("Copying %s to %s", src, dest) try: @@ -463,7 +463,7 @@ def install_package(self, package): """Install the requested package if needed. Args: - package (str): Name of the package to install. + package (str): Name of the package to install. """ raise NotImplementedError("install_package not implemented!") @@ -476,21 +476,21 @@ def check_call(args, require_success=True, retry_with_sudo=False, **kwargs): stdout/stderr as normal. Args: - args (list): Command to invoke and its associated args - require_success (boolean): If ``False``, do not raise an error - when the command exits with a return code other than 0 - retry_with_sudo (boolean): If ``True``, if the command gets - an exception, prepend ``sudo`` to the command and try again. + args (list): Command to invoke and its associated args + require_success (boolean): If ``False``, do not raise an error when the + command exits with a return code other than 0 + retry_with_sudo (boolean): If ``True``, if the command gets + an exception, prepend ``sudo`` to the command and try again. For the other parameters, see :func:`subprocess.check_call`. Raises: - HelperNotFoundError: if the command doesn't exist (instead of a - :class:`OSError`) - HelperError: if :attr:`require_success` is not ``False`` and - the command returns a value other than 0 (instead of a - :class:`subprocess.CalledProcessError`). - OSError: as :func:`subprocess.check_call`. + HelperNotFoundError: if the command doesn't exist (instead of a + :class:`OSError`) + HelperError: if :attr:`require_success` is not ``False`` and the command + returns a value other than 0 (instead of a + :class:`subprocess.CalledProcessError`). + OSError: as :func:`subprocess.check_call`. """ cmd = args[0] logger.info("Calling '%s'...", " ".join(args)) @@ -531,24 +531,24 @@ def check_output(args, require_success=True, retry_with_sudo=False, **kwargs): and generates a debug message with the stdout contents. Args: - args (list): Command to invoke and its associated args - require_success (boolean): If ``False``, do not raise an error - when the command exits with a return code other than 0 - retry_with_sudo (boolean): If ``True``, if the command gets - an exception, prepend ``sudo`` to the command and try again. + args (list): Command to invoke and its associated args + require_success (boolean): If ``False``, do not raise an error when the + command exits with a return code other than 0 + retry_with_sudo (boolean): If ``True``, if the command gets an + exception, prepend ``sudo`` to the command and try again. For the other parameters, see :func:`subprocess.check_output`. Returns: - str: Captured stdout/stderr from the command + str: Captured stdout/stderr from the command Raises: - HelperNotFoundError: if the command doesn't exist (instead of a - :class:`OSError`) - HelperError: if :attr:`require_success` is not ``False`` and - the command returns a value other than 0 (instead of a - :class:`subprocess.CalledProcessError`). - OSError: as :func:`subprocess.check_output`. + HelperNotFoundError: if the command doesn't exist (instead of a + :class:`OSError`) + HelperError: if :attr:`require_success` is not ``False`` and the command + returns a value other than 0 (instead of a + :class:`subprocess.CalledProcessError`). + OSError: as :func:`subprocess.check_output`. """ cmd = args[0] logger.info("Calling '%s' and capturing its output...", " ".join(args)) @@ -587,17 +587,17 @@ def helper_select(choices): first installable helper from the list. Raises: - HelperNotFoundError: if no valid helper is available or installable. + HelperNotFoundError: if no valid helper is available or installable. Args: - choices (list): List of helpers, in order from most preferred to - least preferred. Each choice in this list can be either: + choices (list): List of helpers, in order from most preferred to + least preferred. Each choice in this list can be either: - * a string (the helper name, such as "mkisofs") - * a tuple of (name, minimum version) such as ("qemu-img", "2.1.0"). + * a string (the helper name, such as "mkisofs") + * a tuple of (name, minimum version) such as ("qemu-img", "2.1.0"). Returns: - Helper: The selected helper class instance. + Helper: The selected helper class instance. """ for choice in choices: if isinstance(choice, str): diff --git a/COT/helpers/port.py b/COT/helpers/port.py index b73d072..19328a7 100644 --- a/COT/helpers/port.py +++ b/COT/helpers/port.py @@ -39,7 +39,7 @@ def install_package(self, package): """Install the requested package if needed. Args: - package (str): Name of the package to install. + package (str): Name of the package to install. """ # Check for updates if not Port._updated: diff --git a/COT/helpers/tests/test_helper.py b/COT/helpers/tests/test_helper.py index 10f7ee6..f8c828d 100644 --- a/COT/helpers/tests/test_helper.py +++ b/COT/helpers/tests/test_helper.py @@ -101,10 +101,10 @@ def apt_install_test(self, pkgname, helpername, *_): """Test installation with 'dpkg' and 'apt-get'. Args: - pkgname (str): Apt package to test installation for. - helpername (str): Expected value of - :attr:`~COT.helpers.helper.Helper.name`, if different from - ``pkgname``. + pkgname (str): Apt package to test installation for. + helpername (str): Expected value of + :attr:`~COT.helpers.helper.Helper.name`, if different from + ``pkgname``. """ helpers['dpkg']._installed = True # Python 2.6 doesn't let us do multiple mocks in one 'with' @@ -142,7 +142,7 @@ def port_install_test(self, portname, *_): """Test installation with 'port'. Args: - portname (str): MacPorts package name to test. + portname (str): MacPorts package name to test. """ self.select_package_manager('port') Port._updated = False diff --git a/COT/helpers/yum.py b/COT/helpers/yum.py index 07ade22..44c1722 100644 --- a/COT/helpers/yum.py +++ b/COT/helpers/yum.py @@ -34,7 +34,7 @@ def install_package(self, package): """Install the requested package if needed. Args: - package (str): Name of the package to install. + package (str): Name of the package to install. """ self.call(['--quiet', 'install', package], capture_output=False, retry_with_sudo=True) diff --git a/COT/info.py b/COT/info.py index 3ae3921..9336953 100644 --- a/COT/info.py +++ b/COT/info.py @@ -43,7 +43,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTInfo, self).__init__(ui) self._package_list = None @@ -78,7 +78,7 @@ def ready_to_run(self): """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if not self.package_list: return False, "At least one package must be specified" @@ -88,7 +88,7 @@ def run(self): """Do the actual work of this submodule. Raises: - InvalidInputError: if :func:`ready_to_run` reports ``False`` + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTInfo, self).run() diff --git a/COT/inject_config.py b/COT/inject_config.py index 9872d54..a64ea4a 100644 --- a/COT/inject_config.py +++ b/COT/inject_config.py @@ -46,7 +46,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTInjectConfig, self).__init__(ui) self._config_file = None @@ -58,9 +58,9 @@ def config_file(self): """Primary configuration file. Raises: - InvalidInputError: if the file does not exist - InvalidInputError: if the `platform described by - :attr:`package` doesn't support configuration files. + InvalidInputError: if the file does not exist + InvalidInputError: if the platform described by :attr:`package` + doesn't support configuration files. """ return self._config_file @@ -82,9 +82,9 @@ def secondary_config_file(self): """Secondary configuration file. Raises: - InvalidInputError: if the file does not exist - InvalidInputError: if the platform described by - :attr:`package` doesn't support secondary configuration files. + InvalidInputError: if the file does not exist + InvalidInputError: if the platform described by :attr:`package` + doesn't support secondary configuration files. """ return self._secondary_config_file @@ -106,7 +106,7 @@ def extra_files(self): """Additional files to be embedded as-is. Raises: - InvalidInputError: if any file in the list does not exist + InvalidInputError: if any file in the list does not exist """ return self._extra_files @@ -121,7 +121,7 @@ def ready_to_run(self): """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if not (self.config_file or self.secondary_config_file or @@ -133,14 +133,14 @@ def run(self): """Do the actual work of this submodule. Raises: - InvalidInputError: if :func:`ready_to_run` reports ``False`` - ValueUnsupportedError: if the - :const:`~COT.platforms.GenericPlatform.BOOTSTRAP_DISK_TYPE` of - the associated VM's - :attr:`~COT.vm_description.VMDescription.platform` is not - 'cdrom' or 'harddisk' - LookupError: if unable to find a disk drive device to inject - the configuration into. + InvalidInputError: if :func:`ready_to_run` reports ``False`` + ValueUnsupportedError: if the + :const:`~COT.platforms.GenericPlatform.BOOTSTRAP_DISK_TYPE` of + the associated VM's + :attr:`~COT.vm_description.VMDescription.platform` is not + 'cdrom' or 'harddisk' + LookupError: if unable to find a disk drive device to inject + the configuration into. """ super(COTInjectConfig, self).run() diff --git a/COT/install_helpers.py b/COT/install_helpers.py index 3a6be8d..1c21524 100644 --- a/COT/install_helpers.py +++ b/COT/install_helpers.py @@ -53,9 +53,9 @@ def verify_manpages(man_dir): """Verify installation of COT's manual pages. Args: - man_dir (str): Base directory where manpages should be found. + man_dir (str): Base directory where manpages should be found. Returns: - tuple: (result, message) + tuple: (result, message) """ for f in resource_listdir("COT", "docs/man"): src_path = resource_filename("COT", os.path.join("docs/man", f)) @@ -82,13 +82,13 @@ def _install_manpage(src_path, man_dir): """Install the given manual page for COT. Args: - src_path (str): Path to manual page file. - man_dir (str): Base directory where page should be installed. + src_path (str): Path to manual page file. + man_dir (str): Base directory where page should be installed. Returns: - tuple: (page_previously_installed, page_updated) + tuple: (page_previously_installed, page_updated) Raises: - IOError: if installation fails under some circumstances - OSError: if installation fails under other circumstances + IOError: if installation fails under some circumstances + OSError: if installation fails under other circumstances """ # Which man section does this belong in? f = os.path.basename(src_path) @@ -112,9 +112,9 @@ def install_manpages(man_dir): """Install COT's manual pages. Args: - man_dir (str): Base directory where manpages should be installed. + man_dir (str): Base directory where manpages should be installed. Returns: - tuple: (result, message) + tuple: (result, message) """ installed_any = False some_preinstalled = False @@ -152,7 +152,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTInstallHelpers, self).__init__(ui) self.ignore_errors = False @@ -162,10 +162,10 @@ def install_helper(self, helper): """Install the given helper module. Args: - helper (Helper): Helper module to install. + helper (Helper): Helper module to install. Returns: - tuple: (result, message) + tuple: (result, message) """ if helper.installed: return (True, @@ -188,7 +188,7 @@ def manpages_helper(self): """Verify or install COT's manual pages. Returns: - tuple: (result, message) + tuple: (result, message) """ try: resource_listdir("COT", "docs/man") diff --git a/COT/ovf/hardware.py b/COT/ovf/hardware.py index b39b7b7..8bbb7ec 100644 --- a/COT/ovf/hardware.py +++ b/COT/ovf/hardware.py @@ -54,10 +54,10 @@ def __init__(self, ovf): """Construct an OVFHardware object describing all Items in the OVF. Args: - ovf (OVF): OVF instance to extract hardware information from. + ovf (OVF): OVF instance to extract hardware information from. Raises: - OVFHardwareDataError: if any data errors are seen + OVFHardwareDataError: if any data errors are seen """ self.ovf = ovf self.item_dict = {} @@ -146,7 +146,7 @@ def find_unused_instance_id(self): """Find the first available ``InstanceID`` number. Returns: - str: An instance ID that is not yet in use. + str: An instance ID that is not yet in use. """ i = 1 while str(i) in self.item_dict.keys(): @@ -158,12 +158,12 @@ def new_item(self, resource_type, profile_list=None): """Create a new :class:`~COT.ovf.item.OVFItem` of the given type. Args: - resource_type (str): String such as 'cpu' or 'harddisk' - used as - a key to :data:`~COT.ovf.name_helper.OVFNameHelper1.RES_MAP` - profile_list (list): Profiles the new item should belong to + resource_type (str): String such as 'cpu' or 'harddisk' - used as + a key to :data:`~COT.ovf.name_helper.OVFNameHelper1.RES_MAP` + profile_list (list): Profiles the new item should belong to Returns: - tuple: ``(instance_id, ovfitem)`` + tuple: ``(instance_id, ovfitem)`` """ instance = self.find_unused_instance_id() ovfitem = OVFItem(self.ovf) @@ -185,7 +185,7 @@ def delete_item(self, item): """Delete the given Item from the hardware. Args: - item (OVFItem): Item to delete + item (OVFItem): Item to delete """ instance = item.get_value(self.ovf.INSTANCE_ID) if self.item_dict[instance] == item: @@ -196,11 +196,11 @@ def clone_item(self, parent_item, profile_list): """Clone an :class:`OVFItem` to create a new instance. Args: - parent_item (OVFItem): Instance to clone from - profile_list (list): List of profiles to clone into + parent_item (OVFItem): Instance to clone from + profile_list (list): List of profiles to clone into Returns: - tuple: ``(instance_id, ovfitem)`` + tuple: ``(instance_id, ovfitem)`` """ instance = self.find_unused_instance_id() ovfitem = copy.deepcopy(parent_item) @@ -215,13 +215,13 @@ def item_match(self, item, resource_type, properties, profile_list): """Check whether the given item matches the given filters. Args: - item (OVFItem): Item to validate - resource_type (str): Resource type string like 'scsi' or 'serial' - properties (dict): Properties and their values to match - profile_list (list): List of profiles to filter on + item (OVFItem): Item to validate + resource_type (str): Resource type string like 'scsi' or 'serial' + properties (dict): Properties and their values to match + profile_list (list): List of profiles to filter on Returns: - bool: True if the item matches all filters, False if not. + bool: True if the item matches all filters, False if not. """ if resource_type and (self.ovf.RES_MAP[resource_type] != item.get_value(self.ovf.RESOURCE_TYPE)): @@ -240,12 +240,12 @@ def find_all_items(self, resource_type=None, properties=None, """Find all items matching the given type, properties, and profiles. Args: - resource_type (str): Resource type string like 'scsi' or 'serial' - properties (dict): Properties and their values to match - profile_list (list): List of profiles to filter on + resource_type (str): Resource type string like 'scsi' or 'serial' + properties (dict): Properties and their values to match + profile_list (list): List of profiles to filter on Returns: - list: Matching :class:`~COT.ovf.item.OVFItem` instances + list: Matching :class:`~COT.ovf.item.OVFItem` instances """ items = [self.item_dict[instance] for instance in natural_sort(self.item_dict)] @@ -262,15 +262,15 @@ def find_item(self, resource_type=None, properties=None, profile=None): """Find the only :class:`OVFItem` of the given :attr:`resource_type`. Args: - resource_type (str): Resource type string like 'scsi' or 'serial' - properties (dict): Properties and their values to match - profile (str): Single profile ID to search within + resource_type (str): Resource type string like 'scsi' or 'serial' + properties (dict): Properties and their values to match + profile (str): Single profile ID to search within Returns: - OVFItem: Matching instance, or None + OVFItem: Matching instance, or None Raises: - LookupError: if more than one such Item exists. + LookupError: if more than one such Item exists. """ matches = self.find_all_items(resource_type, properties, [profile]) if len(matches) > 1: @@ -285,11 +285,11 @@ def get_item_count(self, resource_type, profile): """Wrapper for :meth:`get_item_count_per_profile`. Args: - resource_type (str): Resource type string like 'scsi' or 'serial' - profile (str): Single profile identifier string to look up. + resource_type (str): Resource type string like 'scsi' or 'serial' + profile (str): Single profile identifier string to look up. Returns: - int: Number of items of this type in this profile. + int: Number of items of this type in this profile. """ return (self.get_item_count_per_profile(resource_type, [profile]) [profile]) @@ -301,13 +301,13 @@ def get_item_count_per_profile(self, resource_type, profile_list): the total for each profile. Args: - resource_type (str): Resource type string like 'scsi' or 'serial' - profile_list (list): List of profiles to filter on - (default: apply across all profiles) + resource_type (str): Resource type string like 'scsi' or 'serial' + profile_list (list): List of profiles to filter on + (default: apply across all profiles) Returns: - dict: mapping profile strings to the number of items under - each profile. + dict: mapping profile strings to the number of items under each + profile. """ count_dict = {} if not profile_list: @@ -331,13 +331,13 @@ def update_existing_item_count_per_profile(self, resource_type, Helper method for :meth:`set_item_count_per_profile`. Args: - resource_type (str): 'cpu', 'harddisk', etc. - count (int): Desired number of items - profile_list (list): List of profiles to filter on - (default: apply across all profiles) + resource_type (str): 'cpu', 'harddisk', etc. + count (int): Desired number of items + profile_list (list): List of profiles to filter on + (default: apply across all profiles) Returns: - tuple: (count_dict, items_to_add, last_item) + tuple: (count_dict, items_to_add, last_item) """ count_dict = self.get_item_count_per_profile(resource_type, profile_list) @@ -380,20 +380,20 @@ def _update_cloned_item(self, new_item, new_item_profiles, item_count): Helper method for :meth:`set_item_count_per_profile`. Args: - new_item (OVFItem): Newly cloned Item - new_item_profiles (list): Profiles new_item should belong to - item_count (int): How many Items of this type (including this - item) now exist. Used with - :meth:`COT.platform.GenericPlatform.guess_nic_name` + new_item (OVFItem): Newly cloned Item + new_item_profiles (list): Profiles new_item should belong to + item_count (int): How many Items of this type (including this + item) now exist. Used with + :meth:`COT.platform.GenericPlatform.guess_nic_name` Returns: - OVFItem: Updated :param:`new_item` + OVFItem: Updated :param:`new_item` Raises: - NotImplementedError: No support yet for updating ``Address`` - NotImplementedError: If updating ``AddressOnParent`` but the - prior value varies across config profiles. - NotImplementedError: if ``AddressOnParent`` is not an integer. + NotImplementedError: No support yet for updating ``Address`` + NotImplementedError: If updating ``AddressOnParent`` but the + prior value varies across config profiles. + NotImplementedError: if ``AddressOnParent`` is not an integer. """ resource_type = new_item.hardware_type address = new_item.get(self.ovf.ADDRESS) @@ -446,10 +446,10 @@ def set_item_count_per_profile(self, resource_type, count, profile_list): then the highest-numbered instances will be removed preferentially. Args: - resource_type (str): 'cpu', 'harddisk', etc. - count (int): Desired number of items - profile_list (list): List of profiles to filter on - (default: apply across all profiles) + resource_type (str): 'cpu', 'harddisk', etc. + count (int): Desired number of items + profile_list (list): List of profiles to filter on + (default: apply across all profiles) """ if not profile_list: # Set the profile list for all profiles, including the default @@ -491,13 +491,13 @@ def set_value_for_all_items(self, resource_type, prop_name, new_value, and do nothing. Args: - resource_type (str): Resource type such as 'cpu' or 'harddisk' - prop_name (str): Property name to update - new_value (str): New value to set the property to - profile_list (list): List of profiles to filter on - (default: apply across all profiles) - create_new (bool): Whether to create a new entry if no items - of this :attr:`resource_type` presently exist. + resource_type (str): Resource type such as 'cpu' or 'harddisk' + prop_name (str): Property name to update + new_value (str): New value to set the property to + profile_list (list): List of profiles to filter on + (default: apply across all profiles) + create_new (bool): Whether to create a new entry if no items + of this :attr:`resource_type` presently exist. """ ovfitem_list = self.find_all_items(resource_type) if not ovfitem_list: @@ -520,14 +520,14 @@ def set_item_values_per_profile(self, resource_type, prop_name, value_list, """Set value(s) for a property of multiple items of a type. Args: - resource_type (str): Device type such as 'harddisk' or 'cpu' - prop_name (str): Property name to update - value_list (list): List of values to set (one value per item - of the given :attr:`resource_type`) - profile_list (list): List of profiles to filter on - (default: apply across all profiles) - default (str): If there are more matching items than entries in - :attr:`value_list`, set extra items to this value + resource_type (str): Device type such as 'harddisk' or 'cpu' + prop_name (str): Property name to update + value_list (list): List of values to set (one value per item of the + given :attr:`resource_type`) + profile_list (list): List of profiles to filter on + (default: apply across all profiles) + default (str): If there are more matching items than entries in + :attr:`value_list`, set extra items to this value """ if profile_list is None: profile_list = self.ovf.config_profiles + [None] diff --git a/COT/ovf/item.py b/COT/ovf/item.py index 123c69d..321178a 100644 --- a/COT/ovf/item.py +++ b/COT/ovf/item.py @@ -60,10 +60,10 @@ def list_union(*lists): ['bar', 'foo'] Args: - lists (list): List of lists to unify. + lists (list): List of lists to unify. Returns: - list: All distinct values across the given lists. + list: All distinct values across the given lists. """ result = [] for l in lists: @@ -96,8 +96,8 @@ def __init__(self, ovf, item=None): """Create a new OVFItem with contents based on the given Item element. Args: - ovf (OVF): OVF instance that owns the Item (optional) - item (xml.etree.ElementTree.Element): 'Item' element (optional) + ovf (OVF): OVF instance that owns the Item (optional) + item (xml.etree.ElementTree.Element): 'Item' element (optional) """ self.ovf = ovf if ovf is not None: @@ -125,14 +125,14 @@ def __getattr__(self, name): """Transparently pass attribute lookups off to OVF/OVFNameHelper. Args: - name (str): Attribute name. + name (str): Attribute name. Returns: - Value looked up from OVFNameHelper. + Value looked up from OVFNameHelper. Raises: - AttributeError: Magic methods (``__foo``) will not be passed - through but will raise an AttributeError as usual. + AttributeError: Magic methods (``__foo``) will not be passed + through but will raise an AttributeError as usual. """ # Don't pass 'special' attributes through to the helper if re.match(r"^__", name): @@ -164,10 +164,10 @@ def property_values(self, name): """Get list of values known for a given property name. Args: - name (str): Property name. + name (str): Property name. Returns: - list: List of values + list: List of values """ return list(self.properties[name].keys()) @@ -175,11 +175,11 @@ def property_profiles(self, name, value): """Get set of profiles associated with a property name and value. Args: - name (str): Property name. - value (object): Property value of interest. + name (str): Property name. + value (object): Property value of interest. Returns: - set: Profile strings associated with this name/value. + set: Profile strings associated with this name/value. """ return self.properties[name][value] @@ -187,11 +187,11 @@ def all_profiles(self, name, default=None): """Superset of all profiles for which this name has a value. Args: - name (str): Property name. - default (object): Default value to return if there are no matches + name (str): Property name. + default (object): Default value to return if there are no matches Returns: - Set of profile strings, or the given `default` if no matches. + Set of profile strings, or the given `default` if no matches. """ value_dict = self.properties.get(name, None) if not value_dict: @@ -202,13 +202,13 @@ def add_item(self, item): """Add the given ``Item`` element to this OVFItem. Args: - item (xml.etree.ElementTree.Element): XML ``Item`` element + item (xml.etree.ElementTree.Element): XML ``Item`` element Raises: - ValueUnsupportedError: if the ``item`` is not a recognized - Item variant. - OVFItemDataError: if the new Item conflicts with existing data - already in the OVFItem. + ValueUnsupportedError: if the ``item`` is not a recognized + Item variant. + OVFItemDataError: if the new Item conflicts with existing data + already in the OVFItem. """ logger.debug("Adding new %s", item.tag) self.NS = self.name_helper.namespace_for_item_tag(item.tag) @@ -280,12 +280,12 @@ def value_add_wildcards(self, name, value, profiles): VirtualQuantity or ResourceSubType changes, these can change too. Args: - name (str): Property name - value (str): Value to add wildcards to. - profiles (list): Profiles to which this (name, value) applies. + name (str): Property name + value (str): Value to add wildcards to. + profiles (list): Profiles to which this (name, value) applies. Returns: - str: The updated value string with wildcards added. + str: The updated value string with wildcards added. .. seealso:: :meth:`value_replace_wildcards` @@ -314,12 +314,12 @@ def value_replace_wildcards(self, name, value, profiles): """Replace wildcards with actual values. Args: - name (str): Property name - value (str): Value to replace wildcards from. - profiles (list): Profiles to which this (name, value) applies. + name (str): Property name + value (str): Value to replace wildcards from. + profiles (list): Profiles to which this (name, value) applies. Returns: - The updated value string, with wildcards replaced. + str: The updated value string, with wildcards replaced. .. seealso:: :meth:`value_add_wildcards` @@ -349,9 +349,9 @@ def _set_new_property(self, name, value, profiles): """Helper for :meth:`set_property`. Create a new property entry. Args: - name (str): Property name - value (str): Value to store for this property. - profiles (list): Profiles to which this (name, value) applies. + name (str): Property name + value (str): Value to store for this property. + profiles (list): Profiles to which this (name, value) applies. """ if not value: return @@ -366,14 +366,14 @@ def _set_existing_property(self, name, value, profiles, overwrite): """Helper for :meth:`set_property`. Update an existing property. Args: - name (str): Property name - value (str): Value to store for this property. - profiles (list): Profiles to which this (name, value) applies. - overwrite (bool): Whether to permit overwriting existing values. + name (str): Property name + value (str): Value to store for this property. + profiles (list): Profiles to which this (name, value) applies. + overwrite (bool): Whether to permit overwriting existing values. Raises: - OVFItemDataError: If ``overwrite`` is False and the value is - already set for one or more of the requested ``profiles``. + OVFItemDataError: If ``overwrite`` is False and the value is + already set for one or more of the requested ``profiles``. """ for (known_value, profile_set) in list(self.properties[name].items()): if not overwrite and profile_set.intersection(profiles): @@ -417,16 +417,16 @@ def set_property(self, name, value, profiles=None, overwrite=True): """Store the value and profiles associated with it for the given name. Args: - name (str): Property name - value (str): Value associated with :attr:`name` - profiles (list): If ``None``, set for all profiles currently known - to this item, else set only for the given list of profiles. - overwrite (bool): Whether to permit overwriting of existing - value set in this item. + name (str): Property name + value (str): Value associated with :attr:`name` + profiles (list): If ``None``, set for all profiles currently known + to this item, else set only for the given list of profiles. + overwrite (bool): Whether to permit overwriting of existing + value set in this item. Raises: - OVFItemDataError: if a value is already defined and would be - overwritten, unless :attr:`overwrite` is ``True`` + OVFItemDataError: if a value is already defined and would be + overwritten, unless :attr:`overwrite` is ``True`` """ # A ResourceSubType in the XML can be a single value or a # space-separated list of values. Internally, we'll store it as a @@ -472,13 +472,13 @@ def add_profile(self, new_profile, from_item=None): """Add a new profile to this item. Args: - new_profile (str): Profile name to add - from_item (OVFItem): Item to inherit properties from. If unset, - this defaults to ``self``. + new_profile (str): Profile name to add + from_item (OVFItem): Item to inherit properties from. If unset, + this defaults to ``self``. Raises: - RuntimeError: If unable to determine what value to inherit for - a particular property. + RuntimeError: If unable to determine what value to inherit for + a particular property. """ if self.has_profile(new_profile): logger.error("Profile %s already exists under %s!", @@ -515,11 +515,11 @@ def remove_profile(self, profile, split_default=True): """Remove all trace of the given profile from this item. Args: - profile (str): Profile name to remove - split_default (bool): If False, do not split out 'default' - profile items to specifically exclude this profile. Used when - the profile being removed will no longer exist anywhere and - so 'default' will continue to exclude this profile. + profile (str): Profile name to remove + split_default (bool): If False, do not split out 'default' profile + items to specifically exclude this profile. Used when the + profile being removed will no longer exist anywhere and so + 'default' will continue to exclude this profile. """ if not self.has_profile(profile): logger.error("Requested deletion of profile '%s' but it is " @@ -556,10 +556,10 @@ def get(self, tag): """Get the dict associated with the given XML tag, if any. Args: - tag (str): XML tag to look up + tag (str): XML tag to look up Returns: - dict: Dictionary of values associated with this tag (TODO?) + dict: Dictionary of values associated with this tag (TODO?) """ return self.properties.get(tag, None) @@ -573,11 +573,11 @@ def _get_value(self, tag, profiles=None): the tag values differ across the profiles, returns ``None``. Args: - tag (str): Tag to retrieve value for - profiles (set): set of profile names, or None + tag (str): Tag to retrieve value for + profiles (set): set of profile names, or None Returns: - str,tuple: Value, default value, or ``None``, unsanitized. + Value, default value, or ``None``, unsanitized. """ if profiles is not None: profiles = set(profiles) @@ -610,15 +610,15 @@ def get_value(self, tag, profiles=None): tag values differ across the profiles, returns ``None``. Args: - tag (str): Tag to retrieve value for - profiles (set): set of profile names, or None + tag (str): Tag to retrieve value for + profiles (set): set of profile names, or None Returns: - Value string or list, or ``None`` + Value string or list, or ``None`` Raises: - OVFItemDataError: if :meth:`value_replace_wildcards` failed to - remove any wildcards from the internally stored value. + OVFItemDataError: if :meth:`value_replace_wildcards` failed to + remove any wildcards from the internally stored value. """ val = self._get_value(tag, profiles) val = self.value_replace_wildcards(tag, val, profiles) @@ -634,10 +634,10 @@ def get_all_values(self, tag): """Get the list of all value strings for the given tag. Args: - tag (str): Tag to retrieve value for + tag (str): Tag to retrieve value for Returns: - list: List of value strings. + list: List of value strings. """ if tag == self.RESOURCE_SUB_TYPE: # ResourceSubType values may themselves be tuples @@ -651,8 +651,8 @@ def validate(self): 'all profiles' and also redundantly to a specific profile). Raises: - RuntimeError: if validation fails and COT doesn't know - how to automatically repair the error(s) identified. + RuntimeError: if validation fails and COT doesn't know + how to automatically repair the error(s) identified. """ # An OVFItem must describe only one InstanceID # All Items with a given InstanceID must have the same ResourceType @@ -683,10 +683,10 @@ def has_profile(self, profile): """Check if this Item exists under the given profile. Args: - profile (str): Profile name + profile (str): Profile name Returns: - bool: True if the item exists in this profile, False if not. + bool: True if the item exists in this profile, False if not. """ profiles = self.all_profiles(self.INSTANCE_ID) if profiles is None: @@ -701,7 +701,7 @@ def get_nonintersecting_set_list(self): """Identify the minimal non-intersecting set of profiles. Returns: - list: List of profile-set strings. + list: List of profile-set strings. """ set_list = [] for name in self.property_names: @@ -745,7 +745,7 @@ def generate_items(self): """Get a list of Item XML elements derived from this object's data. Returns: - list: Generated list of XML Item elements + list: Generated list of XML Item elements """ set_string_list = self.get_nonintersecting_set_list() diff --git a/COT/ovf/name_helper.py b/COT/ovf/name_helper.py index d25b592..fc372d5 100644 --- a/COT/ovf/name_helper.py +++ b/COT/ovf/name_helper.py @@ -39,10 +39,10 @@ def name_helper(version): """Generate an instance of the correct OVFNameHelper variant class. Args: - version (float): OVF specification version to use, such as 0.9, - 1.0, or 2.0 + version (float): OVF specification version to use, such as 0.9, 1.0, + or 2.0 Returns: - Instance of OVFNameHelper[012] as appropriate. + Instance of OVFNameHelper[012] as appropriate. """ if version < 1.0: return OVFNameHelper0() @@ -59,8 +59,8 @@ def __init__(self, namespace_name, tag): """Store namespace name and tag. Args: - namespace_name (str): XML namespace name - tag (str): XML tag + namespace_name (str): XML namespace name + tag (str): XML tag """ self.namespace_name = namespace_name.upper() self.tag = tag @@ -269,11 +269,11 @@ def __getattr__(self, name): """Transparently pass attribute lookups to _raw and _cache. Args: - name (str): Attribute name to look up. + name (str): Attribute name to look up. Returns: - Value looked up from :attr:`_raw` and/or :attr:`_cache`. + Value looked up from :attr:`_raw` and/or :attr:`_cache`. Raises: - AttributeError: if the given ``name`` is not found. + AttributeError: if the given ``name`` is not found. """ if name in self._item_children: return self._item_children[name] @@ -332,9 +332,9 @@ def namespace_for_item_tag(self, tag): """Get the XML namespace for the given item tag. Args: - tag (str): Un-namespaced XML tag. + tag (str): Un-namespaced XML tag. Returns: - str: XML namespace string, or None. + str: XML namespace string, or None. """ if tag == self.ITEM: return self.RASD @@ -348,9 +348,9 @@ def namespace_for_resource_type(self, resource_type): """Get the XML namespace for the given ResourceType. Args: - resource_type (str): ResourceType value string. + resource_type (str): ResourceType value string. Returns: - str: XML namespace string, or None. + str: XML namespace string, or None. """ if resource_type == self.RES_MAP['ethernet']: return self.EPASD @@ -364,11 +364,11 @@ def item_tag_for_namespace(self, ns): """Get the Item tag for the given XML namespace. Args: - ns (str): XML namespace + ns (str): XML namespace Returns: - str: 'Item', 'StorageItem', or 'EthernetPortItem' as appropriate. + str: 'Item', 'StorageItem', or 'EthernetPortItem' as appropriate. Raises: - ValueUnsupportedError: if the namespace is unrecognized + ValueUnsupportedError: if the namespace is unrecognized """ if ns == self.RASD: return self.ITEM diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index 962d182..6e10788 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -78,12 +78,12 @@ def byte_count(base_val, multiplier): 536870912 Args: - base_val (str): Base value string (value of ``ovf:capacity``, etc.) - multiplier (str): Multiplier string (value of - ``ovf:capacityAllocationUnits``, etc.) + 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 + int: Number of bytes """ if not multiplier: return int(base_val) @@ -122,10 +122,10 @@ def factor_bytes(byte_value): ('134217729', 'byte') Args: - byte_value (int): Number of bytes + byte_value (int): Number of bytes Returns: - ``(base_val, multiplier)`` + tuple: ``(base_val, multiplier)`` """ shift = 0 byte_value = int(byte_value) @@ -165,12 +165,12 @@ def byte_string(byte_value, base_shift=0): '2.5 KiB' Args: - byte_value (float): Value - base_shift (int): Base value of byte_value + byte_value (float): Value + base_shift (int): Base value of byte_value (0 = bytes, 1 = KiB, 2 = MiB, etc.) Returns: - Pretty-printed byte string such as "1.00 GiB" + str: Pretty-printed byte string such as "1.00 GiB" """ tags = ["B", "KiB", "MiB", "GiB", "TiB"] byte_value = float(byte_value) @@ -218,13 +218,13 @@ def detect_type_from_name(filename): Does not check file contents, as the given filename may not yet exist. Args: - filename (str): File name/path + filename (str): File name/path Returns: - '.ovf' or '.ova' + str: '.ovf' or '.ova' Raises: - ValueUnsupportedError: if filename doesn't match ovf/ova + ValueUnsupportedError: if filename doesn't match ovf/ova """ # We don't care about any directory path filename = os.path.basename(filename) @@ -256,10 +256,10 @@ def _ovf_descriptor_from_name(self, input_file): return the path to the extracted OVF descriptor. Args: - input_file (str): Path to an OVF descriptor or OVA file. + input_file (str): Path to an OVF descriptor or OVA file. Returns: - OVF descriptor path + str: OVF descriptor path """ extension = self.detect_type_from_name(input_file) if extension == '.ova': @@ -274,21 +274,20 @@ def __init__(self, input_file, output_file): """Open the specified OVF and read its XML into memory. Args: - input_file (str): Data file to read in. - output_file (str): File name to write to. If this VM is read-only, - (there will never be an output file) this value should be - ``None``; if the output filename is not yet known, use - ``""`` and subsequently set :attr:`output_file` when it is - determined. + input_file (str): Data file to read in. + output_file (str): File name to write to. If this VM is read-only, + (there will never be an output file) this value should be + ``None``; if the output filename is not yet known, use ``""`` + and subsequently set :attr:`output_file` when it is determined. Raises: - VMInitError: - * if the OVF descriptor cannot be located - * if an XML parsing error occurs - * if the XML is not actually an OVF descriptor - * if the OVF hardware validation fails - Exception: will call :meth:`destroy` to clean up before - reraising any exception encountered. + VMInitError: + * if the OVF descriptor cannot be located + * if an XML parsing error occurs + * if the XML is not actually an OVF descriptor + * if the OVF hardware validation fails + Exception: will call :meth:`destroy` to clean up before reraising + any exception encountered. """ try: self.output_extension = None @@ -414,7 +413,7 @@ def output_file(self): """OVF or OVA file that will be created or updated by :meth:`write`. Raises: - ValueUnsupportedError: if :func:`detect_type_from_name` fails + ValueUnsupportedError: if :func:`detect_type_from_name` fails """ return super(OVF, self).output_file @@ -494,7 +493,7 @@ def validate_hardware(self): """Check sanity of hardware properties for this VM/product/platform. Returns: - ``True`` if hardware is sane, ``False`` if not. + bool: ``True`` if hardware is sane, ``False`` if not. """ result = True @@ -509,12 +508,12 @@ def _validate_helper(label, fn, *args): """Call validation function, catch errors and warn user instead. Args: - label (str): Label to prepend to any warning messages - fn (function): Validation function to call. - args: Arguments to validation function. + label (str): Label to prepend to any warning messages + fn (function): Validation function to call. + *args (list): Arguments to validation function. Returns: - bool: True if valid, False if invalid + bool: True if valid, False if invalid """ try: fn(*args) @@ -644,7 +643,7 @@ def network_descriptions(self): """The list of network descriptions currently defined in this VM. Returns: - list: List of network description strings + list: List of network description strings """ if self.network_section is None: return [] @@ -769,14 +768,14 @@ def __getattr__(self, name): """Transparently pass attribute lookups off to name_helper. Args: - name (str): Attribute being looked up. + name (str): Attribute being looked up. Returns: - Attribute value + Attribute value Raises: - AttributeError: Magic methods (``__foo``) will not be passed - through but will raise an AttributeError as usual. + AttributeError: Magic methods (``__foo``) will not be passed + through but will raise an AttributeError as usual. """ # Don't pass 'special' attributes through to the helper if re.match(r"^__", name): @@ -917,10 +916,10 @@ def _info_string_header(self, width): """Generate OVF/OVA file header for :meth:`info_string`. Args: - width (int): Line length to wrap to where possible. + width (int): Line length to wrap to where possible. Returns: - str: File header + str: File header """ str_list = [] str_list.append('-' * width) @@ -935,13 +934,12 @@ def _info_string_product(self, verbosity_option, wrapper): """Generate product information as part of :meth:`info_string`. Args: - verbosity_option (str): ``'brief'``, ``None`` (default), - or ``'verbose'`` - wrapper (textwrap.TextWrapper): Helper object for wrapping text - lines if needed. + verbosity_option (str): 'brief', None (default), or 'verbose' + wrapper (textwrap.TextWrapper): Helper object for wrapping text + lines if needed. Returns: - str: Product information + str: Product information """ if ((not any([self.product, self.vendor, self.version_short])) and (verbosity_option == 'brief' or not any([ @@ -973,11 +971,11 @@ def _info_string_annotation(self, wrapper): """Generate annotation information as part of :meth:`info_string`. Args: - wrapper (textwrap.TextWrapper): Helper object for wrapping - text lines if needed. + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. Returns: - Annotation information string, or None + str: Annotation information string, or None """ if self.annotation_section is None: return None @@ -1002,13 +1000,12 @@ def _info_string_eula(self, verbosity_option, wrapper): """Generate EULA information as part of :meth:`info_string`. Args: - verbosity_option (str): ``'brief'``, ``None`` (default), - or ``'verbose'`` - wrapper (textwrap.TextWrapper): Helper object for wrapping - text lines if needed. + verbosity_option (str): 'brief', None (default), or 'verbose' + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. Returns: - str: EULA information + str: EULA information """ # An OVF may have zero, one, or more eula_header = False @@ -1057,10 +1054,10 @@ def _info_strings_for_file(self, file_obj): Helper for :meth:`_info_string_files_disks`. Args: - file_obj (xml.etree.ElementTree.Element): File to inspect + file_obj (xml.etree.ElementTree.Element): File to inspect Returns: - tuple: (file_id, file_size, disk_id, disk_capacity, device_info) + tuple: (file_id, file_size, disk_id, disk_capacity, device_info) """ # FILE_SIZE is optional reported_size = file_obj.get(self.FILE_SIZE) @@ -1092,12 +1089,11 @@ def _info_string_files_disks(self, width, verbosity_option): """Describe files and disks as part of :meth:`info_string`. Args: - width (int): Line length to wrap to where possible. - verbosity_option (str): ``'brief'``, ``None`` (default), - or ``'verbose'`` + width (int): Line length to wrap to where possible. + verbosity_option (str): 'brief', None (default), or 'verbose' Returns: - File/disk information string, or None + str: File/disk information string, or None """ file_list = self.references.findall(self.FILE) disk_list = (self.disk_section.findall(self.DISK) @@ -1158,11 +1154,11 @@ def _info_string_hardware(self, wrapper): """Describe hardware subtypes as part of :meth:`info_string`. Args: - wrapper (textwrap.TextWrapper): Helper object for wrapping - text lines if needed. + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. Returns: - Hardware information string, or None + str: Hardware information string, or None """ virtual_system_types = self.system_types scsi_subtypes = list_union( @@ -1198,13 +1194,12 @@ def _info_string_networks(self, verbosity_option, wrapper): """Describe virtual networks as part of :meth:`info_string`. Args: - verbosity_option (str): ``'brief'``, ``None`` (default), - or ``'verbose'`` - wrapper (textwrap.TextWrapper): Helper object for wrapping - text lines if needed. + verbosity_option (str): 'brief', None (default), or 'verbose' + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. Returns: - Network information string, or None + str: Network information string, or None """ if self.network_section is None: return None @@ -1239,13 +1234,12 @@ def _info_string_nics(self, verbosity_option, wrapper): """Describe NICs as part of :meth:`info_string`. Args: - verbosity_option (str): ``'brief'``, ``None`` (default), - or ``'verbose'`` - wrapper (textwrap.TextWrapper): Helper object for wrapping - text lines if needed. + verbosity_option (str): 'brief', None (default), or 'verbose' + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. Returns: - NIC information string, or None + str: NIC information string, or None """ if verbosity_option == 'brief': return None @@ -1280,11 +1274,11 @@ def _info_string_environment(self, wrapper): """Describe environment for :meth:`info_string`. Args: - wrapper (textwrap.TextWrapper): Helper object for wrapping - text lines if needed. + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. Returns: - Environment information string, or None + str: Environment information string, or None """ if not self.environment_transports: return None @@ -1300,13 +1294,12 @@ def _info_string_properties(self, verbosity_option, wrapper): """Describe config properties for :meth:`info_string`. Args: - verbosity_option (str): ``'brief'``, ``None`` (default), - or ``'verbose'`` - wrapper (textwrap.TextWrapper): Helper object for wrapping - text lines if needed. + verbosity_option (str): 'brief', None (default), or 'verbose' + wrapper (textwrap.TextWrapper): Helper object for wrapping + text lines if needed. Returns: - Property information string, or None + str: Property information string, or None """ properties = self.environment_properties if not properties: @@ -1357,12 +1350,11 @@ def info_string(self, width=79, verbosity_option=None): """Get a descriptive string summarizing the contents of this OVF. Args: - width (int): Line length to wrap to where possible. - verbosity_option (str): ``'brief'``, ``None`` (default), - or ``'verbose'`` + width (int): Line length to wrap to where possible. + verbosity_option (str): 'brief', None (default), or 'verbose' Returns: - Wrapped, appropriately verbose string. + str: Wrapped, appropriately verbose string. """ # Supposedly it's quicker to construct a list of strings then merge # them all together with 'join()' rather than it is to repeatedly @@ -1400,10 +1392,10 @@ def device_info_str(self, device_item): """Get a one-line summary of a hardware device. Args: - device_item (OVFItem): Device to summarize + device_item (OVFItem): Device to summarize Returns: - Descriptive string such as "harddisk @ IDE 1:0" + str: Descriptive string such as "harddisk @ IDE 1:0" """ if device_item is None: return "" @@ -1433,11 +1425,11 @@ def profile_info_list(self, width=79, verbose=False): """Get a list describing available configuration profiles. Args: - width (int): Line length to wrap to if possible - verbose (bool): if True, generate multiple lines per profile + width (int): Line length to wrap to if possible + verbose (bool): if True, generate multiple lines per profile Returns: - (header, list) + tuple: (header, list) """ str_list = [] @@ -1510,12 +1502,11 @@ def profile_info_string(self, width=79, verbosity_option=None): """Get a string summarizing available configuration profiles. Args: - width (int): Line length to wrap to if possible - verbosity_option (str): ``'brief'``, ``None`` (default), - or ``'verbose'`` + width (int): Line length to wrap to if possible + verbosity_option (str): 'brief', None (default), or 'verbose' Returns: - Appropriately formatted and verbose string. + str: Appropriately formatted and verbose string. """ header, str_list = self.profile_info_list( width, (verbosity_option != 'brief')) @@ -1525,9 +1516,9 @@ def create_configuration_profile(self, pid, label, description): """Create or update a configuration profile with the given ID. Args: - pid (str): Profile identifier - label (str): Brief descriptive label for the profile - description (str): Verbose description of the profile + pid (str): Profile identifier + label (str): Brief descriptive label for the profile + description (str): Verbose description of the profile """ self.deploy_opt_section = self._ensure_section( self.DEPLOY_OPT_SECTION, "Configuration Profiles") @@ -1550,10 +1541,10 @@ def delete_configuration_profile(self, profile): """Delete the profile with the given ID. Args: - profile (str): Profile ID to delete. + profile (str): Profile ID to delete. Raises: - LookupError: if the profile does not exist. + LookupError: if the profile does not exist. """ cfg = self.find_child(self.deploy_opt_section, self.CONFIG, attrib={self.CONFIG_ID: profile}) @@ -1586,8 +1577,8 @@ def set_cpu_count(self, cpus, profile_list): """Set the number of CPUs. Args: - cpus (int): Number of CPUs - profile_list (list): Change only the given profiles + cpus (int): Number of CPUs + profile_list (list): Change only the given profiles """ logger.info("Updating CPU count in OVF under profile %s to %s", profile_list, cpus) @@ -1601,8 +1592,8 @@ def set_memory(self, megabytes, profile_list): """Set the amount of RAM, in megabytes. Args: - megabytes (int): Memory value, in megabytes - profile_list (list): Change only the given profiles + megabytes (int): Memory value, in megabytes + profile_list (list): Change only the given profiles """ logger.info("Updating RAM in OVF under profile %s to %s", profile_list, megabytes) @@ -1620,8 +1611,8 @@ def set_nic_types(self, type_list, profile_list): """Set the hardware type(s) for NICs. Args: - type_list (list): NIC hardware type(s) - profile_list (list): Change only the given profiles. + type_list (list): NIC hardware type(s) + profile_list (list): Change only the given profiles. """ # Just to be safe... type_list = [canonicalize_nic_subtype(t) for t in type_list] @@ -1635,10 +1626,10 @@ def get_nic_count(self, profile_list): """Get the number of NICs under the given profile(s). Args: - profile_list (list): Profile(s) of interest. + profile_list (list): Profile(s) of interest. Returns: - dict: ``{ profile_name : nic_count }`` + dict: ``{ profile_name : nic_count }`` """ return self.hardware.get_item_count_per_profile('ethernet', profile_list) @@ -1647,8 +1638,8 @@ def set_nic_count(self, count, profile_list): """Set the given profile(s) to have the given number of NICs. Args: - count (int): number of NICs - profile_list (list): Change only the given profiles + count (int): number of NICs + profile_list (list): Change only the given profiles """ logger.info("Updating NIC count in OVF under profile %s to %s", profile_list, count) @@ -1662,8 +1653,8 @@ def create_network(self, label, description): Also serves to update the description of an existing network label. Args: - label (str): Brief label for the network - description (str): Verbose description of the network + label (str): Brief label for the network + description (str): Verbose description of the network """ self.network_section = self._ensure_section( self.NETWORK_SECTION, @@ -1681,8 +1672,8 @@ def set_nic_networks(self, network_list, profile_list): NICs, will use the last entry in the list for all remaining NICs. Args: - network_list (list): List of networks to map NICs to - profile_list (list): Change only the given profiles + network_list (list): List of networks to map NICs to + profile_list (list): Change only the given profiles """ self.hardware.set_item_values_per_profile('ethernet', self.CONNECTION, @@ -1698,8 +1689,8 @@ def set_nic_mac_addresses(self, mac_list, profile_list): will use the last entry in the list for all remaining NICs. Args: - mac_list (list): List of MAC addresses to assign to NICs - profile_list (list): Change only the given profiles + mac_list (list): List of MAC addresses to assign to NICs + profile_list (list): Change only the given profiles """ self.hardware.set_item_values_per_profile('ethernet', self.ADDRESS, @@ -1711,8 +1702,8 @@ def set_nic_names(self, name_list, profile_list): """Set the device names for NICs under the given profile(s). Args: - name_list (list): List of names to assign. - profile_list (list): Change only the given profiles + name_list (list): List of names to assign. + profile_list (list): Change only the given profiles """ self.hardware.set_item_values_per_profile('ethernet', self.ELEMENT_NAME, @@ -1723,10 +1714,10 @@ def get_serial_count(self, profile_list): """Get the number of serial ports under the given profile(s). Args: - profile_list (list): Profile(s) of interest. + profile_list (list): Profile(s) of interest. Returns: - dict: ``{ profile_name : serial_count }`` + dict: ``{ profile_name : serial_count }`` """ return self.hardware.get_item_count_per_profile('serial', profile_list) @@ -1734,8 +1725,8 @@ def set_serial_count(self, count, profile_list): """Set the given profile(s) to have the given number of serial ports. Args: - count (int): Number of serial ports - profile_list (list): Change only the given profiles + count (int): Number of serial ports + profile_list (list): Change only the given profiles """ logger.info("Updating serial port count under profile %s to %s", profile_list, count) @@ -1745,8 +1736,8 @@ def set_serial_connectivity(self, conn_list, profile_list): """Set the serial port connectivity under the given profile(s). Args: - conn_list (list): List of connectivity strings - profile_list (list): Change only the given profiles + conn_list (list): List of connectivity strings + profile_list (list): Change only the given profiles """ self.hardware.set_item_values_per_profile('serial', self.ADDRESS, conn_list, @@ -1756,10 +1747,10 @@ def get_serial_connectivity(self, profile): """Get the serial port connectivity strings under the given profile. Args: - profile (str): Profile of interest. + profile (str): Profile of interest. Returns: - list: connectivity strings + list: connectivity strings """ return [item.get_value(self.ADDRESS) for item in self.hardware.find_all_items('serial', profile_list=[profile])] @@ -1768,8 +1759,8 @@ def set_scsi_subtypes(self, type_list, profile_list): """Set the device subtype(s) for the SCSI controller(s). Args: - type_list (list): SCSI subtype string list - profile_list (list): Change only the given profiles + type_list (list): SCSI subtype string list + profile_list (list): Change only the given profiles """ # TODO validate supported types by platform self.hardware.set_value_for_all_items('scsi', @@ -1781,8 +1772,8 @@ def set_ide_subtypes(self, type_list, profile_list): """Set the device subtype(s) for the IDE controller(s). Args: - type_list (list): IDE subtype string list - profile_list (list): Change only the given profiles + type_list (list): IDE subtype string list + profile_list (list): Change only the given profiles """ # TODO validate supported types by platform self.hardware.set_value_for_all_items('ide', @@ -1794,10 +1785,10 @@ def get_property_value(self, key): """Get the value of the given property. Args: - key (str): Property identifier + key (str): Property identifier Returns: - Value of this property as a string, or ``None`` + str: Value of this property as a string, or ``None`` """ if self.ovf_version < 1.0 or self.product_section is None: return None @@ -1814,14 +1805,14 @@ def _validate_value_for_property(self, prop, value): it knows nothing of the property's actual meaning. Args: - prop (xml.etree.ElementTree.Element): Existing Property element. - value (str): Proposed value to set for this property. + prop (xml.etree.ElementTree.Element): Existing Property element. + value (str): Proposed value to set for this property. Returns: - str: the value, potentially canonicalized. + str: the value, potentially canonicalized. Raises: - ValueUnsupportedError: if the value does not meet criteria. + ValueUnsupportedError: if the value does not meet criteria. """ key = prop.get(self.PROP_KEY) @@ -1865,20 +1856,20 @@ def set_property_value(self, key, value, """Set the value of the given property (converting value if needed). Args: - key (str): Property identifier - value (object): Value to set for this property - user_configurable (bool): Should this property be configurable at - deployment time by the user? - property_type (str): Value type - 'string' or 'boolean' - label (str): Brief explanatory label for this property - description (str): Detailed description of this property + key (str): Property identifier + value (object): Value to set for this property + user_configurable (bool): Should this property be configurable at + deployment time by the user? + property_type (str): Value type - 'string' or 'boolean' + label (str): Brief explanatory label for this property + description (str): Detailed description of this property Returns: - str: the (converted) value that was set. + str: the (converted) value that was set. Raises: - NotImplementedError: if :attr:`ovf_version` is less than 1.0; - OVF version 0.9 is not currently supported. + NotImplementedError: if :attr:`ovf_version` is less than 1.0; + OVF version 0.9 is not currently supported. """ if self.ovf_version < 1.0: raise NotImplementedError("No support for setting environment " @@ -1921,14 +1912,14 @@ def config_file_to_properties(self, file_path, user_configurable=None): """Import each line of a text file into a configuration property. Args: - file_path (str): File name to import. - user_configurable (bool): Should the resulting properties be - configurable at deployment time by the user? + file_path (str): File name to import. + user_configurable (bool): Should the resulting properties be + configurable at deployment time by the user? Raises: - NotImplementedError: if the :attr:`platform` for this OVF - does not define - :const:`~COT.platforms.GenericPlatform.LITERAL_CLI_STRING` + NotImplementedError: if the :attr:`platform` for this OVF + does not define + :const:`~COT.platforms.GenericPlatform.LITERAL_CLI_STRING` """ i = 0 if not self.platform.LITERAL_CLI_STRING: @@ -1954,15 +1945,15 @@ def convert_disk_if_needed(self, disk_image, kind): * CD-ROM iso images are accepted without change. Args: - disk_image (COT.disks.DiskRepresentation): Image to inspect and - possibly convert - kind (str): Image type (harddisk/cdrom) + disk_image (COT.disks.DiskRepresentation): Image to inspect and + possibly convert + kind (str): Image type (harddisk/cdrom) Returns: - * :attr:`disk_image`, if no conversion was required - * or a new :class:`~COT.disks.DiskRepresentation` instance - representing a converted image that has been created in - :attr:`output_dir`. + DiskRepresentation: :attr:`disk_image`, if no conversion was + required, or a new :class:`~COT.disks.DiskRepresentation` instance + representing a converted image that has been created in + :attr:`output_dir`. """ if kind != 'harddisk': logger.debug("No disk conversion needed") @@ -1985,15 +1976,15 @@ def search_from_filename(self, filename): ``Item`` entries. Args: - filename (str): Filename to search from + filename (str): Filename to search from Returns: - ``(file, disk, ctrl_item, disk_item)``, any or all of which - may be ``None`` + tuple: ``(file, disk, ctrl_item, disk_item)``, any or all of which + may be ``None`` Raises: - LookupError: If the ``disk_item`` is found but no ``ctrl_item`` - is found to be its parent. + LookupError: If the ``disk_item`` is found but no ``ctrl_item`` is + found to be its parent. """ file_obj = None disk = None @@ -2033,17 +2024,17 @@ def search_from_file_id(self, file_id): ``Item`` entries. Args: - file_id (str): File ID to search from + file_id (str): File ID to search from Returns: - tuple: ``(file, disk, ctrl_item, disk_item)``, any or all of which - may be ``None`` + tuple: ``(file, disk, ctrl_item, disk_item)``, any or all of which + may be ``None`` Raises: - LookupError: If the ``disk`` entry is found but no corresponding - ``file`` is found. - LookupError: If the ``disk_item`` is found but no ``ctrl_item`` - is found to be its parent. + LookupError: If the ``disk`` entry is found but no corresponding + ``file`` is found. + LookupError: If the ``disk_item`` is found but no ``ctrl_item`` is + found to be its parent. """ if file_id is None: return (None, None, None, None) @@ -2089,12 +2080,12 @@ def search_from_controller(self, controller, address): to find matching ``File`` and/or ``Disk``. Args: - controller (str): ``'ide'`` or ``'scsi'`` - address (str): Device address such as ``'1:0'`` + controller (str): ``'ide'`` or ``'scsi'`` + address (str): Device address such as ``'1:0'`` Returns: - ``(file, disk, ctrl_item, disk_item)``, any or all of which - may be ``None`` + tuple: ``(file, disk, ctrl_item, disk_item)``, any or all of which + may be ``None`` """ if controller is None or address is None: return (None, None, None, None) @@ -2172,10 +2163,10 @@ def find_open_controller(self, controller_type): """Find the first open slot on a controller of the given type. Args: - controller_type (str): ``'ide'`` or ``'scsi'`` + controller_type (str): ``'ide'`` or ``'scsi'`` Returns: - ``(ctrl_item, address_string)`` or ``(None, None)`` + tuple: ``(ctrl_item, address_string)`` or ``(None, None)`` """ for ctrl_item in self.hardware.find_all_items(controller_type): ctrl_instance = ctrl_item.get_value(self.INSTANCE_ID) @@ -2203,10 +2194,10 @@ def get_id_from_file(self, file_obj): """Get the file ID from the given opaque file object. Args: - file_obj (xml.etree.ElementTree.Element): 'File' element + file_obj (xml.etree.ElementTree.Element): 'File' element Returns: - str: 'id' attribute value of this element + str: 'id' attribute value of this element """ return file_obj.get(self.FILE_ID) @@ -2214,10 +2205,10 @@ def get_path_from_file(self, file_obj): """Get the file path from the given opaque file object. Args: - file_obj (xml.etree.ElementTree.Element): 'File' element + file_obj (xml.etree.ElementTree.Element): 'File' element Returns: - str: 'href' attribute value of this element + str: 'href' attribute value of this element """ return file_obj.get(self.FILE_HREF) @@ -2225,10 +2216,10 @@ def get_file_ref_from_disk(self, disk): """Get the file reference from the given opaque disk object. Args: - disk (xml.etree.ElementTree.Element): 'Disk' element + disk (xml.etree.ElementTree.Element): 'Disk' element Returns: - str: 'fileRef' attribute value of this element + str: 'fileRef' attribute value of this element """ return disk.get(self.DISK_FILE_REF) @@ -2236,13 +2227,12 @@ def get_common_subtype(self, device_type): """Get the sub-type common to all devices of the given type. Args: - device_type (str): Device type such as ``'ide'`` or ``'memory'``. + device_type (str): Device type such as ``'ide'`` or ``'memory'``. Returns: - Subtype string common to all devices of the type. - - ``None``, if multiple such devices exist and they do not all - have the same sub-type. + str: Subtype string common to all devices of the type, or ``None``, + if multiple such devices exist and they do not all have the same + sub-type. """ subtype = None for item in self.hardware.find_all_items(device_type): @@ -2262,18 +2252,18 @@ def check_sanity_of_disk_device(self, disk, file_obj, """Check if the given disk is linked properly to the other objects. Args: - disk (xml.etree.ElementTree.Element): Disk object to validate - file_obj (xml.etree.ElementTree.Element): File object which this - disk should be linked to (optional) - disk_item (OVFItem): Disk device object which should link to - this disk (optional) - ctrl_item (OVFItem): Controller device object which should link - to the :attr:`disk_item` + disk (xml.etree.ElementTree.Element): Disk object to validate + file_obj (xml.etree.ElementTree.Element): File object which this + disk should be linked to (optional) + disk_item (OVFItem): Disk device object which should link to this + disk (optional) + ctrl_item (OVFItem): Controller device object which should link + to the :attr:`disk_item` Raises: - ValueMismatchError: if the given items are not linked properly. - ValueUnsupportedError: if the :attr:`disk_item` has a - ``HostResource`` value in an unrecognized or invalid format. + ValueMismatchError: if the given items are not linked properly. + ValueUnsupportedError: if the :attr:`disk_item` has a + ``HostResource`` value in an unrecognized or invalid format. """ if disk_item is None: return @@ -2307,15 +2297,15 @@ def add_file(self, file_path, file_id, file_obj=None, disk=None): """Add a new file object to the VM or overwrite the provided one. Args: - file_path (str): Path to file to add - file_id (str): Identifier string for the file in the VM - file_obj (xml.etree.ElementTree.Element): Existing file object - to overwrite - disk (xml.etree.ElementTree.Element): Existing disk object - referencing :attr:`file`. + file_path (str): Path to file to add + file_id (str): Identifier string for the file in the VM + file_obj (xml.etree.ElementTree.Element): Existing file object to + overwrite + disk (xml.etree.ElementTree.Element): Existing disk object + referencing :attr:`file`. Returns: - xml.etree.ElementTree.Element: New or updated file object + xml.etree.ElementTree.Element: New or updated file object """ logger.debug("Adding File to OVF") @@ -2366,14 +2356,14 @@ def remove_file(self, file_obj, disk=None, disk_drive=None): """Remove the given file object from the VM. Args: - file_obj (xml.etree.ElementTree.Element): File object to remove - disk (xml.etree.ElementTree.Element): Disk object referencing - :attr:`file` - disk_drive (OVFItem): Disk drive mapping :attr:`file` to a device + file_obj (xml.etree.ElementTree.Element): File object to remove + disk (xml.etree.ElementTree.Element): Disk object referencing + :attr:`file` + disk_drive (OVFItem): Disk drive mapping :attr:`file` to a device Raises: - ValueUnsupportedError: If the ``disk_drive`` is a device type - other than 'cdrom' or 'harddisk' + ValueUnsupportedError: If the ``disk_drive`` is a device type other + than 'cdrom' or 'harddisk' """ self.references.remove(file_obj) del self._file_references[file_obj.get(self.FILE_HREF)] @@ -2399,14 +2389,13 @@ def add_disk(self, disk_repr, file_id, drive_type, disk=None): """Add a new disk object to the VM or overwrite the provided one. Args: - disk_repr (COT.disks.DiskRepresentation): Disk file representation - file_id (str): Identifier string for the file/disk mapping - drive_type (str): 'harddisk' or 'cdrom' - disk (xml.etree.ElementTree.Element): Existing disk object to - overwrite + disk_repr (COT.disks.DiskRepresentation): Disk file representation + file_id (str): Identifier string for the file/disk mapping + drive_type (str): 'harddisk' or 'cdrom' + disk (xml.etree.ElementTree.Element): Existing object to overwrite Returns: - xml.etree.ElementTree.Element: New or updated disk object + xml.etree.ElementTree.Element: New or updated disk object """ if drive_type != 'harddisk': if disk is not None: @@ -2453,18 +2442,17 @@ def add_controller_device(self, device_type, subtype, address, """Create a new IDE or SCSI controller, or update existing one. Args: - device_type (str): ``'ide'`` or ``'scsi'`` - subtype (object): (Optional) subtype string such as ``'virtio'`` - or list of subtype strings - address (int): Controller address such as 0 or 1 (optional) - ctrl_item (OVFItem): Existing controller device to update - (optional) + device_type (str): ``'ide'`` or ``'scsi'`` + subtype (object): (Optional) subtype string such as ``'virtio'`` + or list of subtype strings + address (int): Controller address such as 0 or 1 (optional) + ctrl_item (OVFItem): Existing controller device to update (optional) Returns: - OVFItem: New or updated controller device object + OVFItem: New or updated controller device object Raises: - ValueTooHighError: if no more controllers can be created + ValueTooHighError: if no more controllers can be created """ if ctrl_item is None: logger.info("Controller not found, adding new Item") @@ -2497,19 +2485,19 @@ def _create_new_disk_device(self, drive_type, address, name, ctrl_item): """Helper for :meth:`add_disk_device`, in the case of no prior Item. Args: - drive_type (str): ``'harddisk'`` or ``'cdrom'`` - address (str): Address on controller, such as "1:0" (optional) - name (str): Device name string (optional) - ctrl_item (OVFItem): Controller object to serve as parent + drive_type (str): ``'harddisk'`` or ``'cdrom'`` + address (str): Address on controller, such as "1:0" (optional) + name (str): Device name string (optional) + ctrl_item (OVFItem): Controller object to serve as parent Returns: - tuple: (disk_item, disk_name) + tuple: (disk_item, disk_name) Raises: - ValueTooHighError: if the requested address is out of range - for the given controller, or if the controller is already full. - ValueUnsupportedError: if ``name`` is not specified and - ``disk_type`` is not 'harddisk' or 'cdrom'. + ValueTooHighError: if the requested address is out of range + for the given controller, or if the controller is already full. + ValueUnsupportedError: if ``name`` is not specified and + ``disk_type`` is not 'harddisk' or 'cdrom'. """ ctrl_instance = ctrl_item.get_value(self.INSTANCE_ID) if address is None: @@ -2555,20 +2543,20 @@ def add_disk_device(self, drive_type, address, name, description, """Create a new disk hardware device or overwrite an existing one. Args: - drive_type (str): ``'harddisk'`` or ``'cdrom'`` - address (str): Address on controller, such as "1:0" (optional) - name (str): Device name string (optional) - description (str): Description string (optional) - disk (xml.etree.ElementTree.Element): Disk object to map to - this device - file_obj (xml.etree.ElementTree.Element): File object to map to - this device - ctrl_item (OVFItem): Controller object to serve as parent - disk_item (OVFItem): Existing disk device to update instead of - making a new device. + drive_type (str): ``'harddisk'`` or ``'cdrom'`` + address (str): Address on controller, such as "1:0" (optional) + name (str): Device name string (optional) + description (str): Description string (optional) + disk (xml.etree.ElementTree.Element): Disk object to map to + this device + file_obj (xml.etree.ElementTree.Element): File object to map to + this device + ctrl_item (OVFItem): Controller object to serve as parent + disk_item (OVFItem): Existing disk device to update instead of + making a new device. Returns: - xml.etree.ElementTree.Element: New or updated disk device object. + xml.etree.ElementTree.Element: New or updated disk device object. """ if disk_item is None: logger.info("Disk Item not found, adding new Item") @@ -2603,14 +2591,13 @@ def untar(self, file_path): """Untar the OVF descriptor from an .ova to the working directory. Args: - file_path (str): OVA file path + file_path (str): OVA file path Returns: - Path to extracted OVF descriptor + str: Path to extracted OVF descriptor Raises: - VMInitError: if the given file does not represent a valid - OVA archive. + VMInitError: if the given file doesn't represent a valid OVA archive. """ logger.verbose("Untarring %s to working directory %s", file_path, self.working_dir) @@ -2667,12 +2654,12 @@ def generate_manifest(self, ovf_file): """Construct the manifest file for this package, if possible. Args: - ovf_file (str): OVF descriptor file path + ovf_file (str): OVF descriptor file path Returns: - bool: True if the manifest was successfully generated, - False if not successful (such as if checksum helper tools are - unavailable). + bool: True if the manifest was successfully generated, + False if not successful (such as if checksum helper tools are + unavailable). """ (prefix, _) = os.path.splitext(ovf_file) logger.verbose("Generating manifest for %s", ovf_file) @@ -2704,8 +2691,8 @@ def tar(self, ovf_descriptor, tar_file): """Create a .ova tar file based on the given OVF descriptor. Args: - ovf_descriptor (str): File path for an OVF descriptor - tar_file (str): File path for the desired OVA archive. + ovf_descriptor (str): File path for an OVF descriptor + tar_file (str): File path for the desired OVA archive. """ logger.verbose("Creating tar file %s", tar_file) @@ -2750,16 +2737,16 @@ def _ensure_section(self, section_tag, info_string, """If the OVF doesn't already have the given Section, create it. Args: - section_tag (str): XML tag of the desired section. - info_string (str): Info string to set if a new Section is created. - attrib (dict): Attributes to filter by when looking for any - existing section (optional). - parent (xml.etree.ElementTree.Element): Parent element (optional). - If not specified, :attr:`envelope` will be the parent. + section_tag (str): XML tag of the desired section. + info_string (str): Info string to set if a new Section is created. + attrib (dict): Attributes to filter by when looking for any existing + section (optional). + parent (xml.etree.ElementTree.Element): Parent element (optional). + If not specified, :attr:`envelope` will be the parent. Returns: - xml.etree.ElementTree.Element: Section element that was found or - created + xml.etree.ElementTree.Element: Section element that was found or + created """ if parent is None: parent = self.envelope @@ -2793,12 +2780,12 @@ def _set_product_section_child(self, child_tag, child_text): Creates the ProductSection itself if necessary. Args: - child_tag (str): XML tag of the product section child element. - child_text (str): Text to set for the child element. + child_tag (str): XML tag of the product section child element. + child_text (str): Text to set for the child element. Returns: - xml.etree.ElementTree.Element: The product section element that - was updated or created + xml.etree.ElementTree.Element: The product section element that + was updated or created """ self.product_section = self._ensure_section( self.PRODUCT_SECTION, @@ -2812,11 +2799,10 @@ def find_parent_from_item(self, item): """Find the parent Item of the given Item. Args: - item (OVFItem): Item whose parent is desired + item (OVFItem): Item whose parent is desired Returns: - :class:`OVFItem` instance representing the parent device, - or None + OVFItem: instance representing the parent device, or None """ if item is None: return None @@ -2833,10 +2819,10 @@ def find_item_from_disk(self, disk): """Find the disk Item that references the given Disk. Args: - disk (xml.etree.ElementTree.Element): Disk element + disk (xml.etree.ElementTree.Element): Disk element Returns: - :class:`OVFItem` instance, or None + OVFItem: Corresponding instance, or None """ if disk is None: return None @@ -2858,10 +2844,10 @@ def find_item_from_file(self, file_obj): """Find the disk Item that references the given File. Args: - file_obj (xml.etree.ElementTree.Element): File element + file_obj (xml.etree.ElementTree.Element): File element Returns: - :class:`OVFItem` instance, or None. + OVFItem: Corresponding instance, or None. """ if file_obj is None: return None @@ -2882,11 +2868,10 @@ def find_disk_from_file_id(self, file_id): """Find the Disk that uses the given file_id for backing. Args: - file_id (str): File identifier string + file_id (str): File identifier string Returns: - xml.etree.ElementTree.Element: Disk element matching the file, - or None + xml.etree.ElementTree.Element: Disk matching the file, or None """ if file_id is None or self.disk_section is None: return None @@ -2898,13 +2883,13 @@ def find_empty_drive(self, drive_type): """Find a disk device that exists but contains no data. Args: - drive_type (str): Either 'cdrom' or 'harddisk' + drive_type (str): Either 'cdrom' or 'harddisk' Returns: - :class:`OVFItem` representing this disk device, or None. + OVFItem: Instance representing this disk device, or None. Raises: - ValueUnsupportedError: if ``drive_type`` is unrecognized. + ValueUnsupportedError: if ``drive_type`` is unrecognized. """ if drive_type == 'cdrom': # Find a drive that has no HostResource property @@ -2936,13 +2921,13 @@ def find_device_location(self, device): """Find the controller type and address of a given device object. Args: - device (OVFItem): Hardware device object. + device (OVFItem): Hardware device object. Returns: - ``(type, address)``, such as ``("ide", "1:0")``. + tuple: ``(type, address)``, such as ``("ide", "1:0")``. Raises: - LookupError: if the controller is not found. + LookupError: if the controller is not found. """ controller = self.find_parent_from_item(device) if controller is None: @@ -2955,10 +2940,10 @@ def get_id_from_disk(self, disk): """Get the identifier string associated with the given Disk object. Args: - disk (xml.etree.ElementTree.Element): Disk object to inspect + disk (xml.etree.ElementTree.Element): Disk object to inspect Returns: - str: Disk identifier + str: Disk identifier """ return disk.get(self.DISK_ID) @@ -2966,10 +2951,10 @@ def get_capacity_from_disk(self, disk): """Get the capacity of the given Disk in bytes. Args: - disk (xml.etree.ElementTree.Element): Disk element to inspect + disk (xml.etree.ElementTree.Element): Disk element to inspect Returns: - int: Disk capacity, in bytes + int: Disk capacity, in bytes """ cap = int(disk.get(self.DISK_CAPACITY)) cap_units = disk.get(self.DISK_CAP_UNITS, 'byte') @@ -2982,8 +2967,8 @@ def set_capacity_of_disk(self, disk, capacity_bytes): instead of 8589934592 bytes). Args: - disk (xml.etree.ElementTree.Element): Disk to update - capacity_bytes (int): Disk capacity, in bytes + disk (xml.etree.ElementTree.Element): Disk to update + capacity_bytes (int): Disk capacity, in bytes """ if self.ovf_version < 1.0: # In OVF 0.9 only bytes is supported as a unit diff --git a/COT/platforms/__init__.py b/COT/platforms/__init__.py index b90024a..f2ba159 100644 --- a/COT/platforms/__init__.py +++ b/COT/platforms/__init__.py @@ -74,10 +74,10 @@ 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' + product_class (str): String such as 'com.cisco.iosv' Returns: - bool: Whether product_class is known. + bool: Whether product_class is known. """ return product_class in PRODUCT_PLATFORM_MAP @@ -86,10 +86,10 @@ 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' + product_class (str): String such as 'com.cisco.iosv' Returns: - class: GenericPlatform or a subclass of it + class: GenericPlatform or a subclass of it """ if product_class is None: return GenericPlatform diff --git a/COT/platforms/cisco_csr1000v.py b/COT/platforms/cisco_csr1000v.py index 3f92e7b..a34b594 100644 --- a/COT/platforms/cisco_csr1000v.py +++ b/COT/platforms/cisco_csr1000v.py @@ -38,9 +38,9 @@ def controller_type_for_device(cls, device_type): """CSR1000V uses SCSI for hard disks and IDE for CD-ROMs. Args: - device_type (str): 'harddisk' or 'cdrom' + device_type (str): 'harddisk' or 'cdrom' Returns: - str: 'ide' for CD-ROM, 'scsi' for hard disk + str: 'ide' for CD-ROM, 'scsi' for hard disk """ if device_type == 'harddisk': return 'scsi' @@ -59,12 +59,11 @@ def guess_nic_name(cls, nic_number): support that. Args: - nic_number (int): Nth NIC to name. + nic_number (int): Nth NIC to name. Returns: - str: - * "GigabitEthernet1" - * "GigabitEthernet2" - * etc. + * "GigabitEthernet1" + * "GigabitEthernet2" + * etc. """ return "GigabitEthernet" + str(nic_number) @@ -73,13 +72,13 @@ def validate_cpu_count(cls, cpus): """CSR1000V supports 1, 2, 4, or 8 CPUs. Args: - cpus (int): Number of CPUs. + cpus (int): Number of CPUs. Raises: - ValueTooLowError: if ``cpus`` is less than 1 - ValueTooHighError: if ``cpus`` is more than 8 - ValueUnsupportedError: if ``cpus`` is an unsupported value - between 1 and 8 + ValueTooLowError: if ``cpus`` is less than 1 + ValueTooHighError: if ``cpus`` is more than 8 + ValueUnsupportedError: if ``cpus`` is an unsupported value + between 1 and 8 """ validate_int(cpus, 1, 8, "CPUs") if cpus not in [1, 2, 4, 8]: @@ -90,11 +89,11 @@ def validate_memory_amount(cls, mebibytes): """Minimum 2.5 GiB, max 8 GiB. Args: - mebibytes (int): RAM, in MiB. + mebibytes (int): RAM, in MiB. Raises: - ValueTooLowError: if ``mebibytes`` is less than 2560 - ValueTooHighError: if ``mebibytes`` is more than 8192 + 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") @@ -106,11 +105,11 @@ def validate_nic_count(cls, count): """CSR1000V requires 3 NICs and supports up to 26. Args: - count (int): Number of NICs. + count (int): Number of NICs. Raises: - ValueTooLowError: if ``count`` is less than 3 - ValueTooHighError: if ``count`` is more than 26 + ValueTooLowError: if ``count`` is less than 3 + ValueTooHighError: if ``count`` is more than 26 """ validate_int(count, 3, 26, "NIC count") @@ -119,10 +118,10 @@ def validate_serial_count(cls, count): """CSR1000V supports 0-2 serial ports. Args: - count (int): Number of serial ports. + count (int): Number of serial ports. Raises: - ValueTooLowError: if ``count`` is less than 0 - ValueTooHighError: if ``count`` is more than 2 + ValueTooLowError: if ``count`` is less than 0 + ValueTooHighError: if ``count`` is more than 2 """ validate_int(count, 0, 2, "serial ports") diff --git a/COT/platforms/cisco_iosv.py b/COT/platforms/cisco_iosv.py index fb152ac..dcd9d10 100644 --- a/COT/platforms/cisco_iosv.py +++ b/COT/platforms/cisco_iosv.py @@ -38,12 +38,11 @@ def guess_nic_name(cls, nic_number): """GigabitEthernet0/0, GigabitEthernet0/1, etc. Args: - nic_number (int): Nth NIC to name. + nic_number (int): Nth NIC to name. Returns: - str: - * "GigabitEthernet0/0" - * "GigabitEthernet0/1" - * etc. + * "GigabitEthernet0/0" + * "GigabitEthernet0/1" + * etc. """ return "GigabitEthernet0/" + str(nic_number - 1) @@ -52,11 +51,11 @@ def validate_cpu_count(cls, cpus): """IOSv only supports a single CPU. Args: - cpus (int): Number of CPUs. + cpus (int): Number of CPUs. Raises: - ValueTooLowError: if ``cpus`` is less than 1 - ValueTooHighError: if ``cpus`` is more than 1 + ValueTooLowError: if ``cpus`` is less than 1 + ValueTooHighError: if ``cpus`` is more than 1 """ validate_int(cpus, 1, 1, "CPUs") @@ -65,11 +64,11 @@ def validate_memory_amount(cls, mebibytes): """IOSv has minimum 192 MiB (with minimal feature set), max 3 GiB. Args: - mebibytes (int): RAM, in MiB. + mebibytes (int): RAM, in MiB. Raises: - ValueTooLowError: if ``mebibytes`` is less than 192 - ValueTooHighError: if ``mebibytes`` is more than 3072 + 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") @@ -85,11 +84,11 @@ def validate_nic_count(cls, count): """IOSv supports up to 16 NICs. Args: - count (int): Number of NICs. + count (int): Number of NICs. Raises: - ValueTooLowError: if ``count`` is less than 0 - ValueTooHighError: if ``count`` is more than 16 + ValueTooLowError: if ``count`` is less than 0 + ValueTooHighError: if ``count`` is more than 16 """ validate_int(count, 0, 16, "NICs") @@ -98,10 +97,10 @@ def validate_serial_count(cls, count): """IOSv requires 1-2 serial ports. Args: - count (int): Number of serial ports. + count (int): Number of serial ports. Raises: - ValueTooLowError: if ``count`` is less than 1 - ValueTooHighError: if ``count`` is more than 2 + ValueTooLowError: if ``count`` is less than 1 + ValueTooHighError: if ``count`` is more than 2 """ validate_int(count, 1, 2, "serial ports") diff --git a/COT/platforms/cisco_iosxrv.py b/COT/platforms/cisco_iosxrv.py index 6b53f7a..ada4a8b 100644 --- a/COT/platforms/cisco_iosxrv.py +++ b/COT/platforms/cisco_iosxrv.py @@ -47,14 +47,13 @@ def guess_nic_name(cls, nic_number): """MgmtEth0/0/CPU0/0, GigabitEthernet0/0/0/0, Gig0/0/0/1, etc. Args: - nic_number (int): Nth NIC to name. + nic_number (int): Nth NIC to name. Returns: - str: - * "MgmtEth0/0/CPU0/0" - * "GigabitEthernet0/0/0/0" - * "GigabitEthernet0/0/0/1" - * etc. + * "MgmtEth0/0/CPU0/0" + * "GigabitEthernet0/0/0/0" + * "GigabitEthernet0/0/0/1" + * etc. """ if nic_number == 1: return "MgmtEth0/0/CPU0/0" @@ -66,11 +65,11 @@ def validate_cpu_count(cls, cpus): """IOS XRv supports 1-8 CPUs. Args: - cpus (int): Number of CPUs + cpus (int): Number of CPUs Raises: - ValueTooLowError: if ``cpus`` is less than 1 - ValueTooHighError: if ``cpus`` is more than 8 + ValueTooLowError: if ``cpus`` is less than 1 + ValueTooHighError: if ``cpus`` is more than 8 """ validate_int(cpus, 1, 8, "CPUs") @@ -79,11 +78,11 @@ def validate_memory_amount(cls, mebibytes): """Minimum 3 GiB, max 8 GiB of RAM. Args: - mebibytes (int): RAM, in MiB. + mebibytes (int): RAM, in MiB. Raises: - ValueTooLowError: if ``mebibytes`` is less than 3072 - ValueTooHighError: if ``mebibytes`` is more than 8192 + 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") @@ -95,10 +94,10 @@ def validate_nic_count(cls, count): """IOS XRv requires at least one NIC. Args: - count (int): Number of NICs. + count (int): Number of NICs. Raises: - ValueTooLowError: if ``count`` is less than 1 + ValueTooLowError: if ``count`` is less than 1 """ validate_int(count, 1, None, "NIC count") @@ -107,11 +106,11 @@ def validate_serial_count(cls, count): """IOS XRv supports 1-4 serial ports. Args: - count (int): Number of serial ports. + count (int): Number of serial ports. Raises: - ValueTooLowError: if ``count`` is less than 1 - ValueTooHighError: if ``count`` is more than 4 + ValueTooLowError: if ``count`` is less than 1 + ValueTooHighError: if ``count`` is more than 4 """ validate_int(count, 1, 4, "serial ports") @@ -126,10 +125,10 @@ def guess_nic_name(cls, nic_number): """Fabric and management only. Args: - nic_number (int): Nth NIC to name. + nic_number (int): Nth NIC to name. Returns: - str: "fabric" or "MgmtEth0/{SLOT}/CPU0/0" only + str: "fabric" or "MgmtEth0/{SLOT}/CPU0/0" only """ if nic_number == 1: return "fabric" @@ -141,11 +140,11 @@ def validate_nic_count(cls, count): """Fabric plus an optional management NIC. Args: - count (int): Number of NICs. + count (int): Number of NICs. Raises: - ValueTooLowError: if ``count`` is less than 1 - ValueTooHighError: if ``count`` is more than 2 + ValueTooLowError: if ``count`` is less than 1 + ValueTooHighError: if ``count`` is more than 2 """ validate_int(count, 1, 2, "NIC count") diff --git a/COT/platforms/cisco_iosxrv_9000.py b/COT/platforms/cisco_iosxrv_9000.py index e26ab5f..85ccd4a 100644 --- a/COT/platforms/cisco_iosxrv_9000.py +++ b/COT/platforms/cisco_iosxrv_9000.py @@ -33,16 +33,15 @@ def guess_nic_name(cls, nic_number): """MgmtEth0/0/CPU0/0, CtrlEth, DevEth, GigabitEthernet0/0/0/0, etc. Args: - nic_number (int): Nth NIC to name. + nic_number (int): Nth NIC to name. Returns: - str: - * "MgmtEth0/0/CPU0/0" - * "CtrlEth" - * "DevEth" - * "GigabitEthernet0/0/0/0" - * "GigabitEthernet0/0/0/1" - * etc. + * "MgmtEth0/0/CPU0/0" + * "CtrlEth" + * "DevEth" + * "GigabitEthernet0/0/0/0" + * "GigabitEthernet0/0/0/1" + * etc. """ if nic_number == 1: return "MgmtEth0/0/CPU0/0" @@ -58,11 +57,11 @@ def validate_cpu_count(cls, cpus): """Minimum 1, maximum 32 CPUs. Args: - cpus (int): Number of CPUs + cpus (int): Number of CPUs Raises: - ValueTooLowError: if ``cpus`` is less than 1 - ValueTooHighError: if ``cpus`` is more than 32 + ValueTooLowError: if ``cpus`` is less than 1 + ValueTooHighError: if ``cpus`` is more than 32 """ validate_int(cpus, 1, 32, "CPUs") @@ -71,11 +70,11 @@ def validate_memory_amount(cls, mebibytes): """Minimum 8 GiB, maximum 32 GiB. Args: - mebibytes (int): RAM, in MiB. + mebibytes (int): RAM, in MiB. Raises: - ValueTooLowError: if ``mebibytes`` is less than 8192 - ValueTooHighError: if ``mebibytes`` is more than 32768 + ValueTooLowError: if ``mebibytes`` is less than 8192 + ValueTooHighError: if ``mebibytes`` is more than 32768 """ if mebibytes < 8192: raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "8 GiB") @@ -87,9 +86,9 @@ def validate_nic_count(cls, count): """IOS XRv 9000 requires at least 4 NICs. Args: - count (int): Number of NICs. + count (int): Number of NICs. Raises: - ValueTooLowError: if ``count`` is less than 4 + ValueTooLowError: if ``count`` is less than 4 """ validate_int(count, 4, None, "NIC count") diff --git a/COT/platforms/cisco_nxosv.py b/COT/platforms/cisco_nxosv.py index fc795f7..0c645a9 100644 --- a/COT/platforms/cisco_nxosv.py +++ b/COT/platforms/cisco_nxosv.py @@ -36,18 +36,17 @@ def guess_nic_name(cls, nic_number): """NX-OSv names its NICs a bit interestingly... Args: - nic_number (int): Nth NIC to name. + nic_number (int): Nth NIC to name. Returns: - str: - * mgmt0 - * Ethernet2/1 - * Ethernet2/2 - * ... - * Ethernet2/48 - * Ethernet3/1 - * Ethernet3/2 - * ... + * mgmt0 + * Ethernet2/1 + * Ethernet2/2 + * ... + * Ethernet2/48 + * Ethernet3/1 + * Ethernet3/2 + * ... """ if nic_number == 1: return "mgmt0" @@ -60,11 +59,11 @@ def validate_cpu_count(cls, cpus): """NX-OSv requires 1-8 CPUs. Args: - cpus (int): Number of CPUs + cpus (int): Number of CPUs Raises: - ValueTooLowError: if ``cpus`` is less than 1 - ValueTooHighError: if ``cpus`` is more than 8 + ValueTooLowError: if ``cpus`` is less than 1 + ValueTooHighError: if ``cpus`` is more than 8 """ validate_int(cpus, 1, 8, "CPUs") @@ -73,10 +72,10 @@ def validate_memory_amount(cls, mebibytes): """NX-OSv requires 2-8 GiB of RAM. Args: - mebibytes (int): RAM, in MiB. + mebibytes (int): RAM, in MiB. Raises: - ValueTooLowError: if ``mebibytes`` is less than 2048 + ValueTooLowError: if ``mebibytes`` is less than 2048 ValueTooHighError: if ``mebibytes`` is more than 8192 """ if mebibytes < 2048: @@ -89,10 +88,10 @@ def validate_serial_count(cls, count): """NX-OSv requires 1-2 serial ports. Args: - count (int): Number of serial ports. + count (int): Number of serial ports. Raises: - ValueTooLowError: if ``count`` is less than 1 - ValueTooHighError: if ``count`` is more than 2 + ValueTooLowError: if ``count`` is less than 1 + ValueTooHighError: if ``count`` is more than 2 """ validate_int(count, 1, 2, "serial ports") diff --git a/COT/platforms/generic.py b/COT/platforms/generic.py index aaad1bc..393c2cb 100644 --- a/COT/platforms/generic.py +++ b/COT/platforms/generic.py @@ -46,10 +46,10 @@ 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. + device_type (str): 'harddisk', 'cdrom', etc. Returns: - str: 'ide' unless overridden by subclass. + str: 'ide' unless overridden by subclass. """ # For most platforms IDE is the correct default. return 'ide' @@ -61,10 +61,10 @@ def guess_nic_name(cls, nic_number): .. note:: This method counts from 1, not from 0! Args: - nic_number (int): Nth NIC to name. + nic_number (int): Nth NIC to name. Returns: - str: "Ethernet1", "Ethernet2", etc. unless overridden by subclass. + str: "Ethernet1", "Ethernet2", etc. unless overridden by subclass. """ return "Ethernet" + str(nic_number) @@ -73,13 +73,13 @@ 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 + 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 + 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") @@ -88,11 +88,11 @@ def validate_memory_amount(cls, mebibytes): """Throw an error if the amount of RAM is not supported. Args: - mebibytes (int): RAM, in MiB. + mebibytes (int): RAM, in MiB. Raises: - ValueTooLowError: if ``mebibytes`` is less than the minimum - required by this platform + ValueTooLowError: if ``mebibytes`` is less than the minimum + required by this platform ValueTooHighError: if ``mebibytes`` is more than the maximum supported by this platform """ @@ -104,13 +104,13 @@ def validate_nic_count(cls, count): """Throw an error if the number of NICs is not supported. Args: - count (int): Number of NICs. + 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 + 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") @@ -123,11 +123,11 @@ def validate_nic_type(cls, type_string): - :data:`COT.data_validation.NIC_TYPES` Args: - type_string (str): See :data:`COT.data_validation.NIC_TYPES` + type_string (str): See :data:`COT.data_validation.NIC_TYPES` Raises: - ValueUnsupportedError: if ``type_string`` is not in - :const:`SUPPORTED_NIC_TYPES` + 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, @@ -138,11 +138,11 @@ 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` + 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` + 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) @@ -152,12 +152,12 @@ 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. + 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 + 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/tests/test_cisco_iosv.py b/COT/platforms/tests/test_cisco_iosv.py index 74b0962..8c44746 100644 --- a/COT/platforms/tests/test_cisco_iosv.py +++ b/COT/platforms/tests/test_cisco_iosv.py @@ -27,7 +27,7 @@ def emit(self, record): """Do nothing. Args: - record (object): Ignored. + record (object): Ignored. """ pass diff --git a/COT/remove_file.py b/COT/remove_file.py index 766cf13..0354f16 100644 --- a/COT/remove_file.py +++ b/COT/remove_file.py @@ -47,7 +47,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTRemoveFile, self).__init__(ui) self.file_path = None @@ -59,7 +59,7 @@ def ready_to_run(self): """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.file_path is None and self.file_id is None: return False, "No file information provided!" @@ -69,7 +69,7 @@ def run(self): """Do the actual work of this submodule. Raises: - InvalidInputError: if :func:`ready_to_run` reports ``False`` + InvalidInputError: if :func:`ready_to_run` reports ``False`` """ super(COTRemoveFile, self).run() diff --git a/COT/submodule.py b/COT/submodule.py index 83a2c00..17727b8 100644 --- a/COT/submodule.py +++ b/COT/submodule.py @@ -51,7 +51,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ self.vm = None """Virtual machine description (:class:`VMDescription`).""" @@ -62,7 +62,7 @@ def ready_to_run(self): # pylint: disable=no-self-use """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ return True, "Ready to go!" @@ -70,7 +70,7 @@ def run(self): """Do the actual work of this submodule. Raises: - InvalidInputError: if :meth:`ready_to_run` reports ``False`` + InvalidInputError: if :meth:`ready_to_run` reports ``False`` """ (ready, reason) = self.ready_to_run() if not ready: @@ -111,7 +111,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTReadOnlySubmodule, self).__init__(ui) self._package = None @@ -124,7 +124,7 @@ def package(self): :attr:`self.vm` from the provided file. Raises: - InvalidInputError: if the file does not exist. + InvalidInputError: if the file does not exist. """ return self._package @@ -144,7 +144,7 @@ def ready_to_run(self): """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.package is None: return False, "PACKAGE is a mandatory argument!" @@ -167,7 +167,7 @@ def __init__(self, ui): """Instantiate this submodule with the given UI. Args: - ui (UI): User interface instance. + ui (UI): User interface instance. """ super(COTSubmodule, self).__init__(ui) self._package = None @@ -182,7 +182,7 @@ def package(self): :attr:`self.vm` from the provided file. Raises: - InvalidInputError: if the file does not exist. + InvalidInputError: if the file does not exist. """ return self._package @@ -221,7 +221,7 @@ def ready_to_run(self): """Check whether the module is ready to :meth:`run`. Returns: - tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` + tuple: ``(True, ready_message)`` or ``(False, reason_why_not)`` """ if self.package is None: return False, "PACKAGE is a mandatory argument!" @@ -234,7 +234,7 @@ def run(self): sets it to the value of :attr:`PACKAGE`. Raises: - InvalidInputError: if :meth:`ready_to_run` reports ``False`` + InvalidInputError: if :meth:`ready_to_run` reports ``False`` """ super(COTSubmodule, self).run() diff --git a/COT/tests/test_install_helpers.py b/COT/tests/test_install_helpers.py index b064a6d..9abe0e1 100644 --- a/COT/tests/test_install_helpers.py +++ b/COT/tests/test_install_helpers.py @@ -36,11 +36,10 @@ def stub_check_output(arg_list, *_args, **_kwargs): """Stub to ensure fixed version number strings. Args: - arg_list (list): arg_list[0] is script being called, - others are ignored. + arg_list (list): arg_list[0] is script being called, others are ignored. Returns: - str: Canned output line, or "" + str: Canned output line, or "" """ versions = { "fatdisk": "fatdisk, version 1.0.0-beta", @@ -59,9 +58,9 @@ def stub_dir_exists_but_not_file(path): """Stub for :func:`os.path.exists`. Args: - path (str): Path to check. + path (str): Path to check. Returns: - bool: True for man dir, False for man file. + bool: True for man dir, False for man file. """ return os.path.basename(path) != "cot.1" diff --git a/COT/tests/ut.py b/COT/tests/ut.py index 8fd559f..01f2b3c 100644 --- a/COT/tests/ut.py +++ b/COT/tests/ut.py @@ -40,7 +40,7 @@ def emit(self, record): """Do nothing. Args: - record (LogRecord): Record to ignore. + record (LogRecord): Record to ignore. """ pass @@ -75,7 +75,7 @@ def __init__(self, testcase): """Create a logging handler for the given test case. Args: - testcase (unittest.TestCase): Owner of this logging handler. + testcase (unittest.TestCase): Owner of this logging handler. """ BufferingHandler.__init__(self, capacity=0) self.setLevel(logging.DEBUG) @@ -85,7 +85,7 @@ def emit(self, record): """Add the given log record to our internal buffer. Args: - record (LogRecord): Record to store. + record (LogRecord): Record to store. """ self.buffer.append(record.__dict__) @@ -93,9 +93,9 @@ def shouldFlush(self, record): # noqa: N802 """Return False - we only flush manually. Args: - record (LogRecord): Record to ignore. + record (LogRecord): Record to ignore. Returns: - bool: always False + bool: always False """ return False @@ -103,9 +103,9 @@ def logs(self, **kwargs): """Look for log entries matching the given dict. Args: - kwargs (dict): logging arguments to match against. + kwargs (dict): logging arguments to match against. Returns: - list: List of record(s) that matched. + list: List of record(s) that matched. """ matches = [] for record in self.buffer: @@ -132,12 +132,12 @@ def assertLogged(self, info='', **kwargs): # noqa: N802 """Fail unless the given log messages were each seen exactly once. Args: - info (str): Optional string to prepend to any failure messages. - kwargs (dict): logging arguments to match against. + info (str): Optional string to prepend to any failure messages. + kwargs (dict): logging arguments to match against. Raises: - AssertionError: if an expected log message was not seen - AssertionError: if an expected log message was seen more than once + AssertionError: if an expected log message was not seen + AssertionError: if an expected log message was seen more than once """ matches = self.logs(**kwargs) if not matches: @@ -155,10 +155,10 @@ def assertNoLogsOver(self, max_level, info=''): # noqa: N802 """Fail if any logs are logged higher than the given level. Args: - max_level (int): Highest logging level to permit. - info (str): Optional string to prepend to any failure messages. + max_level (int): Highest logging level to permit. + info (str): Optional string to prepend to any failure messages. Raises: - AssertionError: if any messages higher than max_level were seen + AssertionError: if any messages higher than max_level were seen """ for level in (logging.CRITICAL, logging.ERROR, logging.WARNING, logging.INFO, logging.VERBOSE, logging.DEBUG): @@ -258,9 +258,9 @@ def localfile(name): """Get the absolute path to a local resource file. Args: - name (str): File name. + name (str): File name. Returns: - str: Absolute file path. + str: Absolute file path. """ return os.path.abspath(resource_filename(__name__, name)) @@ -269,11 +269,11 @@ def invalid_hardware_warning(profile, value, kind): """Warning log message for invalid hardware. Args: - profile (str): Config profile, or "". - value (object): Invalid value - kind (str): Label for this hardware kind. + profile (str): Config profile, or "". + value (object): Invalid value + kind (str): Label for this hardware kind. Returns: - dict: kwargs suitable for passing into :meth:`assertLogged` + dict: kwargs suitable for passing into :meth:`assertLogged` """ msg = "" if profile: @@ -297,7 +297,7 @@ def set_vm_platform(self, plat): """Force the VM under test to use a particular Platform class. Args: - plat (COT.platforms.GenericPlatform): Platform class to use + plat (COT.platforms.GenericPlatform): Platform class to use """ # pylint: disable=protected-access self.instance.vm._platform = plat @@ -306,10 +306,10 @@ def check_cot_output(self, expected): """Grab the output from COT and check it against expected output. Args: - expected (str): Expected output + expected (str): Expected output Raises: - AssertionError: if an error is raised by COT when run - AssertionError: if the output returned does not match expected. + AssertionError: if an error is raised by COT when run + AssertionError: if the output returned does not match expected. """ with mock.patch('sys.stdout', new_callable=StringIO.StringIO) as so: try: @@ -331,12 +331,12 @@ def check_diff(self, expected, file1=None, file2=None): later Python versions. Args: - expected (str): Expected diff output - file1 (str): File path to compare (default: input.ovf file) - file2 (str): File path to compare (default: output.ovf file) + expected (str): Expected diff output + file1 (str): File path to compare (default: input.ovf file) + file2 (str): File path to compare (default: output.ovf file) Raises: - AssertionError: if the two files do not have identical contents. + AssertionError: if the two files do not have identical contents. """ if file1 is None: file1 = self.input_ovf @@ -455,8 +455,8 @@ def validate_with_ovftool(self, filename=None): """Use OVFtool to validate the given OVF/OVA file. Args: - filename (str): File name to validate (optional, default is - :attr:`temp_file`). + filename (str): File name to validate (optional, default is + :attr:`temp_file`). """ if filename is None: filename = self.temp_file diff --git a/COT/ui_shared.py b/COT/ui_shared.py index 74b78cd..edbed83 100644 --- a/COT/ui_shared.py +++ b/COT/ui_shared.py @@ -37,7 +37,7 @@ def __init__(self, force=False): """Constructor. Args: - force (bool): See :attr:`force`. + force (bool): See :attr:`force`. """ self.force = force """Whether to automatically select the default value in all cases. @@ -60,11 +60,11 @@ def fill_usage(self, # pylint: disable=no-self-use """Pretty-print a list of usage strings. Args: - subcommand (str): Subcommand name/keyword - usage_list (list): List of usage strings for this subcommand. + subcommand (str): Subcommand name/keyword + usage_list (list): List of usage strings for this subcommand. Returns: - str: Concatenation of all usage strings, each appropriately - wrapped to the :attr:`terminal_width` value. + str: Concatenation of all usage strings, each appropriately wrapped + to the :attr:`terminal_width` value. """ return "\n".join(["{0} {1}".format(subcommand, usage) for usage in usage_list]) @@ -73,9 +73,9 @@ def fill_examples(self, example_list): """Pretty-print a set of usage examples. Args: - example_list (list): List of (example, description) tuples. + example_list (list): List of (example, description) tuples. Raises: - NotImplementedError: Must be implemented by a subclass. + NotImplementedError: Must be implemented by a subclass. """ raise NotImplementedError("No implementation for fill_examples()") @@ -90,10 +90,10 @@ def confirm(self, prompt): should override this method. Args: - prompt (str): Message to prompt the user with + prompt (str): Message to prompt the user with Returns: - bool: ``True`` (user confirms acceptance) or ``False`` - (user declines) + bool: ``True`` (user confirms acceptance) or ``False`` + (user declines) """ if self.force: logger.warning("Automatically agreeing to '%s'", prompt) @@ -107,9 +107,9 @@ def confirm_or_die(self, prompt): :meth:`confirm` returns ``False``. Args: - prompt (str): Message to prompt the user with + prompt (str): Message to prompt the user with Raises: - SystemExit: if user declines + SystemExit: if user declines """ if not self.confirm(prompt): sys.exit("Aborting.") @@ -119,15 +119,15 @@ def choose_from_list(self, footer, option_list, default_value, """Prompt the user to choose from a list. Args: - footer (str): Prompt string to display following the list - option_list (list): List of strings to choose amongst - default_value (str): Default value to select if user declines - header (str): String to display prior to the list - info_list (list): Verbose strings to display in place of - :attr:`option_list` + footer (str): Prompt string to display following the list + option_list (list): List of strings to choose amongst + default_value (str): Default value to select if user declines + header (str): String to display prior to the list + info_list (list): Verbose strings to display in place of + :attr:`option_list` Returns: - str: :attr:`default_value` or an item from :attr:`option_list`. + str: :attr:`default_value` or an item from :attr:`option_list`. """ if not info_list: info_list = option_list @@ -165,12 +165,12 @@ def get_input(self, prompt, default_value): override this method. Args: - prompt (str): Message to prompt the user with - default_value (str): Default value to input if the user simply - hits Enter without entering a value, or if :attr:`force`. + prompt (str): Message to prompt the user with + default_value (str): Default value to input if the user simply + hits Enter without entering a value, or if :attr:`force`. Returns: - str: Input value + str: Input value """ if self.force: logger.warning("Automatically entering %s in response to '%s'", @@ -182,9 +182,9 @@ def get_password(self, username, host): """Get password string from the user. Args: - username (str): Username the password is associated with - host (str): Host the password is associated with + username (str): Username the password is associated with + host (str): Host the password is associated with Raises: - NotImplementedError: Must be implemented by a subclass. + NotImplementedError: Must be implemented by a subclass. """ raise NotImplementedError("No implementation of get_password()") diff --git a/COT/vm_description.py b/COT/vm_description.py index aadf883..4ab6271 100644 --- a/COT/vm_description.py +++ b/COT/vm_description.py @@ -77,12 +77,12 @@ def detect_type_from_name(cls, filename): Does not check file contents, as the given filename may not yet exist. Args: - filename (str): File name or path + filename (str): File name or path Returns: - str: A string representing a recognized and supported type of file + str: A string representing a recognized and supported type of file Raises: - ValueUnsupportedError: if COT can't recognize the file type or - doesn't know how to handle this file type. + ValueUnsupportedError: if COT can't recognize the file type or + doesn't know how to handle this file type. """ raise ValueUnsupportedError("filename", filename, ("none implemented")) @@ -92,13 +92,13 @@ def __init__(self, input_file, output_file=None): Also creates a temporary directory as a working directory. Args: - input_file (str): Data file to read in. - output_file (str): File name to write to. + input_file (str): Data file to read in. + output_file (str): File name to write to. - * If this VM is read-only, (there will never be an output file) - this value should be ``None`` - * If the output filename is not yet known, use ``""`` and - subsequently set :attr:`output` when it is determined. + * If this VM is read-only, (there will never be an output file) + this value should be ``None`` + * If the output filename is not yet known, use ``""`` and + subsequently set :attr:`output` when it is determined. """ self._input_file = input_file self._product_class = None @@ -167,7 +167,7 @@ def validate_hardware(self): """Check sanity of hardware properties for this VM/product/platform. Returns: - bool: ``True`` if hardware is sane, ``False`` if not. + bool: ``True`` if hardware is sane, ``False`` if not. """ raise NotImplementedError("validate_hardware not implemented!") @@ -185,7 +185,7 @@ def default_config_profile(self): """The name of the default configuration profile. Returns: - str: Profile name or ``None`` if none are defined. + str: Profile name or ``None`` if none are defined. """ if self.config_profiles: return self.config_profiles[0] @@ -196,9 +196,9 @@ def environment_properties(self): """The array of environment properties. Returns: - list: Array of dicts (one per property) with the keys - ``"key"``, ``"value"``, ``"qualifiers"``, ``"type"``, - ``"label"``, and ``"description"``. + list: Array of dicts (one per property) with the keys + ``"key"``, ``"value"``, ``"qualifiers"``, ``"type"``, + ``"label"``, and ``"description"``. """ raise NotImplementedError("environment_properties not implemented") @@ -259,15 +259,13 @@ def convert_disk_if_needed(self, # pylint: disable=no-self-use """Convert the disk to a more appropriate format if needed. Args: - disk_image (DiskRepresentation): Image to inspect and possibly - convert - kind (str): Image type (harddisk/cdrom). + disk_image (DiskRepresentation): Disk to inspect and possibly convert + kind (str): Image type (harddisk/cdrom). Returns: - DiskRepresentation: - * :attr:`disk_image`, if no conversion was required - * or a new :class:`~COT.disks.disk.DiskRepresentation` instance - representing a converted image that has been created in - :attr:`output_dir`. + DiskRepresentation: :attr:`disk_image`, if no conversion was + required, or a new :class:`~COT.disks.disk.DiskRepresentation` + instance representing a converted image that has been created in + :attr:`output_dir`. """ # Some VMs may not need this, so default to do nothing, not error return disk_image @@ -276,10 +274,10 @@ def search_from_filename(self, filename): """From the given filename, try to find any existing objects. Args: - filename (str): Filename to search from + filename (str): Filename to search from Returns: - tuple: ``(file, disk, controller_device, disk_device)``, - opaque objects of which any or all may be ``None`` + tuple: ``(file, disk, controller_device, disk_device)``, opaque + objects of which any or all may be ``None`` """ raise NotImplementedError("search_from_filename not implemented") @@ -287,10 +285,10 @@ def search_from_file_id(self, file_id): """From the given file ID, try to find any existing objects. Args: - file_id (str): File ID to search from + file_id (str): File ID to search from Returns: - tuple: ``(file, disk, controller_device, disk_device)``, - opaque objects of which any or all may be ``None`` + tuple: ``(file, disk, controller_device, disk_device)``, opaque + objects of which any or all may be ``None`` """ raise NotImplementedError("search_from_file_id not implemented") @@ -298,11 +296,11 @@ def search_from_controller(self, controller, address): """From the controller type and device address, look for existing disk. Args: - controller (str): ``'ide'`` or ``'scsi'`` - address (str): Device address such as ``'1:0'`` + controller (str): ``'ide'`` or ``'scsi'`` + address (str): Device address such as ``'1:0'`` Returns: - tuple: ``(file, disk, controller_device, disk_device)``, - opaque objects of which any or all may be ``None`` + tuple: ``(file, disk, controller_device, disk_device)``, opaque + objects of which any or all may be ``None`` """ raise NotImplementedError("search_from_controller not implemented") @@ -310,9 +308,9 @@ def find_open_controller(self, controller_type): """Find the first open slot on a controller of the given type. Args: - controller_type (str): ``'ide'`` or ``'scsi'`` + controller_type (str): ``'ide'`` or ``'scsi'`` Returns: - tuple: ``(controller_device, address_string)`` or ``(None, None)`` + tuple: ``(controller_device, address_string)`` or ``(None, None)`` """ raise NotImplementedError("find_open_controller not implemented") @@ -320,9 +318,9 @@ def get_id_from_file(self, file_obj): """Get the file ID from the given opaque file object. Args: - file_obj (object): File object to query + file_obj (object): File object to query Returns: - str: Identifier string associated with this object + str: Identifier string associated with this object """ raise NotImplementedError("get_id_from_file not implemented") @@ -330,9 +328,9 @@ def get_path_from_file(self, file_obj): """Get the file path from the given opaque file object. Args: - file_obj (object): File object to query + file_obj (object): File object to query Returns: - str: Relative path to the file associated with this object + str: Relative path to the file associated with this object """ raise NotImplementedError("get_path_from_file not implemented") @@ -340,10 +338,10 @@ def get_file_ref_from_disk(self, disk): """Get the file reference from the given opaque disk object. Args: - disk (object): Disk object to query + disk (object): Disk object to query Returns: - str: String that can be used to identify the file associated - with this disk + str: String that can be used to identify the file associated with + this disk """ raise NotImplementedError("get_file_ref_from_disk not implemented") @@ -351,9 +349,9 @@ def get_id_from_disk(self, disk): """Get the identifier string associated with the given Disk object. Args: - disk (object): Disk object + disk (object): Disk object Returns: - str: Identifier string associated with this object + str: Identifier string associated with this object """ raise NotImplementedError("get_id_from_disk not implemented") @@ -361,11 +359,11 @@ def get_common_subtype(self, device_type): """Get the sub-type common to all devices of the given type. Args: - device_type (str): Device type such as ``'ide'`` or ``'memory'``. + device_type (str): Device type such as ``'ide'`` or ``'memory'``. Returns: str: Subtype string common to all devices of this type, or ``None``, - if multiple such devices exist and they do not all - have the same sub-type. + if multiple such devices exist and they do not all have the same + sub-type. """ raise NotImplementedError("get_common_subtype not implemented") @@ -374,15 +372,15 @@ def check_sanity_of_disk_device(self, disk, file_obj, """Check if the given disk is linked properly to the other objects. Args: - disk (object): Disk object to validate - file_obj (object): File object which this disk should be linked to - (optional) - disk_item (object): Disk device object which should link to - this disk (optional) - ctrl_item (object): Controller device object which should link to - the :attr:`disk_item` + disk (object): Disk object to validate + file_obj (object): File object which this disk should be linked to + (optional) + disk_item (object): Disk device object which should link to + this disk (optional) + ctrl_item (object): Controller device object which should link to + the :attr:`disk_item` Raises: - ValueMismatchError: if the given items are not linked properly. + ValueMismatchError: if the given items are not linked properly. """ raise NotImplementedError( "check_sanity_of_disk_device not implemented") @@ -391,13 +389,13 @@ def add_file(self, file_path, file_id, file_obj=None, disk=None): """Add a new file object to the VM or overwrite the provided one. Args: - file_path (str): Path to file to add - file_id (str): Identifier string for the file in the VM - file_obj (object): Existing file object to overwrite - disk (object): Existing disk object referencing :attr:`file`. + file_path (str): Path to file to add + file_id (str): Identifier string for the file in the VM + file_obj (object): Existing file object to overwrite + disk (object): Existing disk object referencing :attr:`file`. Returns: - object: New or updated file object + object: New or updated file object """ raise NotImplementedError("add_file not implemented") @@ -405,9 +403,9 @@ def remove_file(self, file_obj, disk=None, disk_drive=None): """Remove the given file object from the VM. Args: - file_obj (object): File object to remove - disk (object): Disk object referencing :attr:`file` - disk_drive (object): Disk drive mapping :attr:`file` to a device + file_obj (object): File object to remove + disk (object): Disk object referencing :attr:`file` + disk_drive (object): Disk drive mapping :attr:`file` to a device """ raise NotImplementedError("remove_file not implemented") @@ -415,13 +413,13 @@ def add_disk(self, disk_repr, file_id, drive_type, disk=None): """Add a new disk object to the VM or overwrite the provided one. Args: - disk_repr (DiskRepresentation): Disk file representation - file_id (str): Identifier string for the file/disk mapping - drive_type (str): 'harddisk' or 'cdrom' - disk (object): Existing disk object to overwrite + disk_repr (DiskRepresentation): Disk file representation + file_id (str): Identifier string for the file/disk mapping + drive_type (str): 'harddisk' or 'cdrom' + disk (object): Existing disk object to overwrite Returns: - object: New or updated disk object + object: New or updated disk object """ raise NotImplementedError("add_disk not implemented") @@ -430,13 +428,13 @@ def add_controller_device(self, device_type, subtype, address, """Create a new IDE or SCSI controller, or update existing one. Args: - device_type (str): ``'ide'`` or ``'scsi'`` - subtype (str): Subtype such as ``'virtio'`` (optional) - address (int): Controller address such as 0 or 1 (optional) - ctrl_item (object): Existing controller device to update (optional) + device_type (str): ``'ide'`` or ``'scsi'`` + subtype (str): Subtype such as ``'virtio'`` (optional) + address (int): Controller address such as 0 or 1 (optional) + ctrl_item (object): Existing controller device to update (optional) Returns: - object: New or updated controller device object + object: New or updated controller device object """ raise NotImplementedError("add_controller_device not implemented") @@ -445,18 +443,18 @@ def add_disk_device(self, drive_type, address, name, description, """Add a new disk device to the VM or update the provided one. Args: - drive_type (str): ``'harddisk'`` or ``'cdrom'`` - address (str): Address on controller, such as "1:0" (optional) - name (str): Device name string (optional) - description (str): Description string (optional) - disk (object): Disk object to map to this device - file_obj (object): File object to map to this device - ctrl_item (object): Controller object to serve as parent - disk_item (object): Existing disk device to update instead of - making a new device. + drive_type (str): ``'harddisk'`` or ``'cdrom'`` + address (str): Address on controller, such as "1:0" (optional) + name (str): Device name string (optional) + description (str): Description string (optional) + disk (object): Disk object to map to this device + file_obj (object): File object to map to this device + ctrl_item (object): Controller object to serve as parent + disk_item (object): Existing disk device to update instead of + making a new device. Returns: - object: New or updated disk device object. + object: New or updated disk device object. """ raise NotImplementedError("add_disk_device not implemented") @@ -465,9 +463,9 @@ def create_configuration_profile(self, pid, label, description): """Create/update a configuration profile with the given ID. Args: - pid (str): Profile identifier - label (str): Brief descriptive label for the profile - description (str): Verbose description of the profile + pid (str): Profile identifier + label (str): Brief descriptive label for the profile + description (str): Verbose description of the profile """ raise NotImplementedError("create_configuration_profile " "not implemented!") @@ -476,7 +474,7 @@ def delete_configuration_profile(self, profile): """Delete the configuration profile with the given ID. Args: - profile (str): Profile identifier + profile (str): Profile identifier """ raise NotImplementedError("delete_configuration_profile " "not implemented") @@ -505,8 +503,8 @@ def set_cpu_count(self, cpus, profile_list): """Set the number of CPUs. Args: - cpus (int): Number of CPUs - profile_list (list): Change only the given profiles + cpus (int): Number of CPUs + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_cpu_count not implemented!") @@ -514,8 +512,8 @@ def set_memory(self, megabytes, profile_list): """Set the amount of RAM, in megabytes. Args: - megabytes (int): Memory value, in megabytes - profile_list (list): Change only the given profiles + megabytes (int): Memory value, in megabytes + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_memory not implemented!") @@ -526,8 +524,8 @@ def set_nic_type(self, nic_type, profile_list): Use :func:`set_nic_types` instead. Args: - nic_type (str): NIC hardware type - profile_list (list): Change only the given profiles. + nic_type (str): NIC hardware type + profile_list (list): Change only the given profiles. """ warnings.warn("Use set_nic_types() instead", DeprecationWarning) self.set_nic_types([nic_type], profile_list) @@ -536,8 +534,8 @@ def set_nic_types(self, type_list, profile_list): """Set the hardware type(s) for NICs. Args: - type_list (list): NIC hardware type(s) - profile_list (list): Change only the given profiles. + type_list (list): NIC hardware type(s) + profile_list (list): Change only the given profiles. """ raise NotImplementedError("set_nic_types not implemented!") @@ -545,9 +543,9 @@ def get_nic_count(self, profile_list): """Get the number of NICs under the given profile(s). Args: - profile_list (list): Profile(s) of interest. + profile_list (list): Profile(s) of interest. Returns: - dict: ``{ profile_name : nic_count }`` + dict: ``{ profile_name : nic_count }`` """ raise NotImplementedError("get_nic_count not implemented!") @@ -555,8 +553,8 @@ def set_nic_count(self, count, profile_list): """Set the given profile(s) to have the given number of NICs. Args: - count (int): number of NICs - profile_list (list): Change only the given profiles + count (int): number of NICs + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_nic_count not implemented!") @@ -566,8 +564,8 @@ def create_network(self, label, description): Also serves to update the description of an existing network label. Args: - label (str): Brief label for the network - description (str): Verbose description of the network + label (str): Brief label for the network + description (str): Verbose description of the network """ raise NotImplementedError("create_network not implemented!") @@ -579,8 +577,8 @@ def set_nic_networks(self, network_list, profile_list): NICs, will use the last entry in the list for all remaining NICs. Args: - network_list (list): List of networks to map NICs to - profile_list (list): Change only the given profiles + network_list (list): List of networks to map NICs to + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_nic_networks not implemented!") @@ -592,8 +590,8 @@ def set_nic_mac_addresses(self, mac_list, profile_list): will use the last entry in the list for all remaining NICs. Args: - mac_list (list): List of MAC addresses to assign to NICs - profile_list (list): Change only the given profiles + mac_list (list): List of MAC addresses to assign to NICs + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_nic_mac_addresses not implemented!") @@ -613,8 +611,8 @@ def set_nic_names(self, name_list, profile_list): Expands to ``["mgmt0", "eth10", "eth11", "eth12", ...]`` Args: - name_list (list): List of names to assign. - profile_list (list): Change only the given profiles + name_list (list): List of names to assign. + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_nic_names not implemented!") @@ -622,9 +620,9 @@ def get_serial_count(self, profile_list): """Get the number of serial ports under the given profile(s). Args: - profile_list (list): Change only the given profiles + profile_list (list): Change only the given profiles Returns: - dict: ``{ profile_name : serial_count }`` + dict: ``{ profile_name : serial_count }`` """ raise NotImplementedError("get_serial_count not implemented!") @@ -632,8 +630,8 @@ def set_serial_count(self, count, profile_list): """Set the given profile(s) to have the given number of NICs. Args: - count (int): Number of serial ports - profile_list (list): Change only the given profiles + count (int): Number of serial ports + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_serial_count not implemented!") @@ -641,8 +639,8 @@ def set_serial_connectivity(self, conn_list, profile_list): """Set the serial port connectivity under the given profile(s). Args: - conn_list (list): List of connectivity strings - profile_list (list): Change only the given profiles + conn_list (list): List of connectivity strings + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_serial_connectivity not implemented!") @@ -650,9 +648,9 @@ def get_serial_connectivity(self, profile): """Get the serial port connectivity strings under the given profile. Args: - profile (str): Profile of interest. + profile (str): Profile of interest. Returns: - list: List of connectivity strings + list: List of connectivity strings """ raise NotImplementedError("get_serial_connectivity not implemented!") @@ -663,8 +661,8 @@ def set_scsi_subtype(self, subtype, profile_list): Use :func:`set_scsi_subtypes` instead. Args: - subtype (str): SCSI subtype string - profile_list (list): Change only the given profiles + subtype (str): SCSI subtype string + profile_list (list): Change only the given profiles """ warnings.warn("Use set_scsi_subtypes() instead", DeprecationWarning) self.set_scsi_subtypes([subtype], profile_list) @@ -673,8 +671,8 @@ def set_scsi_subtypes(self, type_list, profile_list): """Set the device subtype list for the SCSI controller(s). Args: - type_list (list): SCSI subtype string list - profile_list (list): Change only the given profiles + type_list (list): SCSI subtype string list + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_scsi_subtypes not implemented!") @@ -685,8 +683,8 @@ def set_ide_subtype(self, subtype, profile_list): Use :func:`set_ide_subtypes` instead. Args: - subtype (str): IDE subtype string - profile_list (list): Change only the given profiles + subtype (str): IDE subtype string + profile_list (list): Change only the given profiles """ warnings.warn("Use set_ide_subtypes() instead", DeprecationWarning) self.set_ide_subtypes([subtype], profile_list) @@ -695,8 +693,8 @@ def set_ide_subtypes(self, type_list, profile_list): """Set the device subtype list for the IDE controller(s). Args: - type_list (list): IDE subtype string list - profile_list (list): Change only the given profiles + type_list (list): IDE subtype string list + profile_list (list): Change only the given profiles """ raise NotImplementedError("set_ide_subtypes not implemented!") @@ -706,9 +704,9 @@ def get_property_value(self, key): """Get the value of the given property. Args: - key (str): Property identifier + key (str): Property identifier Returns: - str: Value of this property, or ``None`` + str: Value of this property, or ``None`` """ raise NotImplementedError("get_property_value not implemented") @@ -718,16 +716,16 @@ def set_property_value(self, key, value, """Set the value of the given property (converting value if needed). Args: - key (str): Property identifier - value (object): Value to set for this property - user_configurable (bool): Should this property be configurable at - deployment time by the user? - property_type (str): Value type - 'string' or 'boolean' - label (str): Brief explanatory label for this property - description (str): Detailed description of this property + key (str): Property identifier + value (object): Value to set for this property + user_configurable (bool): Should this property be configurable at + deployment time by the user? + property_type (str): Value type - 'string' or 'boolean' + label (str): Brief explanatory label for this property + description (str): Detailed description of this property Returns: - str: the (converted) value that was set. + str: the (converted) value that was set. """ raise NotImplementedError("set_property_value not implemented") @@ -735,9 +733,9 @@ def config_file_to_properties(self, file_path, user_configurable=None): """Import each line of a text file into a configuration property. Args: - file_path (str): File name to import. - user_configurable (bool): Should the properties be configurable at - deployment time by the user? + file_path (str): File name to import. + user_configurable (bool): Should the properties be configurable at + deployment time by the user? """ raise NotImplementedError("config_file_to_properties not implemented") @@ -752,12 +750,11 @@ def info_string(self, width=79, verbosity_option=None): """Get a descriptive string summarizing the contents of this VM. Args: - width (int): Line length to wrap to where possible. - verbosity_option (str): ``'brief'``, ``None`` (default), - or ``'verbose'`` + width (int): Line length to wrap to where possible. + verbosity_option (str): 'brief', None (default), or 'verbose' Returns: - str: Wrapped, appropriately verbose string. + str: Wrapped, appropriately verbose string. """ raise NotImplementedError("info_string not implemented") @@ -765,12 +762,11 @@ def profile_info_string(self, width=79, verbosity_option=None): """Get a string summarizing available configuration profiles. Args: - width (int): Line length to wrap to if possible - verbosity_option (str): ``'brief'``, ``None`` (default), - or ``'verbose'`` + width (int): Line length to wrap to if possible + verbosity_option (str): 'brief', None (default), or 'verbose' Returns: - str: Appropriately formatted and verbose string. + str: Appropriately formatted and verbose string. """ raise NotImplementedError("profile_info_string not implemented") @@ -779,9 +775,9 @@ def find_empty_drive(self, drive_type): """Find a disk device that exists but contains no data. Args: - drive_type (str): Disk drive type, such as 'cdrom' or 'harddisk' + drive_type (str): Disk drive type, such as 'cdrom' or 'harddisk' Returns: - object: Hardware device object, or None. + object: Hardware device object, or None. """ raise NotImplementedError("find_empty_drive not implemented") @@ -789,8 +785,8 @@ def find_device_location(self, device): """Find the controller type and address of a given device object. Args: - device (object): Hardware device object. + device (object): Hardware device object. Returns: - tuple: ``(type, address)``, such as ``("ide", "1:0")``. + tuple: ``(type, address)``, such as ``("ide", "1:0")``. """ raise NotImplementedError("find_device_location not implemented") diff --git a/COT/vm_factory.py b/COT/vm_factory.py index 63580f4..7dbcc08 100644 --- a/COT/vm_factory.py +++ b/COT/vm_factory.py @@ -33,16 +33,16 @@ def create(cls, input_file, output_file): """Create an appropriate VMDescription subclass instance from a file. Args: - input_file (str): File to read VM description from - output_file (str): File to write to when finished (optional) + input_file (str): File to read VM description from + output_file (str): File to write to when finished (optional) Raises: - VMInitError: if no appropriate class is identified - VMInitError: if the selected subclass raises a - ValueUnsupportedError while loading the file. + VMInitError: if no appropriate class is identified + VMInitError: if the selected subclass raises a + ValueUnsupportedError while loading the file. Returns: - VMDescription: Created object + VMDescription: Created object """ vm_class = None diff --git a/COT/xml_file.py b/COT/xml_file.py index 65b8f1d..d82c704 100644 --- a/COT/xml_file.py +++ b/COT/xml_file.py @@ -28,9 +28,8 @@ def register_namespace(prefix, uri): """Record a particular mapping between a namespace prefix and URI. Args: - prefix (str): Namespace prefix such as "ovf" - uri (str): Namespace URI such as - "http://schemas.dmtf.org/ovf/envelope/1" + prefix (str): Namespace prefix such as "ovf" + uri (str): Namespace URI such as "http://schemas.dmtf.org/ovf/envelope/1" """ try: ET.register_namespace(prefix, uri) @@ -42,17 +41,16 @@ def register_namespace(prefix, uri): class XML(object): """Class capable of reading, editing, and writing XML files.""" - @classmethod - def get_ns(cls, text): + @staticmethod + def get_ns(text): """Get the namespace prefix from an XML element or attribute name. Args: - text (str): Element name or attribute name, such as - "{http://schemas.dmtf.org/ovf/envelope/1}Element". + text (str): Element name or attribute name, such as + "{http://schemas.dmtf.org/ovf/envelope/1}Element". Returns: - str: Namespace prefix, such as - "http://schemas.dmtf.org/ovf/envelope/1", or "" if no prefix - is present. + str: "" if no prefix is present, or a namespace prefix, such as + "http://schemas.dmtf.org/ovf/envelope/1". """ match = re.match(r"\{(.*)\}", str(text)) if not match: @@ -60,15 +58,15 @@ def get_ns(cls, text): return "" return match.group(1) - @classmethod - def strip_ns(cls, text): + @staticmethod + def strip_ns(text): """Remove a namespace prefix from an XML element or attribute name. Args: - text (str): Element name or attribute name, such as - "{http://schemas.dmtf.org/ovf/envelope/1}Element". + text (str): Element name or attribute name, such as + "{http://schemas.dmtf.org/ovf/envelope/1}Element". Returns: - str: Bare name, such as "Element". + str: Bare name, such as "Element". """ match = re.match(r"\{.*\}(.*)", str(text)) if match is None: @@ -84,12 +82,12 @@ def __init__(self, xml_file): :attr:`root`. Args: - xml_file (str): File path to read. + xml_file (str): File path to read. Raises: - xml.etree.ElementTree.ParseError: if parsing fails under Python - 2.7 or later - xml.parsers.expat.ExpatError: if parsing fails under Python 2.6 + xml.etree.ElementTree.ParseError: if parsing fails under Python + 2.7 or later + xml.parsers.expat.ExpatError: if parsing fails under Python 2.6 """ # Parse the XML into memory self.tree = ET.parse(xml_file) @@ -101,7 +99,7 @@ def write_xml(self, xml_file): """Write pretty XML out to the given file. Args: - xml_file (str): Filename to write to + xml_file (str): Filename to write to """ logger.debug("Writing XML to %s", xml_file) @@ -128,9 +126,9 @@ def xml_reindent(self, parent, depth): """Recursively add indentation to XML to make it look nice. Args: - parent (xml.etree.ElementTree.Element): Current parent element - depth (int): How far down the rabbit hole we have recursed. - Increments by 2 for each successive level of nesting. + parent (xml.etree.ElementTree.Element): Current parent element + depth (int): How far down the rabbit hole we have recursed. + Increments by 2 for each successive level of nesting. """ depth += 2 last = None @@ -155,18 +153,17 @@ def find_child(cls, parent, tag, attrib=None, required=False): """Find the unique child element under the specified parent element. Args: - parent (xml.etree.ElementTree.Element): Parent element - tag (str): Child tag to match on - attrib (dict): Child attributes to match on - required (boolean): Whether to raise an error if no child exists + parent (xml.etree.ElementTree.Element): Parent element + tag (str): Child tag to match on + attrib (dict): Child attributes to match on + required (boolean): Whether to raise an error if no child exists Raises: - LookupError: if more than one matching child is found - KeyError: if no matching child is found and :attr:`required` - is True + LookupError: if more than one matching child is found + KeyError: if no matching child is found and :attr:`required` is True Returns: - xml.etree.ElementTree.Element: Child element found, or None + xml.etree.ElementTree.Element: Child element found, or None """ matches = cls.find_all_children(parent, tag, attrib) if len(matches) > 1: @@ -191,12 +188,12 @@ def find_all_children(cls, parent, tag, attrib=None): """Find all matching child elements under the specified parent element. Args: - parent (xml.etree.ElementTree.Element): Parent element - tag (iterable): Child tag string (or list of tags) to match on - attrib (dict): Child attributes to match on + parent (xml.etree.ElementTree.Element): Parent element + tag (iterable): Child tag string (or list of tags) to match on + attrib (dict): Child attributes to match on Returns: - list: (Possibly empty) list of matching child Elements + list: (Possibly empty) list of matching child Elements """ assert parent is not None if isinstance(tag, str): @@ -234,18 +231,17 @@ def add_child(cls, parent, new_child, ordering=None, """Add the given child element under the given parent element. Args: - parent (xml.etree.ElementTree.Element): Parent element - new_child (xml.etree.ElementTree.Element): Child element to attach - ordering (list): (Optional) List describing the expected ordering - of child tags under the parent; if a new child element is - created, its placement under the parent will respect this - sequence. - known_namespaces (list): (Optional) List of well-understood XML - namespaces. If a new child is created, and ``ordering`` is - given, any tag (new or existing) that is encountered but not - accounted for in ``ordering`` will result in COT logging a - warning **if and only if** the unaccounted-for tag is in a - known namespace. + parent (xml.etree.ElementTree.Element): Parent element + new_child (xml.etree.ElementTree.Element): Child element to attach + ordering (list): (Optional) List describing the expected ordering of + child tags under the parent; if a new child element is created, + its placement under the parent will respect this sequence. + known_namespaces (list): (Optional) List of well-understood XML + namespaces. If a new child is created, and ``ordering`` is + given, any tag (new or existing) that is encountered but not + accounted for in ``ordering`` will result in COT logging a + warning **if and only if** the unaccounted-for tag is in a + known namespace. """ if ordering and new_child.tag not in ordering: if (known_namespaces and @@ -293,16 +289,16 @@ def set_or_make_child(cls, parent, tag, text=None, attrib=None, """Update or create a child element under the specified parent element. Args: - parent (xml.etree.ElementTree.Element): Parent element - tag (str): Child element text tag to find or create - text (str): Value to set the child's text attribute to - attrib (dict): Dict of child attributes to match on while - searching and set in the final child element - ordering (list): See :meth:`add_child` - known_namespaces (list): See :meth:`add_child` + parent (xml.etree.ElementTree.Element): Parent element + tag (str): Child element text tag to find or create + text (str): Value to set the child's text attribute to + attrib (dict): Dict of child attributes to match on while + searching and set in the final child element + ordering (list): See :meth:`add_child` + known_namespaces (list): See :meth:`add_child` Returns: - xml.etree.ElementTree.Element: New or updated child Element. + xml.etree.ElementTree.Element: New or updated child Element. """ assert parent is not None if attrib is None: From 3ef96bccfc341e4f3bc40e1c4ff35af7762820e2 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 2 Nov 2016 10:37:52 -0400 Subject: [PATCH 50/59] Clean up docstring examples --- COT/cli.py | 70 ++++++++++++++++++++------------------- COT/deploy.py | 21 ++++++------ COT/edit_hardware.py | 59 +++++++++++++++++---------------- COT/file_reference.py | 16 +++++---- COT/ovf/item.py | 19 ++++++----- COT/ovf/ovf.py | 77 ++++++++++++++++++++++--------------------- COT/vm_description.py | 12 ------- 7 files changed, 137 insertions(+), 137 deletions(-) diff --git a/COT/cli.py b/COT/cli.py index 51e2d33..5744ef1 100644 --- a/COT/cli.py +++ b/COT/cli.py @@ -169,21 +169,22 @@ def fill_usage(self, subcommand, usage_list): Automatically prepends a ``cot subcommand --help`` usage string to the provided list. - :: - - >>> print(CLI(50).fill_usage('add-file', - ... ["FILE PACKAGE [-o OUTPUT] [-f FILE_ID]"])) - - cot add-file --help - cot add-file FILE PACKAGE [-o OUTPUT] - [-f FILE_ID] - Args: subcommand (str): Subcommand name/keyword usage_list (list): List of usage strings for this subcommand. Returns: string: All usage strings, each appropriately wrapped to the :func:`terminal_width` value. + + Examples: + :: + + >>> print(CLI(50).fill_usage('add-file', + ... ["FILE PACKAGE [-o OUTPUT] [-f FILE_ID]"])) + + cot add-file --help + cot add-file FILE PACKAGE [-o OUTPUT] + [-f FILE_ID] """ # Automatically add a line for --help to the usage output_lines = ["\n cot "+subcommand+" --help"] @@ -239,31 +240,6 @@ def fill_usage(self, subcommand, usage_list): def fill_examples(self, example_list): r"""Pretty-print a set of usage examples. - :: - - >>> print(CLI(70).fill_examples([ - ... ("Deploy to vSphere/ESXi server 192.0.2.100 with credentials" - ... " admin/admin, creating a VM named 'test_vm' from foo.ova.", - ... 'cot deploy foo.ova esxi 192.0.2.100 -u admin -p admin' - ... ' -n test_vm'), - ... ("Deploy to vSphere/ESXi server 192.0.2.100, with username" - ... " admin (prompting the user to input a password at runtime)," - ... " creating a VM based on profile '1CPU-2.5GB' in foo.ova.", - ... 'cot deploy foo.ova esxi 192.0.2.100 -u admin -c 1CPU-2.5GB') - ... ])) - Examples: - Deploy to vSphere/ESXi server 192.0.2.100 with credentials - admin/admin, creating a VM named 'test_vm' from foo.ova. - - cot deploy foo.ova esxi 192.0.2.100 -u admin -p admin \ - -n test_vm - - Deploy to vSphere/ESXi server 192.0.2.100, with username admin - (prompting the user to input a password at runtime), creating a VM - based on profile '1CPU-2.5GB' in foo.ova. - - cot deploy foo.ova esxi 192.0.2.100 -u admin -c 1CPU-2.5GB - Args: example_list (list): List of (description, CLI example) tuples. @@ -271,6 +247,32 @@ def fill_examples(self, example_list): str: Concatenation of examples, each wrapped appropriately to the :func:`terminal_width` value. CLI examples will be wrapped with backslashes and a hanging indent. + + Examples: + :: + + >>> print(CLI(70).fill_examples([ + ... ("Deploy to vSphere/ESXi server 192.0.2.100 with credentials" + ... " admin/admin, creating a VM named 'test_vm' from foo.ova.", + ... 'cot deploy foo.ova esxi 192.0.2.100 -u admin -p admin' + ... ' -n test_vm'), + ... ("Deploy to vSphere/ESXi server 192.0.2.100, with username" + ... " admin (prompting the user to input a password at runtime)," + ... " creating a VM based on profile '1CPU-2.5GB' in foo.ova.", + ... 'cot deploy foo.ova esxi 192.0.2.100 -u admin -c 1CPU-2.5GB') + ... ])) + Examples: + Deploy to vSphere/ESXi server 192.0.2.100 with credentials + admin/admin, creating a VM named 'test_vm' from foo.ova. + + cot deploy foo.ova esxi 192.0.2.100 -u admin -p admin \ + -n test_vm + + Deploy to vSphere/ESXi server 192.0.2.100, with username admin + (prompting the user to input a password at runtime), creating a VM + based on profile '1CPU-2.5GB' in foo.ova. + + cot deploy foo.ova esxi 192.0.2.100 -u admin -c 1CPU-2.5GB """ output_lines = ["Examples:"] # Just as in fill_usage, the default textwrap behavior diff --git a/COT/deploy.py b/COT/deploy.py index bb9c20b..3a2bbf2 100644 --- a/COT/deploy.py +++ b/COT/deploy.py @@ -43,20 +43,21 @@ def from_cli_string(cls, cli_string): Args: cli_string (str): String of the form 'kind:value[,opts]' - - :: - - >>> str(SerialConnection.from_cli_string('/dev/ttyS0')) - '' - >>> str(SerialConnection.from_cli_string('tcp::22,server')) - "" - >>> str(SerialConnection.from_cli_string('telnet://1.1.1.1:1111')) - '' - Returns: SerialConnection: Created instance or None. Raises: InvalidInputError: if ``cli_string`` cannot be parsed + + Examples: + :: + + >>> str(SerialConnection.from_cli_string('/dev/ttyS0')) + '' + >>> str(SerialConnection.from_cli_string('tcp::22,server')) + "" + >>> str(SerialConnection.from_cli_string('telnet://1.1.1.1:1111')) + '' + """ if cli_string is None: return None diff --git a/COT/edit_hardware.py b/COT/edit_hardware.py index 24d6b82..cdc4a5d 100644 --- a/COT/edit_hardware.py +++ b/COT/edit_hardware.py @@ -721,28 +721,30 @@ def create_subparser(self): def expand_list_wildcard(name_list, length, quiet=False): """Expand a list containing a wildcard to the desired length. + Args: + name_list (list): List of names to assign, or None + length (list): Length to expand to + quiet (bool): Silence usual log messages generated by this function. + + Returns: + list: Expanded list, or empty list if ``name_list`` is None or empty. + Since various items (NIC names, network names, etc.) are often named or numbered sequentially, we provide this API to allow the user to specify a wildcard value to permit automatically expanding a list of input strings to the desired length. The syntax for the wildcard option is ``{`` followed by a number (indicating the starting index for the name) followed by ``}``. - Examples:: - - >>> expand_list_wildcard(None, 3) - [] - >>> expand_list_wildcard(["eth{0}"], 3) - ['eth0', 'eth1', 'eth2'] - >>> expand_list_wildcard(["mgmt0", "eth{10}"], 4) - ['mgmt0', 'eth10', 'eth11', 'eth12'] - Args: - name_list (list): List of names to assign, or None - length (list): Length to expand to - quiet (bool): Silence usual log messages generated by this function. + Examples: + :: - Returns: - list: Expanded list, or empty list if ``name_list`` is None or empty. + >>> expand_list_wildcard(None, 3) + [] + >>> expand_list_wildcard(["eth{0}"], 3) + ['eth0', 'eth1', 'eth2'] + >>> expand_list_wildcard(["mgmt0", "eth{10}"], 4) + ['mgmt0', 'eth10', 'eth11', 'eth12'] """ if not name_list: return [] @@ -770,25 +772,26 @@ def expand_list_wildcard(name_list, length, quiet=False): def guess_list_wildcard(known_values): """Inverse of :func:`expand_list_wildcard`. Guess the wildcard for a list. - Examples:: - - >>> guess_list_wildcard(['foo', 'bar', 'baz']) - >>> guess_list_wildcard(['foo1', 'foo2', 'foo3']) - ['foo{1}'] - >>> guess_list_wildcard(['foo', 'bar', 'baz3', 'baz4', 'baz5']) - ['foo', 'bar', 'baz{3}'] - >>> guess_list_wildcard(['Eth0/1', 'Eth0/2', 'Eth0/3']) - ['Eth0/{1}'] - >>> guess_list_wildcard(['Eth0/0', 'Eth1/0', 'Eth2/0']) - ['Eth{0}/0'] - >>> guess_list_wildcard(['fake1', 'fake2', 'real4', 'real5']) - ['fake1', 'fake2', 'real{4}'] - Args: known_values (list): Values to guess from Returns: list: Guessed wildcard list, or None if unable to guess + + Examples: + :: + + >>> guess_list_wildcard(['foo', 'bar', 'baz']) + >>> guess_list_wildcard(['foo1', 'foo2', 'foo3']) + ['foo{1}'] + >>> guess_list_wildcard(['foo', 'bar', 'baz3', 'baz4', 'baz5']) + ['foo', 'bar', 'baz{3}'] + >>> guess_list_wildcard(['Eth0/1', 'Eth0/2', 'Eth0/3']) + ['Eth0/{1}'] + >>> guess_list_wildcard(['Eth0/0', 'Eth1/0', 'Eth2/0']) + ['Eth{0}/0'] + >>> guess_list_wildcard(['fake1', 'fake2', 'real4', 'real5']) + ['fake1', 'fake2', 'real{4}'] """ logger.debug("Attempting to infer a pattern from %s", known_values) # Guess sequences ending with simple N, N+1, N+2 diff --git a/COT/file_reference.py b/COT/file_reference.py index 83d52f9..a3d8206 100644 --- a/COT/file_reference.py +++ b/COT/file_reference.py @@ -38,15 +38,17 @@ def __init__(self, file_path, filename=None): directory containing this filename. If not specified, the final element in file_path is considered the filename. - :: - - >>> a = FileOnDisk('/etc/resolv.conf') - >>> b = FileOnDisk('/etc', 'resolv.conf') - >>> a == b - True - Raises: IOError: if no such file exists + + Examples: + :: + + >>> a = FileOnDisk('/etc/resolv.conf') + >>> b = FileOnDisk('/etc', 'resolv.conf') + >>> a == b + True + """ if filename is None: self.file_path = file_path diff --git a/COT/ovf/item.py b/COT/ovf/item.py index 321178a..9d163ae 100644 --- a/COT/ovf/item.py +++ b/COT/ovf/item.py @@ -50,20 +50,21 @@ def list_union(*lists): """Get union of lists. - :: - - >>> list_union([1, 2, 3], [0, 4], [1, 5]) - [1, 2, 3, 0, 4, 5] - >>> list_union(['foo'], ['bar'], ['bar', 'foo']) - ['foo', 'bar'] - >>> list_union(['bar', 'foo'], ['foo'], ['bar']) - ['bar', 'foo'] - Args: lists (list): List of lists to unify. Returns: list: All distinct values across the given lists. + + Examples: + :: + + >>> list_union([1, 2, 3], [0, 4], [1, 5]) + [1, 2, 3, 0, 4, 5] + >>> list_union(['foo'], ['bar'], ['bar', 'foo']) + ['foo', 'bar'] + >>> list_union(['bar', 'foo'], ['foo'], ['bar']) + ['bar', 'foo'] """ result = [] for l in lists: diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index 6e10788..33009a2 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -70,13 +70,6 @@ def byte_count(base_val, multiplier): Inverse operation of :func:`factor_bytes`. - :: - - >>> byte_count("128", "byte * 2^20") - 134217728 - >>> byte_count("512", "MegaBytes") - 536870912 - Args: base_val (str): Base value string (value of ``ovf:capacity``, etc.) multiplier (str): Multiplier string (value of @@ -84,6 +77,14 @@ def byte_count(base_val, multiplier): 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) @@ -114,18 +115,19 @@ def factor_bytes(byte_value): Inverse operation of :func:`byte_count` - :: - - >>> factor_bytes(134217728) - ('128', 'byte * 2^20') - >>> factor_bytes(134217729) - ('134217729', 'byte') - 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) @@ -141,29 +143,6 @@ def factor_bytes(byte_value): def byte_string(byte_value, base_shift=0): """Pretty-print the given bytes value. - :: - - >>> 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' - Args: byte_value (float): Value base_shift (int): Base value of byte_value @@ -171,6 +150,30 @@ def byte_string(byte_value, base_shift=0): 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) diff --git a/COT/vm_description.py b/COT/vm_description.py index 4ab6271..6715b11 100644 --- a/COT/vm_description.py +++ b/COT/vm_description.py @@ -598,18 +598,6 @@ def set_nic_mac_addresses(self, mac_list, profile_list): def set_nic_names(self, name_list, profile_list): """Set the device names for NICs under the given profile(s). - Since NICs are often named sequentially, this API supports a wildcard - option for the final element in :attr:`name_list` which can be - expanded to automatically assign sequential NIC names. - The syntax for the wildcard option is ``{`` followed by a number - (indicating the starting index for the name) followed by ``}``. - Examples: - - ``["eth{0}"]`` - Expands to ``["eth0", "eth1", "eth2", ...]`` - ``["mgmt0" "eth{10}"]`` - Expands to ``["mgmt0", "eth10", "eth11", "eth12", ...]`` - Args: name_list (list): List of names to assign. profile_list (list): Change only the given profiles From f72c1e0a276271b724277e303410bc930d15212a Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 2 Nov 2016 10:38:10 -0400 Subject: [PATCH 51/59] napoleon_use_rtype = False --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 4312cd9..025040a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -275,7 +275,7 @@ def autodoc_skip_member(app, what, name, obj, skip, options): # -- Napoleon configuration ------------------- -napoleon_use_rtype = True +napoleon_use_rtype = False # -- General configuration, continued --------- From 0f9ca4ea8f2825eb408605ac858fb0983e402182 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 7 Nov 2016 12:24:52 -0500 Subject: [PATCH 52/59] Update to versioneer 0.17 --- COT/_version.py | 118 +++++++---- versioneer.py | 517 ++++++++++++++++++++++++++---------------------- 2 files changed, 361 insertions(+), 274 deletions(-) diff --git a/COT/_version.py b/COT/_version.py index 917ebce..6637bb9 100644 --- a/COT/_version.py +++ b/COT/_version.py @@ -6,12 +6,10 @@ # that just contains the computed version number. # This file is released into the public domain. Generated by -# versioneer-0.15+dev (https://github.com/warner/python-versioneer) +# versioneer-0.17 (https://github.com/warner/python-versioneer) """Git implementation of _version.py.""" -from __future__ import print_function - import errno import os import re @@ -27,11 +25,12 @@ def get_keywords(): # get_keywords(). git_refnames = "$Format:%d$" git_full = "$Format:%H$" - keywords = {"refnames": git_refnames, "full": git_full} + git_date = "$Format:%ci$" + keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} return keywords -class VersioneerConfig(object): +class VersioneerConfig: """Container for Versioneer configuration parameters.""" @@ -68,7 +67,8 @@ def decorate(f): return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): """Call the given command(s).""" assert isinstance(commands, list) p = None @@ -76,7 +76,8 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None)) break @@ -87,36 +88,45 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): if verbose: print("unable to run %s" % dispcmd) print(e) - return None + return None, None else: if verbose: print("unable to find command, tried %s" % (commands,)) - return None + return None, None stdout = p.communicate()[0].strip() if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) - return None - return stdout + print("stdout was %s" % stdout) + return None, p.returncode + return stdout, p.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. - Source tarballs conventionally unpack into a directory that includes - both the project name and a version string. + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory """ - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%s', but '%s' doesn't start with " - "prefix '%s'" % (root, dirname, parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None} + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @register_vcs_handler("git", "get_keywords") @@ -138,6 +148,10 @@ def git_get_keywords(versionfile_abs): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) f.close() except EnvironmentError: pass @@ -149,6 +163,15 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if not keywords: raise NotThisMethod("no keywords at all, weird") + date = keywords.get("date") + if date is not None: + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: @@ -169,7 +192,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # "stabilization", as well as "HEAD" and "master". tags = set([r for r in refs if re.search(r'\d', r)]) if verbose: - print("discarding '%s', no digits" % ",".join(refs-tags)) + print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: print("likely tags: %s" % ",".join(sorted(tags))) for ref in sorted(tags): @@ -180,14 +203,14 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): print("picking %s" % r) return {"version": r, "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None - } + "dirty": False, "error": None, + "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags"} + "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") @@ -198,25 +221,28 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ - if not os.path.exists(os.path.join(root, ".git")): - if verbose: - print("no .git in %s" % root) - raise NotThisMethod("no .git directory") - GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %s not under git control" % root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() @@ -267,10 +293,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) pieces["distance"] = int(count_out) # total number of commits + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], + cwd=root)[0].strip() + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + return pieces @@ -417,7 +448,8 @@ def render(pieces, style): return {"version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, - "error": pieces["error"]} + "error": pieces["error"], + "date": None} if not style or style == "default": style = "pep440" # the default @@ -438,7 +470,8 @@ def render(pieces, style): raise ValueError("unknown style '%s'" % style) return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None} + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} def get_versions(): @@ -467,7 +500,8 @@ def get_versions(): except NameError: return {"version": "0+unknown", "full-revisionid": None, "dirty": None, - "error": "unable to find root of source tree"} + "error": "unable to find root of source tree", + "date": None} try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) @@ -483,4 +517,4 @@ def get_versions(): return {"version": "0+unknown", "full-revisionid": None, "dirty": None, - "error": "unable to compute version"} + "error": "unable to compute version", "date": None} diff --git a/versioneer.py b/versioneer.py index aaa3652..f250cde 100644 --- a/versioneer.py +++ b/versioneer.py @@ -1,5 +1,5 @@ -# Version: 0.15+dev +# Version: 0.17 """The Versioneer - like a rocketeer, but for versions. @@ -10,7 +10,7 @@ * https://github.com/warner/python-versioneer * Brian Warner * License: Public Domain -* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, and pypy +* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, and pypy * [![Latest Version] (https://pypip.in/version/versioneer/badge.svg?style=flat) ](https://pypi.python.org/pypi/versioneer/) @@ -88,125 +88,7 @@ ## Installation -First, decide on values for the following configuration variables: - -* `VCS`: the version control system you use. Currently accepts "git". - -* `style`: the style of version string to be produced. See "Styles" below for - details. Defaults to "pep440", which looks like - `TAG[+DISTANCE.gSHORTHASH[.dirty]]`. - -* `versionfile_source`: - - A project-relative pathname into which the generated version strings should - be written. This is usually a `_version.py` next to your project's main - `__init__.py` file, so it can be imported at runtime. If your project uses - `src/myproject/__init__.py`, this should be `src/myproject/_version.py`. - This file should be checked in to your VCS as usual: the copy created below - by `setup.py setup_versioneer` will include code that parses expanded VCS - keywords in generated tarballs. The 'build' and 'sdist' commands will - replace it with a copy that has just the calculated version string. - - This must be set even if your project does not have any modules (and will - therefore never import `_version.py`), since "setup.py sdist" -based trees - still need somewhere to record the pre-calculated version strings. Anywhere - in the source tree should do. If there is a `__init__.py` next to your - `_version.py`, the `setup.py setup_versioneer` command (described below) - will append some `__version__`-setting assignments, if they aren't already - present. - -* `versionfile_build`: - - Like `versionfile_source`, but relative to the build directory instead of - the source directory. These will differ when your setup.py uses - 'package_dir='. If you have `package_dir={'myproject': 'src/myproject'}`, - then you will probably have `versionfile_build='myproject/_version.py'` and - `versionfile_source='src/myproject/_version.py'`. - - If this is set to None, then `setup.py build` will not attempt to rewrite - any `_version.py` in the built tree. If your project does not have any - libraries (e.g. if it only builds a script), then you should use - `versionfile_build = None` and override `distutils.command.build_scripts` - to explicitly insert a copy of `versioneer.get_version()` into your - generated script. - -* `tag_prefix`: - - a string, like 'PROJECTNAME-', which appears at the start of all VCS tags. - If your tags look like 'myproject-1.2.0', then you should use - tag_prefix='myproject-'. If you use unprefixed tags like '1.2.0', this - should be an empty string, using either `tag_prefix=` or `tag_prefix=''`. - -* `parentdir_prefix`: - - a optional string, frequently the same as tag_prefix, which appears at the - start of all unpacked tarball filenames. If your tarball unpacks into - 'myproject-1.2.0', this should be 'myproject-'. To disable this feature, - just omit the field from your `setup.cfg`. - -This tool provides one script, named `versioneer`. That script has one mode, -"install", which writes a copy of `versioneer.py` into the current directory -and runs `versioneer.py setup` to finish the installation. - -To versioneer-enable your project: - -* 1: Modify your `setup.cfg`, adding a section named `[versioneer]` and - populating it with the configuration values you decided earlier (note that - the option names are not case-sensitive): - - ```` - [versioneer] - VCS = git - style = pep440 - versionfile_source = src/myproject/_version.py - versionfile_build = myproject/_version.py - tag_prefix = - parentdir_prefix = myproject- - ```` - -* 2: Run `versioneer install`. This will do the following: - - * copy `versioneer.py` into the top of your source tree - * create `_version.py` in the right place (`versionfile_source`) - * modify your `__init__.py` (if one exists next to `_version.py`) to define - `__version__` (by calling a function from `_version.py`) - * modify your `MANIFEST.in` to include both `versioneer.py` and the - generated `_version.py` in sdist tarballs - - `versioneer install` will complain about any problems it finds with your - `setup.py` or `setup.cfg`. Run it multiple times until you have fixed all - the problems. - -* 3: add a `import versioneer` to your setup.py, and add the following - arguments to the setup() call: - - version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), - -* 4: commit these changes to your VCS. To make sure you won't forget, - `versioneer install` will mark everything it touched for addition using - `git add`. Don't forget to add `setup.py` and `setup.cfg` too. - -## Post-Installation Usage - -Once established, all uses of your tree from a VCS checkout should get the -current version string. All generated tarballs should include an embedded -version string (so users who unpack them will not need a VCS tool installed). - -If you distribute your project through PyPI, then the release process should -boil down to two steps: - -* 1: git tag 1.0 -* 2: python setup.py register sdist upload - -If you distribute it through github (i.e. users use github to generate -tarballs with `git archive`), the process is: - -* 1: git tag 1.0 -* 2: git push; git push --tags - -Versioneer will report "0+untagged.NUMCOMMITS.gHASH" until your tree has at -least one tag in its history. +See [INSTALL.md](./INSTALL.md) for detailed installation instructions. ## Version-String Flavors @@ -227,6 +109,10 @@ * `['full-revisionid']`: detailed revision identifier. For Git, this is the full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". +* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the + commit date in ISO 8601 format. This will be None if the date is not + available. + * `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that this is only accurate if run in a VCS checkout, otherwise it is likely to be False or None @@ -276,47 +162,95 @@ display the full contents of `get_versions()` (including the `error` string, which may help identify what went wrong). -## Updating Versioneer +## Known Limitations -To upgrade your project to a new release of Versioneer, do the following: +Some situations are known to cause problems for Versioneer. This details the +most significant ones. More can be found on Github +[issues page](https://github.com/warner/python-versioneer/issues). -* install the new Versioneer (`pip install -U versioneer` or equivalent) -* edit `setup.cfg`, if necessary, to include any new configuration settings - indicated by the release notes -* re-run `versioneer install` in your source tree, to replace - `SRC/_version.py` -* commit any changed files +### Subprojects + +Versioneer has limited support for source trees in which `setup.py` is not in +the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are +two common reasons why `setup.py` might not be in the root: + +* Source trees which contain multiple subprojects, such as + [Buildbot](https://github.com/buildbot/buildbot), which contains both + "master" and "slave" subprojects, each with their own `setup.py`, + `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI + distributions (and upload multiple independently-installable tarballs). +* Source trees whose main purpose is to contain a C library, but which also + provide bindings to Python (and perhaps other langauges) in subdirectories. + +Versioneer will look for `.git` in parent directories, and most operations +should get the right version string. However `pip` and `setuptools` have bugs +and implementation details which frequently cause `pip install .` from a +subproject directory to fail to find a correct version string (so it usually +defaults to `0+unknown`). + +`pip install --editable .` should work correctly. `setup.py install` might +work too. + +Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in +some later version. -### Upgrading to 0.15 +[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking +this issue. The discussion in +[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the +issue from the Versioneer side in more detail. +[pip PR#3176](https://github.com/pypa/pip/pull/3176) and +[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve +pip to let Versioneer work correctly. -Starting with this version, Versioneer is configured with a `[versioneer]` -section in your `setup.cfg` file. Earlier versions required the `setup.py` to -set attributes on the `versioneer` module immediately after import. The new -version will refuse to run (raising an exception during import) until you -have provided the necessary `setup.cfg` section. +Versioneer-0.16 and earlier only looked for a `.git` directory next to the +`setup.cfg`, so subprojects were completely unsupported with those releases. -In addition, the Versioneer package provides an executable named -`versioneer`, and the installation process is driven by running `versioneer -install`. In 0.14 and earlier, the executable was named -`versioneer-installer` and was run without an argument. +### Editable installs with setuptools <= 18.5 -### Upgrading to 0.14 +`setup.py develop` and `pip install --editable .` allow you to install a +project into a virtualenv once, then continue editing the source code (and +test) without re-installing after every change. -0.14 changes the format of the version string. 0.13 and earlier used -hyphen-separated strings like "0.11-2-g1076c97-dirty". 0.14 and beyond use a -plus-separated "local version" section strings, with dot-separated -components, like "0.11+2.g1076c97". PEP440-strict tools did not like the old -format, but should be ok with the new one. +"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a +convenient way to specify executable scripts that should be installed along +with the python package. -### Upgrading from 0.11 to 0.12 +These both work as expected when using modern setuptools. When using +setuptools-18.5 or earlier, however, certain operations will cause +`pkg_resources.DistributionNotFound` errors when running the entrypoint +script, which must be resolved by re-installing the package. This happens +when the install happens with one version, then the egg_info data is +regenerated while a different version is checked out. Many setup.py commands +cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into +a different virtualenv), so this can be surprising. -Nothing special. +[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes +this one, but upgrading to a newer version of setuptools should probably +resolve it. -### Upgrading from 0.10 to 0.11 +### Unicode version strings -You must add a `versioneer.VCS = "git"` to your `setup.py` before re-running -`setup.py setup_versioneer`. This will enable the use of additional -version-control systems (SVN, etc) in the future. +While Versioneer works (and is continually tested) with both Python 2 and +Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. +Newer releases probably generate unicode version strings on py2. It's not +clear that this is wrong, but it may be surprising for applications when then +write these strings to a network connection or include them in bytes-oriented +APIs like cryptographic checksums. + +[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates +this question. + + +## Updating Versioneer + +To upgrade your project to a new release of Versioneer, do the following: + +* install the new Versioneer (`pip install -U versioneer` or equivalent) +* edit `setup.cfg`, if necessary, to include any new configuration settings + indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. +* re-run `versioneer install` in your source tree, to replace + `SRC/_version.py` +* commit any changed files ## Future Directions @@ -388,7 +322,9 @@ def get_root(): # os.path.dirname(__file__), as that will find whichever # versioneer.py was first imported, even in later projects. me = os.path.realpath(os.path.abspath(__file__)) - if os.path.splitext(me)[0] != os.path.splitext(versioneer_py)[0]: + me_dir = os.path.normcase(os.path.splitext(me)[0]) + vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) + if me_dir != vsr_dir: print("Warning: build in %s is using versioneer.py from %s" % (os.path.dirname(me), versioneer_py)) except NameError: @@ -444,7 +380,8 @@ def decorate(f): return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): """Call the given command(s).""" assert isinstance(commands, list) p = None @@ -452,7 +389,8 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None)) break @@ -463,19 +401,20 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): if verbose: print("unable to run %s" % dispcmd) print(e) - return None + return None, None else: if verbose: print("unable to find command, tried %s" % (commands,)) - return None + return None, None stdout = p.communicate()[0].strip() if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) - return None - return stdout + print("stdout was %s" % stdout) + return None, p.returncode + return stdout, p.returncode LONG_VERSION_PY['git'] = ''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag @@ -484,7 +423,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): # that just contains the computed version number. # This file is released into the public domain. Generated by -# versioneer-0.15+dev (https://github.com/warner/python-versioneer) +# versioneer-0.17 (https://github.com/warner/python-versioneer) """Git implementation of _version.py.""" @@ -503,7 +442,8 @@ def get_keywords(): # get_keywords(). git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" - keywords = {"refnames": git_refnames, "full": git_full} + git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" + keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} return keywords @@ -544,7 +484,8 @@ def decorate(f): return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, + env=None): """Call the given command(s).""" assert isinstance(commands, list) p = None @@ -552,7 +493,8 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, + p = subprocess.Popen([c] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None)) break @@ -563,36 +505,45 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): if verbose: print("unable to run %%s" %% dispcmd) print(e) - return None + return None, None else: if verbose: print("unable to find command, tried %%s" %% (commands,)) - return None + return None, None stdout = p.communicate()[0].strip() if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: print("unable to run %%s (error)" %% dispcmd) - return None - return stdout + print("stdout was %%s" %% stdout) + return None, p.returncode + return stdout, p.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. - Source tarballs conventionally unpack into a directory that includes - both the project name and a version string. + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory """ - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%%s', but '%%s' doesn't start with " - "prefix '%%s'" %% (root, dirname, parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None} + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %%s but none started with prefix %%s" %% + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @register_vcs_handler("git", "get_keywords") @@ -614,6 +565,10 @@ def git_get_keywords(versionfile_abs): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) f.close() except EnvironmentError: pass @@ -625,6 +580,15 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if not keywords: raise NotThisMethod("no keywords at all, weird") + date = keywords.get("date") + if date is not None: + # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: @@ -645,7 +609,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # "stabilization", as well as "HEAD" and "master". tags = set([r for r in refs if re.search(r'\d', r)]) if verbose: - print("discarding '%%s', no digits" %% ",".join(refs-tags)) + print("discarding '%%s', no digits" %% ",".join(refs - tags)) if verbose: print("likely tags: %%s" %% ",".join(sorted(tags))) for ref in sorted(tags): @@ -656,14 +620,14 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): print("picking %%s" %% r) return {"version": r, "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None - } + "dirty": False, "error": None, + "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags"} + "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") @@ -674,25 +638,28 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ - if not os.path.exists(os.path.join(root, ".git")): - if verbose: - print("no .git in %%s" %% root) - raise NotThisMethod("no .git directory") - GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %%s not under git control" %% root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%%s*" %% tag_prefix], - cwd=root) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%%s*" %% tag_prefix], + cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() @@ -743,10 +710,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) pieces["distance"] = int(count_out) # total number of commits + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], + cwd=root)[0].strip() + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + return pieces @@ -893,7 +865,8 @@ def render(pieces, style): return {"version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, - "error": pieces["error"]} + "error": pieces["error"], + "date": None} if not style or style == "default": style = "pep440" # the default @@ -914,7 +887,8 @@ def render(pieces, style): raise ValueError("unknown style '%%s'" %% style) return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None} + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} def get_versions(): @@ -943,7 +917,8 @@ def get_versions(): except NameError: return {"version": "0+unknown", "full-revisionid": None, "dirty": None, - "error": "unable to find root of source tree"} + "error": "unable to find root of source tree", + "date": None} try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) @@ -959,7 +934,7 @@ def get_versions(): return {"version": "0+unknown", "full-revisionid": None, "dirty": None, - "error": "unable to compute version"} + "error": "unable to compute version", "date": None} ''' @@ -982,6 +957,10 @@ def git_get_keywords(versionfile_abs): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) f.close() except EnvironmentError: pass @@ -993,6 +972,15 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if not keywords: raise NotThisMethod("no keywords at all, weird") + date = keywords.get("date") + if date is not None: + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: @@ -1013,7 +1001,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # "stabilization", as well as "HEAD" and "master". tags = set([r for r in refs if re.search(r'\d', r)]) if verbose: - print("discarding '%s', no digits" % ",".join(refs-tags)) + print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: print("likely tags: %s" % ",".join(sorted(tags))) for ref in sorted(tags): @@ -1024,14 +1012,14 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): print("picking %s" % r) return {"version": r, "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None - } + "dirty": False, "error": None, + "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags"} + "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") @@ -1042,25 +1030,28 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ - if not os.path.exists(os.path.join(root, ".git")): - if verbose: - print("no .git in %s" % root) - raise NotThisMethod("no .git directory") - GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] + + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=True) + if rc != 0: + if verbose: + print("Directory %s not under git control" % root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) + describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() @@ -1111,10 +1102,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) pieces["distance"] = int(count_out) # total number of commits + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], + cwd=root)[0].strip() + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + return pieces @@ -1122,7 +1118,7 @@ def do_vcs_install(manifest_in, versionfile_source, ipy): """Git-specific installation logic for Versioneer. For Git, this means creating/changing .gitattributes to mark _version.py - for export-time keyword substitution. + for export-subst keyword substitution. """ GITS = ["git"] if sys.platform == "win32": @@ -1159,27 +1155,34 @@ def do_vcs_install(manifest_in, versionfile_source, ipy): def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. - Source tarballs conventionally unpack into a directory that includes - both the project name and a version string. + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory """ - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%s', but '%s' doesn't start with " - "prefix '%s'" % (root, dirname, parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None} + rootdirs = [] + + for i in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + else: + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.15+dev) from +# This file was generated by 'versioneer.py' (0.17) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. import json -import sys version_json = ''' %s @@ -1200,6 +1203,9 @@ def versions_from_file(filename): raise NotThisMethod("unable to read _version.py") mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) + if not mo: + mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) if not mo: raise NotThisMethod("no version_json in _version.py") return json.loads(mo.group(1)) @@ -1359,7 +1365,8 @@ def render(pieces, style): return {"version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, - "error": pieces["error"]} + "error": pieces["error"], + "date": None} if not style or style == "default": style = "pep440" # the default @@ -1380,7 +1387,8 @@ def render(pieces, style): raise ValueError("unknown style '%s'" % style) return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None} + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} class VersioneerBadRootError(Exception): @@ -1459,7 +1467,8 @@ def get_versions(verbose=False): print("unable to compute version") return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, "error": "unable to compute version"} + "dirty": None, "error": "unable to compute version", + "date": None} def get_version(): @@ -1505,6 +1514,7 @@ def run(self): print("Version: %s" % vers["version"]) print(" full-revisionid: %s" % vers.get("full-revisionid")) print(" dirty: %s" % vers.get("dirty")) + print(" date: %s" % vers.get("date")) if vers["error"]: print(" error: %s" % vers["error"]) cmds["version"] = cmd_version @@ -1518,8 +1528,17 @@ def run(self): # setuptools/bdist_egg -> distutils/install_lib -> build_py # setuptools/install -> bdist_egg ->.. # setuptools/develop -> ? + # pip install: + # copies source tree to a tempdir before running egg_info/etc + # if .git isn't copied too, 'git describe' will fail + # then does setup.py bdist_wheel, or sometimes setup.py install + # setup.py egg_info -> ? - from distutils.command.build_py import build_py as _build_py + # we override different "build_py" commands for both environments + if "setuptools" in sys.modules: + from setuptools.command.build_py import build_py as _build_py + else: + from distutils.command.build_py import build_py as _build_py class cmd_build_py(_build_py): def run(self): @@ -1538,6 +1557,12 @@ def run(self): if "cx_Freeze" in sys.modules: # cx_freeze enabled? from cx_Freeze.dist import build_exe as _build_exe + # nczeczulin reports that py2exe won't like the pep440-style string + # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. + # setup(console=[{ + # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION + # "product_version": versioneer.get_version(), + # ... class cmd_build_exe(_build_exe): def run(self): @@ -1562,6 +1587,34 @@ def run(self): cmds["build_exe"] = cmd_build_exe del cmds["build_py"] + if 'py2exe' in sys.modules: # py2exe enabled? + try: + from py2exe.distutils_buildexe import py2exe as _py2exe # py3 + except ImportError: + from py2exe.build_exe import py2exe as _py2exe # py2 + + class cmd_py2exe(_py2exe): + def run(self): + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + target_versionfile = cfg.versionfile_source + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + + _py2exe.run(self) + os.unlink(target_versionfile) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + cmds["py2exe"] = cmd_py2exe + # we override different "sdist" commands for both environments if "setuptools" in sys.modules: from setuptools.command.sdist import sdist as _sdist @@ -1713,7 +1766,7 @@ def do_setup(): print(" versionfile_source already in MANIFEST.in") # Make VCS-specific changes. For git, this means creating/changing - # .gitattributes to mark _version.py for export-time keyword + # .gitattributes to mark _version.py for export-subst keyword # substitution. do_vcs_install(manifest_in, cfg.versionfile_source, ipy) return 0 From b9ba3026b2998d995c12b2c2bb33d5d126ad2a3f Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 7 Nov 2016 13:01:16 -0500 Subject: [PATCH 53/59] Fix TypeError when raising LookupError in find_item. Partial fix for #54. --- CHANGELOG.rst | 5 +++++ COT/ovf/hardware.py | 5 +++-- COT/ovf/item.py | 5 +++++ COT/ovf/tests/test_hardware.py | 38 ++++++++++++++++++++++++++++++++++ COT/vm_context_manager.py | 2 +- docs/thanks.rst | 1 + 6 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 COT/ovf/tests/test_hardware.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 45dbc7c..136f213 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,10 @@ This project adheres to `Semantic Versioning`_. `Unreleased`_ ------------- +**Fixed** + +- TypeError in ``find_item`` method (`#54`_). + **Added** - ``cot inject-config --extra-files`` parameter (`#53`_). @@ -549,6 +553,7 @@ Initial public release. .. _#51: https://github.com/glennmatthews/cot/issues/51 .. _#52: https://github.com/glennmatthews/cot/issues/52 .. _#53: https://github.com/glennmatthews/cot/issues/53 +.. _#54: https://github.com/glennmatthews/cot/issues/54 .. _Semantic Versioning: http://semver.org/ .. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/ diff --git a/COT/ovf/hardware.py b/COT/ovf/hardware.py index b08d494..e6f4187 100644 --- a/COT/ovf/hardware.py +++ b/COT/ovf/hardware.py @@ -242,8 +242,9 @@ def find_item(self, resource_type=None, properties=None, profile=None): """ matches = self.find_all_items(resource_type, properties, [profile]) if len(matches) > 1: - raise LookupError("Found multiple matching {0} Items:\n{2}" - .format(resource_type, "\n".join(matches))) + raise LookupError( + "Found multiple matching '{0}' Items (instances {1})" + .format(resource_type, [m.instance_id for m in matches])) elif len(matches) == 0: return None else: diff --git a/COT/ovf/item.py b/COT/ovf/item.py index 1884eec..d1c599d 100644 --- a/COT/ovf/item.py +++ b/COT/ovf/item.py @@ -132,6 +132,11 @@ def hardware_subtype(self): """Device hardware subtype such as 'virtio' or 'lsilogic'.""" return self.get_value(self.RESOURCE_SUB_TYPE) + @property + def instance_id(self): + """Device instance ID.""" + return self.get_value(self.INSTANCE_ID) + def property_values(self, name): """Get list of values known for a given property name.""" return list(self.properties[name].keys()) diff --git a/COT/ovf/tests/test_hardware.py b/COT/ovf/tests/test_hardware.py new file mode 100644 index 0000000..e21ad30 --- /dev/null +++ b/COT/ovf/tests/test_hardware.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# +# November 2016, Glenn F. Matthews +# Copyright (c) 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 COT.ovf.OVFHardware class.""" + +from COT.tests.ut import COT_UT + +from COT.vm_context_manager import VMContextManager + + +class TestOVFHardware(COT_UT): + """Unit test cases for the OVFHardware class.""" + + def test_find_item_multiple_matches(self): + """Check find_item raises LookupError if multiple matches are found.""" + with VMContextManager(self.input_ovf) as ovf: + hw = ovf.hardware + self.assertRaisesRegexp( + LookupError, + r"multiple matching 'ide' Items", + hw.find_item, resource_type='ide') + + def test_find_item_no_matches(self): + """Test that find_item returns None if no matches are found.""" + with VMContextManager(self.input_ovf) as ovf: + hw = ovf.hardware + self.assertEqual(None, hw.find_item(resource_type='usb')) diff --git a/COT/vm_context_manager.py b/COT/vm_context_manager.py index d67b86b..889784b 100644 --- a/COT/vm_context_manager.py +++ b/COT/vm_context_manager.py @@ -38,7 +38,7 @@ class VMContextManager(object): vm.bar() """ - def __init__(self, input_file, output_file): + def __init__(self, input_file, output_file=None): """Create a VM instance.""" self.obj = VMFactory.create(input_file, output_file) diff --git a/docs/thanks.rst b/docs/thanks.rst index 5ef465f..bb88116 100644 --- a/docs/thanks.rst +++ b/docs/thanks.rst @@ -13,6 +13,7 @@ We would like to thank: * Jeff Haag * Jeff Loughridge * Jonathan Muslow + * Scott O'Donnell * Rick Ogg * Keerthi Rawat * David Rosenfeld From be8a093bf42ee04ed5ca1d224aef144d882bad35 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 7 Nov 2016 13:47:29 -0500 Subject: [PATCH 54/59] Allow inject-config to handle multiple CD-ROM drives. Fixes #54 --- CHANGELOG.rst | 2 ++ COT/ovf/ovf.py | 5 +++- COT/ovf/tests/test_hardware.py | 2 +- COT/tests/test_inject_config.py | 48 +++++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 136f213..8f139ec 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,8 @@ This project adheres to `Semantic Versioning`_. **Fixed** - TypeError in ``find_item`` method (`#54`_). +- ``cot inject-config`` correctly handles OVAs with multiple empty CD-ROM + drives to choose amongst (`#54`_ also). **Added** diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index d83a96d..d97cc93 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -2606,9 +2606,12 @@ def find_empty_drive(self, drive_type): """ if drive_type == 'cdrom': # Find a drive that has no HostResource property - return self.hardware.find_item( + drives = self.hardware.find_all_items( resource_type=drive_type, properties={self.HOST_RESOURCE: None}) + if drives: + return drives[0] + return None elif drive_type == 'harddisk': # All harddisk items must have a HostResource, so we need a # different way to indicate an empty drive. By convention, diff --git a/COT/ovf/tests/test_hardware.py b/COT/ovf/tests/test_hardware.py index e21ad30..c159118 100644 --- a/COT/ovf/tests/test_hardware.py +++ b/COT/ovf/tests/test_hardware.py @@ -26,7 +26,7 @@ def test_find_item_multiple_matches(self): """Check find_item raises LookupError if multiple matches are found.""" with VMContextManager(self.input_ovf) as ovf: hw = ovf.hardware - self.assertRaisesRegexp( + self.assertRaisesRegex( LookupError, r"multiple matching 'ide' Items", hw.find_item, resource_type='ide') diff --git a/COT/tests/test_inject_config.py b/COT/tests/test_inject_config.py index c9ed9b0..92b6f5f 100644 --- a/COT/tests/test_inject_config.py +++ b/COT/tests/test_inject_config.py @@ -32,6 +32,7 @@ from COT.platforms import CSR1000V, IOSv, IOSXRv, IOSXRvLC from COT.helpers import helpers from COT.disks import disk_representation_from_file +from COT.remove_file import COTRemoveFile logger = logging.getLogger(__name__) @@ -184,6 +185,53 @@ def test_inject_config_iso_secondary(self): else: logger.info("isoinfo not available, not checking disk contents") + def test_inject_config_iso_multiple_drives(self): + """Inject config file on an ISO when multiple empty drives exist.""" + temp_ovf = os.path.join(self.temp_dir, "intermediate.ovf") + + # Remove the existing ISO from our input_ovf: + rm = COTRemoveFile(UI()) + rm.package = self.input_ovf + rm.output = temp_ovf + rm.file_path = "input.iso" + rm.run() + rm.finished() + rm.destroy() + + # Now we have two empty drives. + self.instance.package = temp_ovf + self.instance.config_file = self.config_file + self.instance.run() + self.assertLogged(**self.OVERWRITING_DISK_ITEM) + self.instance.finished() + config_iso = os.path.join(self.temp_dir, 'config.iso') + self.check_diff(""" + +- + ++ + +... + true ++ Configuration disk + CD-ROM 1 +- ovf:/file/file2 ++ ovf:/file/config.iso + 7""" + .format(vmdk_size=self.FILE_SIZE['input.vmdk'], + iso_size=self.FILE_SIZE['input.iso'], + cfg_size=self.FILE_SIZE['sample_cfg.txt'], + config_size=os.path.getsize(config_iso))) + 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, + ["config.txt"]) + else: + logger.info("isoinfo not available, not checking disk contents") + def test_inject_config_vmdk(self): """Inject config file on a VMDK.""" self.instance.package = self.iosv_ovf From 07e51423cd716b49bbbf5fe0bd1ed69e060dd520 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 7 Nov 2016 14:16:38 -0500 Subject: [PATCH 55/59] Don't warn about changing disk type if it's not actually changing --- COT/add_disk.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/COT/add_disk.py b/COT/add_disk.py index 213eef7..031e49c 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -409,9 +409,11 @@ def confirm_elements(vm, ui, file_obj, disk_image, disk_obj, disk_item, logger.warning("Overwriting existing Disk in OVF") if disk_item is not None: - ui.confirm_or_die("Existing disk Item is a {0}. Change it to a {1}?" - .format(disk_item.hardware_type, - drive_type)) + if disk_item.hardware_type != drive_type: + ui.confirm_or_die( + "Existing disk Item is a {0}. Change it to a {1}?" + .format(disk_item.hardware_type, + drive_type)) # We'll overwrite the existing disk Item instead of deleting # and recreating it, in order to preserve things like Description logger.warning("Overwriting existing disk Item in OVF") From e483503a5d1837768797085433d2eb6d443adfc6 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 8 Nov 2016 09:30:14 -0500 Subject: [PATCH 56/59] Fix pylint error under Python 3 --- COT/deploy.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/COT/deploy.py b/COT/deploy.py index 3a2bbf2..58b342b 100644 --- a/COT/deploy.py +++ b/COT/deploy.py @@ -144,10 +144,7 @@ def validate_value(cls, kind, value): .format(kind)) @classmethod - def validate_options(cls, - kind, - value, # pylint: disable=unused-argument - options): + def validate_options(cls, kind, value, options): """Check that the given set of options are valid for this connection. Args: @@ -159,6 +156,7 @@ def validate_options(cls, Raises: InvalidInputError: if options are not valid. """ + # pylint: disable=unused-argument if kind == 'file': if 'datastore' not in options: raise InvalidInputError("For a serial connection to a file, " From c98d00449aa376eff6a17c7ce6ab752d4aab8e91 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 8 Nov 2016 10:06:47 -0500 Subject: [PATCH 57/59] Stop running pylint on python 3.3 due to http://bugs.python.org/issue10445 causing false positives (https://github.com/PyCQA/pylint/issues/1158) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fb42743..e2a8e81 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ envlist = [tox:travis] 2.6 = setup, py26, stats 2.7 = setup, flake8, pylint, py27, docs, stats -3.3 = setup, pylint, py33, stats +3.3 = setup, py33, stats 3.4 = setup, flake8, pylint, py34, docs, stats 3.5 = setup, pylint, py35, stats PyPy = setup, pypy, stats From c7ca1dadf3a1ae1a3b2270d9853cf8c5efd6262a Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 8 Nov 2016 11:15:45 -0500 Subject: [PATCH 58/59] needs_sphinx doesn't support minor versions in sphinx 1.3.x --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 025040a..7687728 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -233,7 +233,7 @@ def help_text_to_rst(help, dirpath): # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '1.3.1' +needs_sphinx = '1.3' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom From 905e7e2a2932b542f2066794c7e69366e25c6a4d Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 8 Nov 2016 11:20:27 -0500 Subject: [PATCH 59/59] Add version 1.8.0 --- CHANGELOG.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 77a0a46..b651c21 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,8 +3,8 @@ Change Log All notable changes to the COT project will be documented in this file. This project adheres to `Semantic Versioning`_. -`Unreleased`_ -------------- +`1.8.0`_ - 2016-11-08 +--------------------- **Fixed** @@ -594,6 +594,7 @@ Initial public release. .. _napoleon: http://www.sphinx-doc.org/en/latest/ext/napoleon.html .. _Unreleased: https://github.com/glennmatthews/cot/compare/master...develop +.. _1.8.0: https://github.com/glennmatthews/cot/compare/v1.7.4...v1.8.0 .. _1.7.4: https://github.com/glennmatthews/cot/compare/v1.7.3...v1.7.4 .. _1.7.3: https://github.com/glennmatthews/cot/compare/v1.7.2...v1.7.3 .. _1.7.2: https://github.com/glennmatthews/cot/compare/v1.7.1...v1.7.2