Skip to content

Commit

Permalink
Allow user to override Platform validation checks. Fixes #61.
Browse files Browse the repository at this point in the history
In support of this:
- add UI.validate_value() API
- Platform instances cache check results to avoid nagging the user
  over and over and over again about the same change from various
  points in COT runtime.
  • Loading branch information
glennmatthews committed Feb 23, 2017
1 parent e4be140 commit 5080706
Show file tree
Hide file tree
Showing 32 changed files with 506 additions and 622 deletions.
54 changes: 46 additions & 8 deletions CHANGELOG.rst
Expand Up @@ -8,14 +8,51 @@ This project adheres to `Semantic Versioning`_.

**Changed**

- Moved the ``to_string`` function from ``COT.data_validation`` to
``COT.ui_shared``.
- ``COT.ovf.ovf.byte_string`` has been moved and renamed to
``COT.ui_shared.pretty_bytes``.
- ``COT.ovf.ovf.byte_count`` has been moved and renamed to
``COT.ovf.utilities.programmatic_bytes_to_int``.
- ``COT.ovf.ovf.factor_bytes`` has been moved and renamed to
``COT.ovf.utilities.int_bytes_to_programmatic_units``.
- With ``cot edit-hardware``, the platform hardware validation is no longer
a hard limit. Instead, if a value appears to be invalid, the user will be
warned about the validation failure and given the option to continue or
abort (`#61`_).
``cot --force ...``, as usual, can be used to auto-continue without prompting.
- Lots of API changes:

- Moved the ``to_string`` function from ``COT.data_validation`` to
``COT.ui_shared``.
- Function ``COT.deploy_esxi.get_object_from_connection`` is now method
``PyVmomiVMReconfigSpec.lookup_object``.

- COT.disks module:

- Function ``create_disk`` is now split into class methods
``DiskRepresentation.for_new_file`` (creates the disk file and returns a
corresponding ``DiskRepresentation`` instance) and
``DiskRepresentation.create_file`` (creates disk file only).
- Function ``convert_disk`` is now class method
``DiskRepresentation.convert_to``
- Function ``disk_representation_from_file`` is now
class method ``DiskRepresentation.from_file``
- The ``DiskRepresentation`` constructor now only takes the path to a file
as input - if you want to create a new file, use
``DiskRepresentation.for_new_file`` instead of calling the
constructor directly.

- COT.ovf module:

- ``COT.ovf.ovf.byte_string`` has been moved and renamed to
``COT.ui_shared.pretty_bytes``.
- ``COT.ovf.ovf.byte_count`` has been moved and renamed to
``COT.ovf.utilities.programmatic_bytes_to_int``.
- ``COT.ovf.ovf.factor_bytes`` has been moved and renamed to
``COT.ovf.utilities.int_bytes_to_programmatic_units``.

- COT.platforms module:

- Class ``GenericPlatform`` is now ``Platform``.
- Function ``platform_from_product_class`` is now class method
``Platform.for_product_string`` and returns an instance
of a ``Platform`` class rather than the class object itself.
- Most ``Platform`` APIs are now instance methods instead of
class methods.
- Function ``is_known_product_class`` has been removed.

`1.9.1`_ - 2017-02-21
---------------------
Expand Down Expand Up @@ -633,6 +670,7 @@ Initial public release.
.. _#58: https://github.com/glennmatthews/cot/issues/58
.. _#59: https://github.com/glennmatthews/cot/issues/59
.. _#60: https://github.com/glennmatthews/cot/issues/60
.. _#61: https://github.com/glennmatthews/cot/issues/61

.. _Semantic Versioning: http://semver.org/
.. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/
Expand Down
6 changes: 3 additions & 3 deletions COT/add_disk.py
Expand Up @@ -393,7 +393,7 @@ def guess_controller_type(platform, ctrl_item, drive_type):
"""If a controller type wasn't specified, try to guess from context.
Args:
platform (Platform): Platform class to guess controller for
platform (Platform): Platform instance to guess controller for
ctrl_item (object): Any known controller object, or None
drive_type (str): "cdrom" or "harddisk"
Returns:
Expand All @@ -405,7 +405,7 @@ def guess_controller_type(platform, ctrl_item, drive_type):
::
>>> from COT.platforms import Platform
>>> guess_controller_type(Platform, None, 'harddisk')
>>> guess_controller_type(Platform(), None, 'harddisk')
'ide'
"""
if ctrl_item is None:
Expand All @@ -416,7 +416,7 @@ def guess_controller_type(platform, ctrl_item, drive_type):
ctrl_type = platform.controller_type_for_device(drive_type)
logger.warning("Guessing controller type should be %s "
"based on disk drive type %s and platform %s",
ctrl_type, drive_type, platform.__name__)
ctrl_type, drive_type, platform)
else:
ctrl_type = ctrl_item.hardware_type
if ctrl_type != 'ide' and ctrl_type != 'scsi':
Expand Down
19 changes: 14 additions & 5 deletions COT/data_validation.py
Expand Up @@ -57,6 +57,7 @@

import hashlib
import re
from collections import namedtuple
from distutils.util import strtobool

from COT.ui_shared import to_string
Expand Down Expand Up @@ -443,7 +444,7 @@ def no_whitespace(string):

def validate_int(string,
minimum=None, maximum=None,
label="input"):
label=None):
"""Parser helper function for validating integer arguments in a range.
Args:
Expand Down Expand Up @@ -476,6 +477,8 @@ def validate_int(string,
... print(e)
Value '100' for x is too high - must be at most 10
"""
if label is None:
label = "input"
try:
i = int(string)
except ValueError:
Expand All @@ -487,13 +490,14 @@ def validate_int(string,
return i


def non_negative_int(string):
def non_negative_int(string, label=None):
"""Parser helper function for integer arguments that must be 0 or more.
Alias for :func:`validate_int` setting :attr:`minimum` to 0.
Args:
string (str): String to validate.
label (str): Label to include in any errors raised
Returns:
int: Validated integer value
Raises:
Expand All @@ -512,16 +516,17 @@ def non_negative_int(string):
... print(e)
Value '-1' for input is too low - must be at least 0
"""
return validate_int(string, minimum=0)
return validate_int(string, minimum=0, label=label)


def positive_int(string):
def positive_int(string, label=None):
"""Parser helper function for integer arguments that must be 1 or more.
Alias for :func:`validate_int` setting :attr:`minimum` to 1.
Args:
string (str): String to validate.
label (str): Label to include in any errors raised
Returns:
int: Validated integer value
Raises:
Expand All @@ -538,7 +543,7 @@ def positive_int(string):
... print(e)
Value '0' for input is too low - must be at least 1
"""
return validate_int(string, minimum=1)
return validate_int(string, minimum=1, label=label)


def truth_value(value):
Expand Down Expand Up @@ -582,6 +587,10 @@ def truth_value(value):
)


ValidRange = namedtuple('ValidRange', ['minimum', 'maximum'])
"""Simple helper class representing a range of valid values."""


# Some handy exception and error types we can throw
class ValueMismatchError(ValueError):
"""Values which were expected to be equal turned out to be not equal."""
Expand Down
13 changes: 8 additions & 5 deletions COT/edit_hardware.py
Expand Up @@ -130,7 +130,7 @@ def cpus(self, value):
raise InvalidInputError("cpus value must be an integer")
if value < 1:
raise InvalidInputError("CPU count must be at least 1")
self.vm.platform.validate_cpu_count(value)
self.ui.validate_value(self.vm.platform.validate_cpu_count, value)
self._cpus = value

@property
Expand Down Expand Up @@ -169,7 +169,8 @@ def memory(self, value):
logger.warning("Memory units not specified, "
"guessing '%s' means '%s MiB'",
mem_value, mem_value)
self.vm.platform.validate_memory_amount(mem_value)
self.ui.validate_value(self.vm.platform.validate_memory_amount,
mem_value)
self._memory = mem_value

@property
Expand All @@ -183,7 +184,8 @@ def nics(self, value):
value = int(value)
except ValueError:
raise InvalidInputError("nics value must be an integer")
self.vm.platform.validate_nic_count(value)
non_negative_int(value, label="nics")
self.ui.validate_value(self.vm.platform.validate_nic_count, value)
self._nics = value

@property
Expand Down Expand Up @@ -215,7 +217,7 @@ def nic_types(self):
@nic_types.setter
def nic_types(self, value):
value = [canonicalize_nic_subtype(v) for v in value]
self.vm.platform.validate_nic_types(value)
self.ui.validate_value(self.vm.platform.validate_nic_types, value)
self._nic_types = value

@property
Expand All @@ -229,7 +231,8 @@ def serial_ports(self, value):
value = int(value)
except ValueError:
raise InvalidInputError("serial_ports value must be an integer")
self.vm.platform.validate_serial_count(value)
non_negative_int(value, label="serial_ports")
self.ui.validate_value(self.vm.platform.validate_serial_count, value)
self._serial_ports = value

@property
Expand Down
6 changes: 3 additions & 3 deletions COT/inject_config.py
Expand Up @@ -3,7 +3,7 @@
# inject_config.py - Implements "cot inject-config" command
#
# February 2014, Glenn F. Matthews
# Copyright (c) 2014-2016 the COT project developers.
# Copyright (c) 2014-2017 the COT project developers.
# See the COPYRIGHT.txt file at the top-level directory of this distribution
# and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt.
#
Expand Down Expand Up @@ -74,7 +74,7 @@ def config_file(self, value):
if not self.vm.platform.CONFIG_TEXT_FILE:
raise InvalidInputError(
"Configuration file not supported for platform {0}"
.format(self.vm.platform.__name__))
.format(self.vm.platform))
self._config_file = value

@property
Expand All @@ -98,7 +98,7 @@ def secondary_config_file(self, value):
if not self.vm.platform.SECONDARY_CONFIG_TEXT_FILE:
raise InvalidInputError(
"Secondary configuration file not supported "
"for platform {0}".format(self.vm.platform.__name__))
"for platform {0}".format(self.vm.platform))
self._secondary_config_file = value

@property
Expand Down
8 changes: 4 additions & 4 deletions COT/ovf/ovf.py
Expand Up @@ -365,7 +365,7 @@ def platform(self):
if self._platform is None:
self._platform = Platform.for_product_string(self.product_class)
logger.info("OVF product class %s --> platform %s",
self.product_class, self.platform.__name__)
self.product_class, self.platform)
return self._platform

def validate_hardware(self):
Expand Down Expand Up @@ -803,9 +803,9 @@ def _info_string_header(self, width):
str_list = []
str_list.append('-' * width)
str_list.append(self.input_file)
if self.platform and self.platform is not Platform:
if self.platform and self.platform.__class__ is not Platform:
str_list.append("COT detected platform type: {0}"
.format(self.platform.PLATFORM_NAME))
.format(self.platform))
str_list.append('-' * width)
return '\n'.join(str_list)

Expand Down Expand Up @@ -1803,7 +1803,7 @@ def config_file_to_properties(self, file_path, user_configurable=None):
i = 0
if not self.platform.LITERAL_CLI_STRING:
raise NotImplementedError("no known support for literal CLI on " +
self.platform.PLATFORM_NAME)
str(self.platform))
with open(file_path, 'r') as f:
for line in f:
line = line.strip()
Expand Down
9 changes: 5 additions & 4 deletions COT/platforms/__init__.py
Expand Up @@ -12,28 +12,29 @@

"""Package for identifying guest platforms and handling platform differences.
The :class:`Platform` class describes the API
The :class:`~COT.platforms.platform.Platform` class describes the API
and provides a generic implementation that can be overridden by subclasses
to provide platform-specific logic.
In general, other modules should not access subclasses directly but should
instead use the :meth:`Platform.for_product_string` API to
derive the appropriate subclass object.
instead use the :meth:`~COT.platforms.platform.Platform.for_product_string`
API to derive the appropriate subclass object.
API
---
.. autosummary::
:nosignatures:
Platform
~COT.platforms.platform.Platform
Platform modules
----------------
.. autosummary::
:toctree:
COT.platforms.platform
COT.platforms.cisco_csr1000v
COT.platforms.cisco_iosv
COT.platforms.cisco_iosxrv
Expand Down

0 comments on commit 5080706

Please sign in to comment.