From 26c0eb4ad568f892a25cd1c540cba546ca8f4774 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 18 Jan 2017 10:51:37 -0500 Subject: [PATCH 01/19] Add support for Python 3.6 --- CHANGELOG.rst | 7 +++++++ setup.py | 3 ++- tox.ini | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b23323b..3da2bd9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,13 @@ Change Log All notable changes to the COT project will be documented in this file. This project adheres to `Semantic Versioning`_. +`Unreleased`_ +------------- + +**Added** + +- Support for Python 3.6 + `1.8.2`_ - 2017-01-18 --------------------- diff --git a/setup.py b/setup.py index 30bf96e..1ce8522 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ # setup.py - installer script for COT package # # April 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. # @@ -169,6 +169,7 @@ def with_project_on_sys_path(self, func): 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], keywords='virtualization ovf ova esxi vmware vcenter', ) diff --git a/tox.ini b/tox.ini index c5c85f5..938ab24 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ max-complexity = 10 minversion=2.3.1 envlist = setup - py{26,27,33,34,35,py} + py{26,27,33,34,35,36,py} flake8 pylint docs @@ -24,6 +24,7 @@ envlist = 3.3 = setup, py33, stats 3.4 = setup, flake8, pylint, py34, docs, stats 3.5 = setup, pylint, py35, stats +3.6 = setup, pylint, py36, stats PyPy = setup, pypy, stats [testenv] From f3cb672cfb0739b9739b72472f3161af4601a1b0 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 18 Jan 2017 10:53:05 -0500 Subject: [PATCH 02/19] Enable 3.6 for Travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d5f228a..5608907 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ python: - 3.3 - 3.4 - 3.5 + - 3.6 addons: apt: From b87b50fa4eb4f95dc22d5cc88203c80e633d1d73 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 18 Jan 2017 11:13:15 -0500 Subject: [PATCH 03/19] Disable pylint under 3.6 until supported --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 938ab24..eaa689c 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,8 @@ envlist = 3.3 = setup, py33, stats 3.4 = setup, flake8, pylint, py34, docs, stats 3.5 = setup, pylint, py35, stats -3.6 = setup, pylint, py36, stats +# No pylint support for 3.6 yet - https://github.com/PyCQA/pylint/issues/1072 +3.6 = setup, py36, stats PyPy = setup, pypy, stats [testenv] From 69b0d1c68bffceb4d9f17c3234dfe0f64cd6d5ba Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 18 Jan 2017 13:56:51 -0500 Subject: [PATCH 04/19] Improve messages when helpers cannot be installed. Fixes #57 --- CHANGELOG.rst | 5 +++++ COT/helpers/fatdisk.py | 4 ++-- COT/helpers/helper.py | 49 ++++++++++++++++++++++++++++++----------- COT/helpers/ovftool.py | 6 ++--- COT/helpers/vmdktool.py | 4 ++-- 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3da2bd9..15c0556 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,10 @@ This project adheres to `Semantic Versioning`_. - Support for Python 3.6 +**Fixed** + +- Improved messaging when COT is unable to install a helper program (`#57`_). + `1.8.2`_ - 2017-01-18 --------------------- @@ -601,6 +605,7 @@ Initial public release. .. _#52: https://github.com/glennmatthews/cot/issues/52 .. _#53: https://github.com/glennmatthews/cot/issues/53 .. _#54: https://github.com/glennmatthews/cot/issues/54 +.. _#57: https://github.com/glennmatthews/cot/issues/57 .. _#58: https://github.com/glennmatthews/cot/issues/58 .. _#59: https://github.com/glennmatthews/cot/issues/59 diff --git a/COT/helpers/fatdisk.py b/COT/helpers/fatdisk.py index 465a6c3..c718e8b 100644 --- a/COT/helpers/fatdisk.py +++ b/COT/helpers/fatdisk.py @@ -3,7 +3,7 @@ # fatdisk.py - Helper for 'fatdisk' # # February 2015, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -54,7 +54,7 @@ def _install(self): helpers['port'].install_package('fatdisk') return elif platform.system() != 'Linux': - self.unsure_how_to_install() + raise self.unsure_how_to_install() # Fatdisk installation requires make helpers['make'].install() diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index f54c6a6..59491a0 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -3,7 +3,7 @@ # helper.py - Abstract provider of a non-Python helper program. # # February 2015, Glenn F. Matthews -# Copyright (c) 2015-2016 the COT project developers. +# Copyright (c) 2015-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. # @@ -51,6 +51,7 @@ import os.path import contextlib import errno +import platform import re import shutil import subprocess @@ -324,7 +325,7 @@ def install(self): if self.installed: return if not self.installable: - self.unsure_how_to_install() + raise self.unsure_how_to_install() logger.info("Installing '%s'...", self.name) # Call the subclass implementation self._install() @@ -338,11 +339,27 @@ def install(self): logger.info("Successfully installed '%s'", self.name) def unsure_how_to_install(self): - """Raise a NotImplementedError about missing install logic.""" + """Return a RuntimeError or NotImplementedError for install trouble.""" 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) + + if platform.system() == 'Darwin' and ( + 'port' in self._provider_package and not helpers['port']): + msg += ("\nCOT can use MacPorts (https://www.macports.org/)," + " if available on your system, to install {0} and other" + " helpers for you.".format(self.name)) + return RuntimeError(msg) + elif platform.system() == 'Linux' and ( + ('apt-get' in self._provider_package or + 'yum' in self._provider_package) and + not (helpers['apt-get'] or helpers['yum'])): + msg += ("\nCOT can use package managers 'yum' or 'apt-get' to" + " install helpers on your system, but it appears that" + " you have neither of these package managers?") + return RuntimeError(msg) + else: + return NotImplementedError(msg) def _install(self): """Subclass-specific implementation of installation logic.""" @@ -352,7 +369,7 @@ def _install(self): helpers[pm_name].install_package(package) return # We shouldn't get here under normal call flow and logic. - self.unsure_how_to_install() + raise self.unsure_how_to_install() @staticmethod @contextlib.contextmanager @@ -602,7 +619,7 @@ def helper_select(choices): Returns: Helper: The selected helper class instance. """ - for choice in choices: + def _name_min_ver_from_choice(choice): if isinstance(choice, str): # Helper name only, no version constraints name = choice @@ -611,21 +628,27 @@ def helper_select(choices): # Tuple of (name, version) (name, vers) = choice min_version = StrictVersion(vers) + + return (name, min_version) + + for choice in choices: + name, min_version = _name_min_ver_from_choice(choice) 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) + name, min_version = _name_min_ver_from_choice(choice) 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!") + msg = "No helper in list {0} is available or installable!".format(choices) + + for choice in choices: + name, _ = _name_min_ver_from_choice(choice) + msg += "\n" + str(helpers[name].unsure_how_to_install()) + + raise HelperNotFoundError(msg) diff --git a/COT/helpers/ovftool.py b/COT/helpers/ovftool.py index 2c36fab..6fdabd7 100644 --- a/COT/helpers/ovftool.py +++ b/COT/helpers/ovftool.py @@ -3,7 +3,7 @@ # ovftool.py - Helper for 'ovftool' # # February 2015, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -41,12 +41,12 @@ def installable(self): return False def unsure_how_to_install(self): - """Raise a NotImplementedError about missing install logic. + """Return a NotImplementedError about missing install logic. We override the default install implementation to raise a more detailed error message for ovftool. """ - raise NotImplementedError( + return 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/vmdktool.py b/COT/helpers/vmdktool.py index e1d560e..74e2257 100644 --- a/COT/helpers/vmdktool.py +++ b/COT/helpers/vmdktool.py @@ -3,7 +3,7 @@ # vmdktool.py - Helper for 'vmdktool' # # February 2015, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -54,7 +54,7 @@ def _install(self): helpers['port'].install_package('vmdktool') return elif platform.system() != 'Linux': - self.unsure_how_to_install() + raise self.unsure_how_to_install() # We don't have vmdktool in apt or yum yet, # but we can build it manually: From fd18dc3673b9084b4cff88c305c97d525a6e47f5 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 25 Jan 2017 14:07:40 -0500 Subject: [PATCH 05/19] Add support for 'brew' package manager. Fixes #55. --- CHANGELOG.rst | 2 ++ COT/disks/tests/test_iso.py | 29 ++++++++++------- COT/helpers/__init__.py | 4 ++- COT/helpers/brew.py | 50 ++++++++++++++++++++++++++++++ COT/helpers/fatdisk.py | 6 +++- COT/helpers/helper.py | 24 +++++++++----- COT/helpers/isoinfo.py | 3 +- COT/helpers/mkisofs.py | 3 +- COT/helpers/qemu_img.py | 3 +- COT/helpers/tests/test_fatdisk.py | 6 +++- COT/helpers/tests/test_helper.py | 22 +++++++++++-- COT/helpers/tests/test_mkisofs.py | 6 +++- COT/helpers/tests/test_qemu_img.py | 6 +++- COT/helpers/tests/test_vmdktool.py | 6 +++- COT/helpers/vmdktool.py | 8 ++++- docs/COT.helpers.brew.rst | 4 +++ 16 files changed, 152 insertions(+), 30 deletions(-) create mode 100644 COT/helpers/brew.py create mode 100644 docs/COT.helpers.brew.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 15c0556..0cb9be7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ This project adheres to `Semantic Versioning`_. **Added** - Support for Python 3.6 +- Support for `brew` package manager (`#55`_). **Fixed** @@ -605,6 +606,7 @@ Initial public release. .. _#52: https://github.com/glennmatthews/cot/issues/52 .. _#53: https://github.com/glennmatthews/cot/issues/53 .. _#54: https://github.com/glennmatthews/cot/issues/54 +.. _#55: https://github.com/glennmatthews/cot/issues/55 .. _#57: https://github.com/glennmatthews/cot/issues/57 .. _#58: https://github.com/glennmatthews/cot/issues/58 .. _#59: https://github.com/glennmatthews/cot/issues/59 diff --git a/COT/disks/tests/test_iso.py b/COT/disks/tests/test_iso.py index 6be1911..d2beffa 100644 --- a/COT/disks/tests/test_iso.py +++ b/COT/disks/tests/test_iso.py @@ -3,7 +3,7 @@ # test_iso.py - Unit test cases for ISO disk representation. # # October 2016, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -34,6 +34,11 @@ class TestISO(COT_UT): """Test cases for ISO class.""" + def setUp(self): + """Test case setup function called automatically before each test.""" + super(TestISO, self).setUp() + self.foo_iso = os.path.join(self.temp_dir, "foo.iso") + def tearDown(self): """Test case cleanup function called automatically after each test.""" for name in ['mkisofs', 'genisoimage', 'xorriso', 'isoinfo']: @@ -112,9 +117,9 @@ def test_create_without_files(self): def test_create_with_mkisofs(self, mock_call): """Creation of an ISO with mkisofs (default).""" helpers['mkisofs']._installed = True - ISO(path='foo.iso', files=[self.input_ovf]) + ISO(path=self.foo_iso, files=[self.input_ovf]) mock_call.assert_called_with( - ['-output', 'foo.iso', '-full-iso9660-filenames', + ['-output', self.foo_iso, '-full-iso9660-filenames', '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) @mock.patch("COT.helpers.mkisofs.GenISOImage.call") @@ -122,9 +127,9 @@ 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]) + ISO(path=self.foo_iso, files=[self.input_ovf]) mock_call.assert_called_with( - ['-output', 'foo.iso', '-full-iso9660-filenames', + ['-output', self.foo_iso, '-full-iso9660-filenames', '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) @mock.patch("COT.helpers.mkisofs.XorrISO.call") @@ -133,10 +138,11 @@ def test_create_with_xorriso(self, mock_call): helpers['mkisofs']._installed = False helpers['genisoimage']._installed = False helpers['xorriso']._installed = True - ISO(path='foo.iso', files=[self.input_ovf]) + ISO(path=self.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]) + ['-as', 'mkisofs', '-output', self.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].""" @@ -144,20 +150,21 @@ def test_create_no_helpers_available(self): helpers['genisoimage']._installed = False helpers['xorriso']._installed = False helpers['apt-get']._installed = False + helpers['brew']._installed = False helpers['port']._installed = False helpers['yum']._installed = False self.assertRaises(HelperNotFoundError, ISO, - path='foo.iso', + path=self.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).""" helpers['mkisofs']._installed = True - ISO(path='foo.iso', files=[self.input_ovf], disk_subformat="") + ISO(path=self.foo_iso, files=[self.input_ovf], disk_subformat="") mock_call.assert_called_with( - ['-output', 'foo.iso', '-full-iso9660-filenames', + ['-output', self.foo_iso, '-full-iso9660-filenames', '-iso-level', '2', '-allow-lowercase', self.input_ovf]) def test_file_is_this_type_nonexistent(self): diff --git a/COT/helpers/__init__.py b/COT/helpers/__init__.py index aa4a5f6..ab4e02c 100644 --- a/COT/helpers/__init__.py +++ b/COT/helpers/__init__.py @@ -1,5 +1,5 @@ # February 2015, Glenn F. Matthews -# Copyright (c) 2015-2016 the COT project developers. +# Copyright (c) 2015-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. # @@ -38,6 +38,7 @@ COT.helpers.helper COT.helpers.apt_get + COT.helpers.brew COT.helpers.fatdisk COT.helpers.gcc COT.helpers.isoinfo @@ -55,6 +56,7 @@ HelperError, HelperNotFoundError, helper_select, ) from .apt_get import AptGet # noqa +from .brew import Brew # noqa from .fatdisk import FatDisk # noqa from .gcc import GCC # noqa from .isoinfo import ISOInfo # noqa diff --git a/COT/helpers/brew.py b/COT/helpers/brew.py new file mode 100644 index 0000000..6910a83 --- /dev/null +++ b/COT/helpers/brew.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# +# brew.py - Wrapper for the Homebrew 'brew' package manager. +# +# January 2017, Glenn F. Matthews +# Copyright (c) 2017 the COT project developers. +# See the COPYRIGHT.txt file at the top-level directory of this distribution +# and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. +# +# This file is part of the Common OVF Tool (COT) project. +# It is subject to the license terms in the LICENSE.txt file found in the +# top-level directory of this distribution and at +# https://github.com/glennmatthews/cot/blob/master/LICENSE.txt. No part +# of COT, including this file, may be copied, modified, propagated, or +# distributed except according to the terms contained in the LICENSE.txt file. + +"""Wrapper for the Homebrew 'brew' package manager for Mac (http://brew.sh).""" + +import logging + +from COT.helpers.helper import PackageManager + +logger = logging.getLogger(__name__) + + +class Brew(PackageManager): + """The 'brew' package manager utility.""" + + def __init__(self): + """Initializer.""" + super(Brew, self).__init__( + "brew", + info_uri="http://brew.sh/", + version_args=['--version'], + version_regexp=r"Homebrew ([0-9.]+)") + + def install_package(self, # pylint: disable=arguments-differ + package, + opts=None): + """Install the requested package if needed. + + Args: + package (str): Name of the package to install. + opts (list): Additional parameters to append to the command. + """ + # Brew automatically updates when called so no need for us to do it. + cmd = ['install', package] + if opts: + cmd += opts + self.call(cmd, capture_output=False) diff --git a/COT/helpers/fatdisk.py b/COT/helpers/fatdisk.py index c718e8b..9df2b3d 100644 --- a/COT/helpers/fatdisk.py +++ b/COT/helpers/fatdisk.py @@ -42,7 +42,7 @@ def __init__(self): @property def installable(self): """Whether COT is capable of installing this program on this system.""" - return bool(helpers['port'] or + return bool(helpers['brew'] or helpers['port'] or (platform.system() == 'Linux' and (helpers['make'] or helpers['make'].installable) and (helpers['clang'] or helpers['gcc'] or @@ -53,6 +53,10 @@ def _install(self): if helpers['port']: helpers['port'].install_package('fatdisk') return + elif helpers['brew']: + helpers['brew'].install_package('glennmatthews/fatdisk/fatdisk', + opts=['--devel']) + return elif platform.system() != 'Linux': raise self.unsure_how_to_install() diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 59491a0..dd8ad89 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -344,12 +344,20 @@ def unsure_how_to_install(self): if self.info_uri: msg += "\nRefer to {0} for information".format(self.info_uri) - if platform.system() == 'Darwin' and ( - 'port' in self._provider_package and not helpers['port']): - msg += ("\nCOT can use MacPorts (https://www.macports.org/)," - " if available on your system, to install {0} and other" - " helpers for you.".format(self.name)) - return RuntimeError(msg) + if platform.system() == 'Darwin': + if 'brew' in self._provider_package and not helpers['brew']: + msg += ("\nCOT can use Homebrew (https://brew.sh), " + "if available on your system, to install {0}." + .format(self.name)) + if 'port' in self._provider_package and not helpers['port']: + msg += ("\nCOT can use MacPorts (https://www.macports.org/), " + "if available on your system, to install {0}." + .format(self.name)) + if ('brew' in self._provider_package or + 'port' in self._provider_package): + return RuntimeError(msg) + else: + return NotImplementedError(msg) elif platform.system() == 'Linux' and ( ('apt-get' in self._provider_package or 'yum' in self._provider_package) and @@ -479,11 +487,13 @@ def cp(src, dest): class PackageManager(Helper): """Helper program with additional API method install_package().""" - def install_package(self, package): + def install_package(self, package, *args, **kwargs): """Install the requested package if needed. Args: package (str): Name of the package to install. + *args (list): Subclasses may accept additional positional args. + **kwargs (dict): Subclasses may accept additional keyword args. """ raise NotImplementedError("install_package not implemented!") diff --git a/COT/helpers/isoinfo.py b/COT/helpers/isoinfo.py index 638299f..7381470 100644 --- a/COT/helpers/isoinfo.py +++ b/COT/helpers/isoinfo.py @@ -3,7 +3,7 @@ # isoinfo.py - Helper for 'isoinfo' # # September 2016, Glenn F. Matthews -# Copyright (c) 2016 the COT project developers. +# Copyright (c) 2016-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -30,6 +30,7 @@ class ISOInfo(Helper): _provider_package = { 'apt-get': 'genisoimage', + 'brew': 'cdrtools', 'port': 'cdrtools', } diff --git a/COT/helpers/mkisofs.py b/COT/helpers/mkisofs.py index 63fb0d7..1fdb4a0 100644 --- a/COT/helpers/mkisofs.py +++ b/COT/helpers/mkisofs.py @@ -3,7 +3,7 @@ # mkisofs.py - Helper for 'mkisofs' and 'genisoimage' # # February 2015, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -39,6 +39,7 @@ class MkISOFS(Helper): """ _provider_package = { + 'brew': 'cdrtools', 'port': 'cdrtools', } diff --git a/COT/helpers/qemu_img.py b/COT/helpers/qemu_img.py index e85410c..2c6f881 100644 --- a/COT/helpers/qemu_img.py +++ b/COT/helpers/qemu_img.py @@ -3,7 +3,7 @@ # qemu_img.py - Helper for 'qemu-img' # # February 2015, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -27,6 +27,7 @@ class QEMUImg(Helper): _provider_package = { 'apt-get': 'qemu-utils', + 'brew': 'qemu', 'port': 'qemu', 'yum': 'qemu-img', } diff --git a/COT/helpers/tests/test_fatdisk.py b/COT/helpers/tests/test_fatdisk.py index c455ea1..40cac25 100644 --- a/COT/helpers/tests/test_fatdisk.py +++ b/COT/helpers/tests/test_fatdisk.py @@ -3,7 +3,7 @@ # fatdisk.py - Unit test cases for COT.helpers.fatdisk submodule. # # March 2015, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -128,6 +128,10 @@ def test_install_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_brew(self, *_): + """Test installation via 'brew'.""" + self.brew_install_test(['glennmatthews/fatdisk/fatdisk', '--devel']) + def test_install_port(self, *_): """Test installation via 'port'.""" self.port_install_test('fatdisk') diff --git a/COT/helpers/tests/test_helper.py b/COT/helpers/tests/test_helper.py index f8c828d..cc937de 100644 --- a/COT/helpers/tests/test_helper.py +++ b/COT/helpers/tests/test_helper.py @@ -3,7 +3,7 @@ # test_helpers.py - Unit test cases for COT.helpers submodule. # # February 2015, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -74,7 +74,7 @@ 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 ['apt-get', 'port', 'yum']: + for pm_name in ['apt-get', 'brew', 'port', 'yum']: helpers[pm_name]._installed = (pm_name == name) def enable_apt_install(self): @@ -137,6 +137,24 @@ def apt_install_test(self, pkgname, helpername, *_): mock_check_call, [['apt-get', '-q', 'install', pkgname]]) + @mock.patch('distutils.spawn.find_executable', return_value=None) + def brew_install_test(self, brew_params, *_): + """Test installation with 'brew'. + + Args: + brew_params (str,): Homebrew formula name to test, or list of args. + """ + self.select_package_manager('brew') + if isinstance(brew_params, str): + brew_params = [brew_params] + # 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') as mock_path: + mock_path.return_value = (None, '/bin/' + brew_params[0]) + self.helper.install() + mock_check_call.assert_called_with( + ['brew', 'install'] + brew_params) + @mock.patch('distutils.spawn.find_executable', return_value=None) def port_install_test(self, portname, *_): """Test installation with 'port'. diff --git a/COT/helpers/tests/test_mkisofs.py b/COT/helpers/tests/test_mkisofs.py index 64c73a7..48f5150 100644 --- a/COT/helpers/tests/test_mkisofs.py +++ b/COT/helpers/tests/test_mkisofs.py @@ -4,7 +4,7 @@ # test_mkisofs.py - Unit test cases for MkISOFS helper class. # # March 2015, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -56,6 +56,10 @@ def test_install_helper_already_present(self, mock_check_call, mock_check_output.assert_not_called() mock_check_call.assert_not_called() + def test_install_helper_brew(self): + """Test installation via 'brew'.""" + self.brew_install_test('cdrtools') + def test_install_helper_port(self): """Test installation via 'port'.""" self.port_install_test('cdrtools') diff --git a/COT/helpers/tests/test_qemu_img.py b/COT/helpers/tests/test_qemu_img.py index d71aa77..da074c0 100644 --- a/COT/helpers/tests/test_qemu_img.py +++ b/COT/helpers/tests/test_qemu_img.py @@ -3,7 +3,7 @@ # test_qemu_img.py - Unit test cases for COT.helpers.qemu_img submodule. # # March 2015, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -84,6 +84,10 @@ def test_install_apt_get(self): """Test installation via 'apt-get'.""" self.apt_install_test('qemu-utils', 'qemu-img') + def test_install_brew(self): + """Test installation via 'brew'.""" + self.brew_install_test('qemu') + def test_install_port(self): """Test installation via 'port'.""" self.port_install_test('qemu') diff --git a/COT/helpers/tests/test_vmdktool.py b/COT/helpers/tests/test_vmdktool.py index a1ae60e..2268a84 100644 --- a/COT/helpers/tests/test_vmdktool.py +++ b/COT/helpers/tests/test_vmdktool.py @@ -3,7 +3,7 @@ # test_vmdktool.py - Unit test cases for COT.helpers.vmdktool submodule. # # March 2015, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -118,6 +118,10 @@ def test_install_helper_apt_get(self, ['make', 'install', 'PREFIX=/opt/local', 'DESTDIR=/home/cot'], ]) + def test_install_helper_brew(self, *_): + """Test installation via 'brew'.""" + self.brew_install_test('glennmatthews/core/vmdktool') + def test_install_helper_port(self, *_): """Test installation via 'port'.""" self.port_install_test('vmdktool') diff --git a/COT/helpers/vmdktool.py b/COT/helpers/vmdktool.py index 74e2257..53c5dd3 100644 --- a/COT/helpers/vmdktool.py +++ b/COT/helpers/vmdktool.py @@ -46,13 +46,19 @@ def __init__(self): @property def installable(self): """Whether COT is capable of installing this program on this system.""" - return bool(helpers['apt-get'] or helpers['port'] or helpers['yum']) + return bool(helpers['apt-get'] or + helpers['brew'] or + helpers['port'] or + helpers['yum']) def _install(self): """Install ``vmdktool``.""" if helpers['port']: helpers['port'].install_package('vmdktool') return + elif helpers['brew']: + helpers['brew'].install_package('glennmatthews/core/vmdktool') + return elif platform.system() != 'Linux': raise self.unsure_how_to_install() diff --git a/docs/COT.helpers.brew.rst b/docs/COT.helpers.brew.rst new file mode 100644 index 0000000..da026ec --- /dev/null +++ b/docs/COT.helpers.brew.rst @@ -0,0 +1,4 @@ +``COT.helpers.brew`` module +=========================== + +.. automodule:: COT.helpers.brew From adeea936d2c68681fce0295f0b6272cf69beca19 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 2 Feb 2017 11:47:40 -0500 Subject: [PATCH 06/19] Fix local file path potential bug --- COT/disks/tests/test_iso.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/COT/disks/tests/test_iso.py b/COT/disks/tests/test_iso.py index 6be1911..8c5aacb 100644 --- a/COT/disks/tests/test_iso.py +++ b/COT/disks/tests/test_iso.py @@ -3,7 +3,7 @@ # test_iso.py - Unit test cases for ISO disk representation. # # October 2016, Glenn F. Matthews -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -34,6 +34,11 @@ class TestISO(COT_UT): """Test cases for ISO class.""" + def setUp(self): + """Test case setup function called automatically before each test.""" + super(TestISO, self).setUp() + self.foo_iso = os.path.join(self.temp_dir, "foo.iso") + def tearDown(self): """Test case cleanup function called automatically after each test.""" for name in ['mkisofs', 'genisoimage', 'xorriso', 'isoinfo']: @@ -112,9 +117,9 @@ def test_create_without_files(self): def test_create_with_mkisofs(self, mock_call): """Creation of an ISO with mkisofs (default).""" helpers['mkisofs']._installed = True - ISO(path='foo.iso', files=[self.input_ovf]) + ISO(path=self.foo_iso, files=[self.input_ovf]) mock_call.assert_called_with( - ['-output', 'foo.iso', '-full-iso9660-filenames', + ['-output', self.foo_iso, '-full-iso9660-filenames', '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) @mock.patch("COT.helpers.mkisofs.GenISOImage.call") @@ -122,9 +127,9 @@ 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]) + ISO(path=self.foo_iso, files=[self.input_ovf]) mock_call.assert_called_with( - ['-output', 'foo.iso', '-full-iso9660-filenames', + ['-output', self.foo_iso, '-full-iso9660-filenames', '-iso-level', '2', '-allow-lowercase', '-r', self.input_ovf]) @mock.patch("COT.helpers.mkisofs.XorrISO.call") @@ -133,10 +138,11 @@ def test_create_with_xorriso(self, mock_call): helpers['mkisofs']._installed = False helpers['genisoimage']._installed = False helpers['xorriso']._installed = True - ISO(path='foo.iso', files=[self.input_ovf]) + ISO(path=self.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]) + ['-as', 'mkisofs', '-output', self.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].""" @@ -148,16 +154,16 @@ def test_create_no_helpers_available(self): helpers['yum']._installed = False self.assertRaises(HelperNotFoundError, ISO, - path='foo.iso', + path=self.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).""" helpers['mkisofs']._installed = True - ISO(path='foo.iso', files=[self.input_ovf], disk_subformat="") + ISO(path=self.foo_iso, files=[self.input_ovf], disk_subformat="") mock_call.assert_called_with( - ['-output', 'foo.iso', '-full-iso9660-filenames', + ['-output', self.foo_iso, '-full-iso9660-filenames', '-iso-level', '2', '-allow-lowercase', self.input_ovf]) def test_file_is_this_type_nonexistent(self): From cbfd133961de0ebf5b63203105ebc55dd86cffe7 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 31 Jan 2017 19:35:07 -0500 Subject: [PATCH 07/19] Add some more doctests where possible and appropriate --- .pylintrc | 2 ++ COT/add_disk.py | 34 +++++++++++++++++++++++++--- COT/data_validation.py | 46 ++++++++++++++++++++++++++++++++++++-- COT/tests/test_doctests.py | 2 ++ 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/.pylintrc b/.pylintrc index aa69e81..e50751d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -18,6 +18,7 @@ reports=no # # Disabled due to redundancy with flake8: # bad-continuation +# line-too-long # trailing-whitespace # # Disabled due to useless noise: @@ -32,6 +33,7 @@ disable=bad-continuation, duplicate-code, fixme, invalid-name, + line-too-long, locally-disabled, too-few-public-methods, trailing-whitespace, diff --git a/COT/add_disk.py b/COT/add_disk.py index b5aa8a2..898341c 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -3,7 +3,7 @@ # add_disk.py - Implements "cot add-disk" command # # August 2013, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -59,6 +59,20 @@ def validate_controller_address(controller, address): Raises: InvalidInputError: if the address/controller combo is invalid. + + Examples: + :: + + >>> validate_controller_address("ide", "0:0") + >>> validate_controller_address("ide", "1:3") + Traceback (most recent call last): + ... + InvalidInputError: IDE disk address must be between 0:0 and 1:1 + >>> validate_controller_address("scsi", "1:3") + >>> validate_controller_address("scsi", "4:0") + Traceback (most recent call last): + ... + InvalidInputError: SCSI disk address must be between 0:0 and 3:15 """ logger.info("validate_controller_address: %s, %s", controller, address) if controller is not None and address is not None: @@ -269,7 +283,20 @@ def guess_drive_type_from_extension(disk_file_name): str: "cdrom" or "harddisk" Raises: InvalidInputError: if the disk type cannot be guessed. - """ + Examples: + :: + + >>> guess_drive_type_from_extension('/foo/bar.vmdk') + 'harddisk' + >>> guess_drive_type_from_extension('baz.vmdk.iso') + 'cdrom' + >>> guess_drive_type_from_extension('/etc/os-release') + Traceback (most recent call last): + ... + InvalidInputError: Unable to guess disk drive type for file '/etc/os-release' from its extension ''. + Known extensions are ['.img', '.iso', '.qcow2', '.raw', '.vmdk'] + Please specify '--type harddisk' or '--type cdrom'. + """ # noqa: E501 disk_extension = os.path.splitext(disk_file_name)[1] ext_type_map = { '.iso': 'cdrom', @@ -285,7 +312,8 @@ def guess_drive_type_from_extension(disk_file_name): "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())) + .format(disk_file_name, disk_extension, + sorted(ext_type_map.keys()))) return drive_type diff --git a/COT/data_validation.py b/COT/data_validation.py index 475a1c3..a51f5da 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -3,7 +3,7 @@ # data_validation.py - Helper libraries to validate data sanity # # September 2013, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -69,6 +69,18 @@ def to_string(obj): obj (object): Object to represent as a string. Returns: str: string representation + Examples: + :: + + >>> to_string("Hello") + 'Hello' + >>> to_string(27.5) + '27.5' + >>> e = ET.Element('hello', attrib={'key': 'value'}) + >>> str(e) # doctest: +ELLIPSIS + "" + >>> to_string(e) + '' """ if ET.iselement(obj): return ET.tostring(obj) @@ -77,12 +89,19 @@ def to_string(obj): def alphanum_split(key): - """Split the key into a list of [text, int, text, int, ...]. + """Split the key into a list of [text, int, text, int, ..., text]. Args: key (str): String to split. Returns: list: List of tokens + Examples: + :: + + >>> alphanum_split("hello1world27") + ['hello', 1, 'world', 27, ''] + >>> alphanum_split("1istheloneliestnumber") + ['', 1, 'istheloneliestnumber'] """ def text_to_int(text): """Convert number strings to ints, leave other strings as text. @@ -108,6 +127,13 @@ def natural_sort(l): l (list): List to sort Returns: list: Sorted list + Examples: + :: + + >>> natural_sort(["Eth3", "Eth1", "Eth10", "Eth2"]) + ['Eth1', 'Eth2', 'Eth3', 'Eth10'] + >>> natural_sort(["3rd", "1st", "10th", "101st"]) + ['1st', '3rd', '10th', '101st'] """ # Sort based on alphanum_split return value return sorted(l, key=alphanum_split) @@ -123,6 +149,13 @@ def match_or_die(first_label, first, second_label, second): second (object): Second object to compare Raises: ValueMismatchError: if ``first != second`` + Examples: + :: + + >>> match_or_die("old", 1, "new", 2) + Traceback (most recent call last): + ... + ValueMismatchError: old 1 does not match new 2 """ if first != second: raise ValueMismatchError("{0} {1} does not match {2} {3}" @@ -355,6 +388,15 @@ def no_whitespace(string): InvalidInputError: if string contains internal whitespace Returns: str: Validated string (with leading/trailing whitespace stripped) + Examples: + :: + + >>> no_whitespace(" hello ") + 'hello' + >>> no_whitespace('hello world') + Traceback (most recent call last): + ... + InvalidInputError: 'hello world' contains invalid whitespace """ string = string.strip() if len(string.split()) > 1: diff --git a/COT/tests/test_doctests.py b/COT/tests/test_doctests.py index 2265bad..b43907a 100644 --- a/COT/tests/test_doctests.py +++ b/COT/tests/test_doctests.py @@ -26,7 +26,9 @@ def load_tests(*_): For the parameters, see :mod:`unittest`. The parameters are unused here. """ suite = TestSuite() + suite.addTests(DocTestSuite('COT.add_disk')) suite.addTests(DocTestSuite('COT.cli')) + suite.addTests(DocTestSuite('COT.data_validation')) suite.addTests(DocTestSuite('COT.deploy')) suite.addTests(DocTestSuite('COT.edit_hardware')) suite.addTests(DocTestSuite('COT.edit_properties')) From 37b2fceb6e45288c58fa1c6578437add36a6508c Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 1 Feb 2017 09:53:53 -0500 Subject: [PATCH 08/19] More data validation doctests. --- COT/data_validation.py | 111 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/COT/data_validation.py b/COT/data_validation.py index a51f5da..7adab8f 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -200,6 +200,17 @@ def canonicalize_ide_subtype(subtype): Raises: ValueUnsupportedError: If the canonical string cannot be determined + Examples: + :: + + >>> canonicalize_ide_subtype('VirtIO') + 'virtio' + >>> canonicalize_ide_subtype('PIIX4') + 'PIIX4' + >>> canonicalize_ide_subtype('usb') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueUnsupportedError: Unsupported value 'usb' for IDE controller ... """ return canonicalize_helper("IDE controller subtype", subtype, [ @@ -230,6 +241,17 @@ def canonicalize_nic_subtype(subtype): str: The canonical string, one of :data:`NIC_TYPES` Raises: ValueUnsupportedError: If the canonical string cannot be determined + Examples: + :: + + >>> canonicalize_nic_subtype('e1000') + 'E1000' + >>> canonicalize_nic_subtype('vmxnet 3') + 'VMXNET3' + >>> canonicalize_nic_subtype('foobar') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueUnsupportedError: Unsupported value 'foobar' for NIC subtype ... .. seealso:: :meth:`COT.platforms.GenericPlatform.validate_nic_type` @@ -254,6 +276,17 @@ def canonicalize_scsi_subtype(subtype): Raises: ValueUnsupportedError: If the canonical string cannot be determined + Examples: + :: + + >>> canonicalize_scsi_subtype('LSI Logic') + 'lsilogic' + >>> canonicalize_scsi_subtype('VirtIO') + 'virtio' + >>> canonicalize_scsi_subtype('baz') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueUnsupportedError: Unsupported value 'baz' for SCSI controller ... """ return canonicalize_helper("SCSI controller subtype", subtype, [ @@ -276,6 +309,19 @@ def check_for_conflict(label, li): ValueMismatchError: if references differ Returns: object: the object or ``None`` + Examples: + :: + + >>> check_for_conflict("example", ['foo', None, 'foo']) + 'foo' + >>> check_for_conflict("conflict", [None, 'foo', 'bar']) + Traceback (most recent call last): + ... + ValueMismatchError: Found multiple candidates for the conflict: + foo + ...and... + bar + Please correct or clarify your search parameters. """ obj = None for i, obj1 in enumerate(li): @@ -284,7 +330,7 @@ def check_for_conflict(label, li): for obj2 in li[(i+1):]: if obj2 is not None and obj1 != obj2: raise ValueMismatchError( - "Found multiple candidates for the {0}: " + "Found multiple candidates for the {0}:" "\n{1}\n...and...\n{2}\nPlease correct or clarify " "your search parameters." .format(label, to_string(obj1), to_string(obj2))) @@ -361,7 +407,7 @@ def mac_address(string): def device_address(string): - """Parser helper function for device address arguments. + r"""Parser helper function for device address arguments. Validate string is an appropriately formed device address such as '1:0'. @@ -371,6 +417,15 @@ def device_address(string): InvalidInputError: if string is not a well-formatted device address Returns: str: Validated string (with leading/trailing whitespace stripped) + Examples: + :: + + >>> device_address(" 1:0\n") + '1:0' + >>> device_address("1:0:1") + Traceback (most recent call last): + ... + InvalidInputError: '1:0:1' is not a valid device address """ string = string.strip() if not re.match(r"\d+:\d+$", string): @@ -423,6 +478,20 @@ def validate_int(string, 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` + + Examples: + :: + + >>> validate_int('1') + 1 + >>> validate_int('foo', label='x') + Traceback (most recent call last): + ... + ValueUnsupportedError: Unsupported value 'foo' for x - expected integer + >>> validate_int('100', label='x', maximum=10) + Traceback (most recent call last): + ... + ValueTooHighError: Value '100' for x is too high - must be at most 10 """ try: i = int(string) @@ -447,6 +516,13 @@ def non_negative_int(string): Raises: ValueUnsupportedError: if :attr:`string` can't be converted to int ValueTooLowError: if value is less than 0 + Examples: + :: + + >>> non_negative_int('-1') + Traceback (most recent call last): + ... + ValueTooLowError: Value '-1' for input is too low - must be at least 0 """ return validate_int(string, minimum=0) @@ -463,6 +539,13 @@ def positive_int(string): Raises: ValueUnsupportedError: if :attr:`string` can't be converted to int ValueTooLowError: if value is less than 1 + Examples: + :: + + >>> positive_int('0') + Traceback (most recent call last): + ... + ValueTooLowError: Value '0' for input is too low - must be at least 1 """ return validate_int(string, minimum=1) @@ -470,7 +553,8 @@ def positive_int(string): def truth_value(value): """Parser helper function for truth values like '0', 'y', or 'false'. - Wrapper for :func:`distutils.util.strtobool` + Makes use of :func:`distutils.util.strtobool`, but returns True/False + rather than 1/0. Args: value (str): String to parse/validate @@ -478,11 +562,25 @@ def truth_value(value): bool: True or False Raises: ValueUnsupportedError: if the value can't be parsed to a boolean. + Examples: + :: + + >>> truth_value('y') + True + >>> truth_value('false') + False + >>> truth_value(True) + True + >>> truth_value('foo') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueUnsupportedError: Unsupported value 'foo' for truth value ... """ if isinstance(value, bool): return value try: - return strtobool(value) + # Despite its name, strtobool returns 1 or 0 not True or False + return bool(strtobool(value)) except ValueError: raise ValueUnsupportedError( "truth value", @@ -558,3 +656,8 @@ def __str__(self): return ("Value '{0}' for {1} is too high - must be at most {2}" .format(self.actual_value, self.value_type, self.expected_value)) + + +if __name__ == "__main__": + import doctest + doctest.testmod() From c4734aeb9577d01055e6e15aec0a29c4f8eaa677 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 1 Feb 2017 10:08:02 -0500 Subject: [PATCH 09/19] More doctests --- COT/add_disk.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/COT/add_disk.py b/COT/add_disk.py index 898341c..31987d9 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -41,9 +41,11 @@ 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 +from COT.data_validation import ( + InvalidInputError, ValueUnsupportedError, + check_for_conflict, device_address, match_or_die, +) +from COT.submodule import COTSubmodule logger = logging.getLogger(__name__) @@ -383,28 +385,34 @@ 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, drive_type): +def guess_controller_type(platform, 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 + platform (GenericPlatform): Platform class to guess controller for + ctrl_item (object): Any known controller object, or None drive_type (str): "cdrom" or "harddisk" Returns: str: 'ide' or 'scsi' Raises: - ValueUnsupportedError: if ``ctrl_item`` is not an IDE or SCSI - controller device. + ValueUnsupportedError: if ``ctrl_item`` is not None but is also not + an IDE or SCSI controller device. + Examples: + :: + + >>> from COT.platforms import GenericPlatform + >>> guess_controller_type(GenericPlatform, None, 'harddisk') + 'ide' """ 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 drive type. - ctrl_type = vm.platform.controller_type_for_device(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, vm.platform.__name__) + ctrl_type, drive_type, platform.__name__) else: ctrl_type = ctrl_item.hardware_type if ctrl_type != 'ide' and ctrl_type != 'scsi': @@ -571,7 +579,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, drive_type) + controller = guess_controller_type(vm.platform, ctrl_item, drive_type) if ctrl_item is None and address is None: # We didn't find a specific controller from the user info, @@ -620,3 +628,8 @@ def add_disk_worker(vm, # Finally, the disk Item vm.add_disk_device(drive_type, disk_addr, diskname, description, disk, file_obj, ctrl_item, disk_item) + + +if __name__ == "__main__": + import doctest + doctest.testmod() From fca4170ee8001e9b1e03761b574b8a64a1dc3e21 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 1 Feb 2017 10:09:07 -0500 Subject: [PATCH 10/19] Make doctest directly runnable --- COT/deploy.py | 7 ++++++- COT/edit_hardware.py | 5 +++++ COT/edit_properties.py | 9 +++++++-- COT/file_reference.py | 7 ++++++- COT/ovf/item.py | 5 +++++ COT/ovf/ovf.py | 5 +++++ 6 files changed, 34 insertions(+), 4 deletions(-) diff --git a/COT/deploy.py b/COT/deploy.py index 58b342b..cff7d18 100644 --- a/COT/deploy.py +++ b/COT/deploy.py @@ -3,7 +3,7 @@ # deploy.py - Implements "cot deploy" command # # June 2014, Kevin A. Keim -# Copyright (c) 2014-2016 the COT project developers. +# Copyright (c) 2014-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # # This file is part of the Common OVF Tool (COT) project. @@ -464,3 +464,8 @@ def create_subparser(self): "This argument may be repeated to specify more port connections. " "Each entry should be structured as 'kind:value' or " "'kind:value,options'.") + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/COT/edit_hardware.py b/COT/edit_hardware.py index f347a60..d8a4fd3 100644 --- a/COT/edit_hardware.py +++ b/COT/edit_hardware.py @@ -827,3 +827,8 @@ def guess_list_wildcard(known_values): logger.debug("Unable to guess a pattern") return None + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/COT/edit_properties.py b/COT/edit_properties.py index 62a6737..06d833d 100644 --- a/COT/edit_properties.py +++ b/COT/edit_properties.py @@ -30,8 +30,8 @@ import re import textwrap -from .submodule import COTSubmodule -from .data_validation import ( +from COT.submodule import COTSubmodule +from COT.data_validation import ( truth_value, ValueUnsupportedError, InvalidInputError ) @@ -382,3 +382,8 @@ def create_subparser(self): "arbitrary URI may be specified.") p.set_defaults(instance=self) + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/COT/file_reference.py b/COT/file_reference.py index 3386920..782d588 100644 --- a/COT/file_reference.py +++ b/COT/file_reference.py @@ -3,7 +3,7 @@ # file_reference.py - APIs abstracting away various ways to refer to a file. # # August 2015, Glenn F. Matthews -# Copyright (c) 2015-2016 the COT project developers. +# Copyright (c) 2015-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. # @@ -252,3 +252,8 @@ def add_to_archive(self, tarf): tarf.addfile(self.tarf.getmember(self.filename), self.obj) finally: self.close() + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/COT/ovf/item.py b/COT/ovf/item.py index ecdbc8b..1b5ee1d 100644 --- a/COT/ovf/item.py +++ b/COT/ovf/item.py @@ -812,3 +812,8 @@ def generate_items(self): item_list.append(item) return item_list + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/COT/ovf/ovf.py b/COT/ovf/ovf.py index 528a4e5..24b7705 100644 --- a/COT/ovf/ovf.py +++ b/COT/ovf/ovf.py @@ -2993,3 +2993,8 @@ def set_capacity_of_disk(self, disk, capacity_bytes): (capacity, cap_units) = factor_bytes(capacity_bytes) disk.set(self.DISK_CAPACITY, capacity) disk.set(self.DISK_CAP_UNITS, cap_units) + + +if __name__ == "__main__": + import doctest + doctest.testmod() From 586a16d901c3b019c3192a13e06b1f3fa056e594 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Wed, 1 Feb 2017 10:28:25 -0500 Subject: [PATCH 11/19] Add doctests for check_call/check_output --- COT/helpers/helper.py | 46 +++++++++++++++++++++++++++++- COT/helpers/tests/test_doctests.py | 30 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 COT/helpers/tests/test_doctests.py diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 59491a0..c1d9666 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -511,6 +511,23 @@ def check_call(args, require_success=True, retry_with_sudo=False, **kwargs): returns a value other than 0 (instead of a :class:`subprocess.CalledProcessError`). OSError: as :func:`subprocess.check_call`. + + Examples: + :: + + >>> check_call(['/nope/does/not/exist']) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + HelperNotFoundError: [Errno 2] Unable to locate helper program ... + >>> check_call(['false']) + Traceback (most recent call last): + ... + HelperError: [Errno 1] Helper program 'false' exited with error 1 + >>> check_call(['false'], require_success=False) + >>> check_call(['/etc/']) + Traceback (most recent call last): + ... + OSError: [Errno 13] Permission denied """ cmd = args[0] logger.info("Calling '%s'...", " ".join(args)) @@ -545,7 +562,7 @@ def check_call(args, require_success=True, retry_with_sudo=False, **kwargs): def check_output(args, require_success=True, retry_with_sudo=False, **kwargs): - """Wrapper for :func:`subprocess.check_output`. + r"""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. @@ -569,6 +586,28 @@ def check_output(args, require_success=True, retry_with_sudo=False, **kwargs): returns a value other than 0 (instead of a :class:`subprocess.CalledProcessError`). OSError: as :func:`subprocess.check_output`. + + Examples: + :: + + >>> check_output(['/nope/does/not/exist']) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + HelperNotFoundError: [Errno 2] Unable to locate helper program ... + >>> check_output(['false']) + Traceback (most recent call last): + ... + HelperError: [Errno 1] Helper program 'false' exited with error 1: + > false + + >>> check_output(['false'], require_success=False) + u'' + >>> check_output(['echo', 'Hello world!']) + u'Hello world!\n' + >>> check_output(['/etc/']) + Traceback (most recent call last): + ... + OSError: [Errno 13] Permission denied """ cmd = args[0] logger.info("Calling '%s' and capturing its output...", " ".join(args)) @@ -652,3 +691,8 @@ def _name_min_ver_from_choice(choice): msg += "\n" + str(helpers[name].unsure_how_to_install()) raise HelperNotFoundError(msg) + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/COT/helpers/tests/test_doctests.py b/COT/helpers/tests/test_doctests.py new file mode 100644 index 0000000..27c745b --- /dev/null +++ b/COT/helpers/tests/test_doctests.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# +# test_doctests.py - test runner for COT.helpers doctests +# +# February 2017, Glenn F. Matthews +# Copyright (c) 2016-2017 the COT project developers. +# See the COPYRIGHT.txt file at the top-level directory of this distribution +# and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. +# +# 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.helpers 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.helpers.helper')) + return suite From 1b4c4afb225af5927ddb049ff728019acc0f23da Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Thu, 2 Feb 2017 17:19:49 -0500 Subject: [PATCH 12/19] Fix doctests to work on Python 3.x --- COT/add_disk.py | 28 +++++---- COT/data_validation.py | 128 ++++++++++++++++++++++++----------------- COT/helpers/helper.py | 75 +++++++++++++++--------- 3 files changed, 138 insertions(+), 93 deletions(-) diff --git a/COT/add_disk.py b/COT/add_disk.py index 31987d9..e7f2e87 100644 --- a/COT/add_disk.py +++ b/COT/add_disk.py @@ -66,15 +66,17 @@ def validate_controller_address(controller, address): :: >>> validate_controller_address("ide", "0:0") - >>> validate_controller_address("ide", "1:3") - Traceback (most recent call last): - ... - InvalidInputError: IDE disk address must be between 0:0 and 1:1 + >>> try: + ... validate_controller_address("ide", "1:3") + ... except InvalidInputError as e: + ... print(e) + IDE disk address must be between 0:0 and 1:1 >>> validate_controller_address("scsi", "1:3") - >>> validate_controller_address("scsi", "4:0") - Traceback (most recent call last): - ... - InvalidInputError: SCSI disk address must be between 0:0 and 3:15 + >>> try: + ... validate_controller_address("scsi", "4:0") + ... except InvalidInputError as e: + ... print(e) + SCSI disk address must be between 0:0 and 3:15 """ logger.info("validate_controller_address: %s, %s", controller, address) if controller is not None and address is not None: @@ -292,10 +294,12 @@ def guess_drive_type_from_extension(disk_file_name): 'harddisk' >>> guess_drive_type_from_extension('baz.vmdk.iso') 'cdrom' - >>> guess_drive_type_from_extension('/etc/os-release') - Traceback (most recent call last): - ... - InvalidInputError: Unable to guess disk drive type for file '/etc/os-release' from its extension ''. + >>> try: + ... guess_drive_type_from_extension('/etc/os-release') + ... raise AssertionError("no exception raised") + ... except InvalidInputError as e: + ... print(e) + Unable to guess disk drive type for file '/etc/os-release' from its extension ''. Known extensions are ['.img', '.iso', '.qcow2', '.raw', '.vmdk'] Please specify '--type harddisk' or '--type cdrom'. """ # noqa: E501 diff --git a/COT/data_validation.py b/COT/data_validation.py index 7adab8f..1250451 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -59,6 +59,7 @@ import xml.etree.ElementTree as ET import hashlib import re +import sys from distutils.util import strtobool @@ -77,13 +78,16 @@ def to_string(obj): >>> to_string(27.5) '27.5' >>> e = ET.Element('hello', attrib={'key': 'value'}) - >>> str(e) # doctest: +ELLIPSIS - "" - >>> to_string(e) - '' + >>> print(e) # doctest: +ELLIPSIS + + >>> print(to_string(e)) + """ if ET.iselement(obj): - return ET.tostring(obj) + if sys.version_info[0] >= 3: + return ET.tostring(obj, encoding='unicode') + else: + return ET.tostring(obj) else: return str(obj) @@ -152,10 +156,11 @@ def match_or_die(first_label, first, second_label, second): Examples: :: - >>> match_or_die("old", 1, "new", 2) - Traceback (most recent call last): - ... - ValueMismatchError: old 1 does not match new 2 + >>> try: + ... match_or_die("old", 1, "new", 2) + ... except ValueMismatchError as e: + ... print(e) + old 1 does not match new 2 """ if first != second: raise ValueMismatchError("{0} {1} does not match {2} {3}" @@ -207,10 +212,11 @@ def canonicalize_ide_subtype(subtype): 'virtio' >>> canonicalize_ide_subtype('PIIX4') 'PIIX4' - >>> canonicalize_ide_subtype('usb') # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - ValueUnsupportedError: Unsupported value 'usb' for IDE controller ... + >>> try: # doctest: +ELLIPSIS + ... canonicalize_ide_subtype('usb') + ... except ValueUnsupportedError as e: + ... print(e) + Unsupported value 'usb' for IDE controller subtype... """ return canonicalize_helper("IDE controller subtype", subtype, [ @@ -248,10 +254,11 @@ def canonicalize_nic_subtype(subtype): 'E1000' >>> canonicalize_nic_subtype('vmxnet 3') 'VMXNET3' - >>> canonicalize_nic_subtype('foobar') # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - ValueUnsupportedError: Unsupported value 'foobar' for NIC subtype ... + >>> try: # doctest: +ELLIPSIS + ... canonicalize_nic_subtype('foobar') + ... except ValueUnsupportedError as e: + ... print(e) + Unsupported value 'foobar' for NIC subtype ... .. seealso:: :meth:`COT.platforms.GenericPlatform.validate_nic_type` @@ -283,10 +290,11 @@ def canonicalize_scsi_subtype(subtype): 'lsilogic' >>> canonicalize_scsi_subtype('VirtIO') 'virtio' - >>> canonicalize_scsi_subtype('baz') # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - ValueUnsupportedError: Unsupported value 'baz' for SCSI controller ... + >>> try: # doctest: +ELLIPSIS + ... canonicalize_scsi_subtype('baz') + ... except ValueUnsupportedError as e: + ... print(e) + Unsupported value 'baz' for SCSI controller subtype... """ return canonicalize_helper("SCSI controller subtype", subtype, [ @@ -314,10 +322,11 @@ def check_for_conflict(label, li): >>> check_for_conflict("example", ['foo', None, 'foo']) 'foo' - >>> check_for_conflict("conflict", [None, 'foo', 'bar']) - Traceback (most recent call last): - ... - ValueMismatchError: Found multiple candidates for the conflict: + >>> try: + ... check_for_conflict("conflict", [None, 'foo', 'bar']) + ... except ValueMismatchError as e: + ... print(e) + Found multiple candidates for the conflict: foo ...and... bar @@ -422,10 +431,11 @@ def device_address(string): >>> device_address(" 1:0\n") '1:0' - >>> device_address("1:0:1") - Traceback (most recent call last): - ... - InvalidInputError: '1:0:1' is not a valid device address + >>> try: + ... device_address("1:0:1") + ... except InvalidInputError as e: + ... print(e) + '1:0:1' is not a valid device address """ string = string.strip() if not re.match(r"\d+:\d+$", string): @@ -448,10 +458,11 @@ def no_whitespace(string): >>> no_whitespace(" hello ") 'hello' - >>> no_whitespace('hello world') - Traceback (most recent call last): - ... - InvalidInputError: 'hello world' contains invalid whitespace + >>> try: + ... no_whitespace('hello world') + ... except InvalidInputError as e: + ... print(e) + 'hello world' contains invalid whitespace """ string = string.strip() if len(string.split()) > 1: @@ -484,14 +495,16 @@ def validate_int(string, >>> validate_int('1') 1 - >>> validate_int('foo', label='x') - Traceback (most recent call last): - ... - ValueUnsupportedError: Unsupported value 'foo' for x - expected integer - >>> validate_int('100', label='x', maximum=10) - Traceback (most recent call last): - ... - ValueTooHighError: Value '100' for x is too high - must be at most 10 + >>> try: + ... validate_int('foo', label='x') + ... except ValueUnsupportedError as e: + ... print(e) + Unsupported value 'foo' for x - expected integer + >>> try: + ... validate_int('100', label='x', maximum=10) + ... except ValueTooHighError as e: + ... print(e) + Value '100' for x is too high - must be at most 10 """ try: i = int(string) @@ -519,10 +532,15 @@ def non_negative_int(string): Examples: :: - >>> non_negative_int('-1') - Traceback (most recent call last): - ... - ValueTooLowError: Value '-1' for input is too low - must be at least 0 + >>> non_negative_int('0') + 0 + >>> non_negative_int('1000') + 1000 + >>> try: + ... non_negative_int('-1') + ... except ValueTooLowError as e: + ... print(e) + Value '-1' for input is too low - must be at least 0 """ return validate_int(string, minimum=0) @@ -542,10 +560,13 @@ def positive_int(string): Examples: :: - >>> positive_int('0') - Traceback (most recent call last): - ... - ValueTooLowError: Value '0' for input is too low - must be at least 1 + >>> positive_int('1') + 1 + >>> try: + ... positive_int('0') + ... except ValueTooLowError as e: + ... print(e) + Value '0' for input is too low - must be at least 1 """ return validate_int(string, minimum=1) @@ -571,10 +592,11 @@ def truth_value(value): False >>> truth_value(True) True - >>> truth_value('foo') # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - ValueUnsupportedError: Unsupported value 'foo' for truth value ... + >>> try: # doctest: +ELLIPSIS + ... truth_value('foo') + ... except ValueUnsupportedError as e: + ... print(e) + Unsupported value 'foo' for truth value - expected ['y', ... """ if isinstance(value, bool): return value diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index c1d9666..ee6dd30 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -515,19 +515,29 @@ def check_call(args, require_success=True, retry_with_sudo=False, **kwargs): Examples: :: - >>> check_call(['/nope/does/not/exist']) # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - HelperNotFoundError: [Errno 2] Unable to locate helper program ... - >>> check_call(['false']) - Traceback (most recent call last): - ... - HelperError: [Errno 1] Helper program 'false' exited with error 1 + >>> check_call(['true']) + >>> try: + ... check_call(['false']) + ... except HelperError as e: + ... print(e.errno) + ... print(e.strerror) + 1 + Helper program 'false' exited with error 1 >>> check_call(['false'], require_success=False) - >>> check_call(['/etc/']) - Traceback (most recent call last): - ... - OSError: [Errno 13] Permission denied + >>> try: + ... check_call(['/non/exist']) + ... except HelperNotFoundError as e: + ... print(e.errno) + ... print(e.strerror) + 2 + Unable to locate helper program '/non/exist'. Please check your $PATH. + >>> try: + ... check_call(['/etc/']) + ... except OSError as e: + ... print(e.errno) + ... print(e.strerror) + 13 + Permission denied """ cmd = args[0] logger.info("Calling '%s'...", " ".join(args)) @@ -590,24 +600,33 @@ def check_output(args, require_success=True, retry_with_sudo=False, **kwargs): Examples: :: - >>> check_output(['/nope/does/not/exist']) # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - HelperNotFoundError: [Errno 2] Unable to locate helper program ... - >>> check_output(['false']) - Traceback (most recent call last): - ... - HelperError: [Errno 1] Helper program 'false' exited with error 1: + >>> output = check_output(['echo', 'Hello world!']) + >>> assert output == "Hello world!\n" + >>> try: + ... check_output(['false']) + ... except HelperError as e: + ... print(e.errno) + ... print(e.strerror) + 1 + Helper program 'false' exited with error 1: > false - >>> check_output(['false'], require_success=False) - u'' - >>> check_output(['echo', 'Hello world!']) - u'Hello world!\n' - >>> check_output(['/etc/']) - Traceback (most recent call last): - ... - OSError: [Errno 13] Permission denied + >>> output = check_output(['false'], require_success=False) + >>> assert output == '' + >>> try: + ... check_output(['/non/exist']) + ... except HelperNotFoundError as e: + ... print(e.errno) + ... print(e.strerror) + 2 + Unable to locate helper program '/non/exist'. Please check your $PATH. + >>> try: + ... check_output(['/etc/']) + ... except OSError as e: + ... print(e.errno) + ... print(e.strerror) + 13 + Permission denied """ cmd = args[0] logger.info("Calling '%s' and capturing its output...", " ".join(args)) From df3d81440f8c5e22ff749ea9dd85a6c07707255b Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 3 Feb 2017 09:12:33 -0500 Subject: [PATCH 13/19] Fix doctest to handle 2.6 str(Element) variation --- COT/data_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/COT/data_validation.py b/COT/data_validation.py index 1250451..bccea62 100644 --- a/COT/data_validation.py +++ b/COT/data_validation.py @@ -79,7 +79,7 @@ def to_string(obj): '27.5' >>> e = ET.Element('hello', attrib={'key': 'value'}) >>> print(e) # doctest: +ELLIPSIS - + >>> print(to_string(e)) """ From f5797175ed7f6da160980deeb8f68ee501d679cf Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 10 Feb 2017 11:08:36 -0500 Subject: [PATCH 14/19] vmdktool is now a core homebrew formula --- COT/helpers/tests/test_vmdktool.py | 2 +- COT/helpers/vmdktool.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/COT/helpers/tests/test_vmdktool.py b/COT/helpers/tests/test_vmdktool.py index 2268a84..b7493f8 100644 --- a/COT/helpers/tests/test_vmdktool.py +++ b/COT/helpers/tests/test_vmdktool.py @@ -120,7 +120,7 @@ def test_install_helper_apt_get(self, def test_install_helper_brew(self, *_): """Test installation via 'brew'.""" - self.brew_install_test('glennmatthews/core/vmdktool') + self.brew_install_test('vmdktool') def test_install_helper_port(self, *_): """Test installation via 'port'.""" diff --git a/COT/helpers/vmdktool.py b/COT/helpers/vmdktool.py index 53c5dd3..3f88187 100644 --- a/COT/helpers/vmdktool.py +++ b/COT/helpers/vmdktool.py @@ -57,7 +57,7 @@ def _install(self): helpers['port'].install_package('vmdktool') return elif helpers['brew']: - helpers['brew'].install_package('glennmatthews/core/vmdktool') + helpers['brew'].install_package('vmdktool') return elif platform.system() != 'Linux': raise self.unsure_how_to_install() From dd83ee1a372298f36707ab7e8ccfca3bc477e237 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 10 Feb 2017 11:28:44 -0500 Subject: [PATCH 15/19] Simplify install_package API --- COT/helpers/brew.py | 15 +++++++-------- COT/helpers/fatdisk.py | 5 +++++ COT/helpers/helper.py | 7 +++---- COT/helpers/vmdktool.py | 5 +++++ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/COT/helpers/brew.py b/COT/helpers/brew.py index 6910a83..a42e68e 100644 --- a/COT/helpers/brew.py +++ b/COT/helpers/brew.py @@ -34,17 +34,16 @@ def __init__(self): version_args=['--version'], version_regexp=r"Homebrew ([0-9.]+)") - def install_package(self, # pylint: disable=arguments-differ - package, - opts=None): + def install_package(self, package): """Install the requested package if needed. Args: - package (str): Name of the package to install. - opts (list): Additional parameters to append to the command. + package (str,list): Name of the package to install or list of + parameters needed to install the package. """ # Brew automatically updates when called so no need for us to do it. - cmd = ['install', package] - if opts: - cmd += opts + if isinstance(package, list): + cmd = ['install'] + package + else: + cmd = ['install', package] self.call(cmd, capture_output=False) diff --git a/COT/helpers/fatdisk.py b/COT/helpers/fatdisk.py index 9df2b3d..4fbf2d2 100644 --- a/COT/helpers/fatdisk.py +++ b/COT/helpers/fatdisk.py @@ -39,6 +39,11 @@ def __init__(self): info_uri="http://github.com/goblinhack/fatdisk", version_regexp="version ([0-9.]+)") + _provider_package = { + 'brew': ['glennmatthews/fatdisk/fatdisk', '--devel'], + 'port': 'fatdisk', + } + @property def installable(self): """Whether COT is capable of installing this program on this system.""" diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index c216203..82fee1b 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -487,13 +487,12 @@ def cp(src, dest): class PackageManager(Helper): """Helper program with additional API method install_package().""" - def install_package(self, package, *args, **kwargs): + def install_package(self, package): """Install the requested package if needed. Args: - package (str): Name of the package to install. - *args (list): Subclasses may accept additional positional args. - **kwargs (dict): Subclasses may accept additional keyword args. + package (str,list): Name of the package to install or list of + parameters needed to install the package. """ raise NotImplementedError("install_package not implemented!") diff --git a/COT/helpers/vmdktool.py b/COT/helpers/vmdktool.py index 3f88187..9db9b04 100644 --- a/COT/helpers/vmdktool.py +++ b/COT/helpers/vmdktool.py @@ -35,6 +35,11 @@ class VMDKTool(Helper): http://www.freshports.org/sysutils/vmdktool/ """ + _provider_package = { + 'brew': 'vmdktool', + 'port': 'vmdktool', + } + def __init__(self): """Initializer.""" super(VMDKTool, self).__init__( From 628bef5115e7f1f3405a16fe0845a395100dc738 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Fri, 10 Feb 2017 17:10:29 -0500 Subject: [PATCH 16/19] Improve code coverage of _install methods --- COT/helpers/brew.py | 4 ++-- COT/helpers/fatdisk.py | 15 +++++++-------- COT/helpers/helper.py | 14 ++++++++++---- COT/helpers/tests/test_fatdisk.py | 2 +- COT/helpers/tests/test_helper.py | 24 ++++++++++++++++++------ COT/helpers/tests/test_vmdktool.py | 2 +- COT/helpers/vmdktool.py | 14 +++++++------- docs/COT.helpers.helper.rst | 1 + 8 files changed, 47 insertions(+), 29 deletions(-) diff --git a/COT/helpers/brew.py b/COT/helpers/brew.py index a42e68e..75a9ac8 100644 --- a/COT/helpers/brew.py +++ b/COT/helpers/brew.py @@ -38,8 +38,8 @@ def install_package(self, package): """Install the requested package if needed. Args: - package (str,list): Name of the package to install or list of - parameters needed to install the package. + package (str): Name of the package to install, or a list of + parameters used to install the package. """ # Brew automatically updates when called so no need for us to do it. if isinstance(package, list): diff --git a/COT/helpers/fatdisk.py b/COT/helpers/fatdisk.py index 4fbf2d2..919b03d 100644 --- a/COT/helpers/fatdisk.py +++ b/COT/helpers/fatdisk.py @@ -55,15 +55,14 @@ def installable(self): def _install(self): """Install ``fatdisk``.""" - if helpers['port']: - helpers['port'].install_package('fatdisk') + try: + super(FatDisk, self)._install() return - elif helpers['brew']: - helpers['brew'].install_package('glennmatthews/fatdisk/fatdisk', - opts=['--devel']) - return - elif platform.system() != 'Linux': - raise self.unsure_how_to_install() + except NotImplementedError: + # We have an alternative install method available for Linux, + # below - but if not Linux, you're out of luck! + if platform.system() != 'Linux': + raise # Fatdisk installation requires make helpers['make'].install() diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 82fee1b..0a4422d 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -180,6 +180,7 @@ class Helper(object): call install + _install unsure_how_to_install """ @@ -370,13 +371,18 @@ def unsure_how_to_install(self): return NotImplementedError(msg) def _install(self): - """Subclass-specific implementation of installation logic.""" + """Subclass-specific implementation of installation logic. + + This method should only be called from :meth:`install`, + which does the appropriate pre-validation against the + :attr:`installed` and :attr:`installable` properties before + calling into this method if appropriate. + """ # Default implementation for pm_name, package in self._provider_package.items(): if helpers[pm_name]: helpers[pm_name].install_package(package) return - # We shouldn't get here under normal call flow and logic. raise self.unsure_how_to_install() @staticmethod @@ -491,8 +497,8 @@ def install_package(self, package): """Install the requested package if needed. Args: - package (str,list): Name of the package to install or list of - parameters needed to install the package. + package (str): Name of the package to install, or a list of + parameters used to install the package. """ raise NotImplementedError("install_package not implemented!") diff --git a/COT/helpers/tests/test_fatdisk.py b/COT/helpers/tests/test_fatdisk.py index 40cac25..4b5a184 100644 --- a/COT/helpers/tests/test_fatdisk.py +++ b/COT/helpers/tests/test_fatdisk.py @@ -203,4 +203,4 @@ def test_install_linux_need_compiler_no_package_manager(self, def test_install_helper_mac_no_package_manager(self, *_): """Mac installation requires port.""" self.select_package_manager(None) - self.assertRaises(NotImplementedError, self.helper.install) + self.assertRaises(RuntimeError, self.helper.install) diff --git a/COT/helpers/tests/test_helper.py b/COT/helpers/tests/test_helper.py index cc937de..b4cdaa7 100644 --- a/COT/helpers/tests/test_helper.py +++ b/COT/helpers/tests/test_helper.py @@ -215,13 +215,25 @@ def tearDown(self): @mock.patch('distutils.spawn.find_executable', return_value=None) @mock.patch('platform.system', return_value='Windows') - def test_install_unsupported(self, *_): - """Unable to install without a package manager.""" + def test_install_windows_unsupported(self, *_): + """No support for installation on Windows. + + This is a somewhat artificial test of logic in ``_install`` + that is normally unreachable when calling ``install()``. + """ + if self.helper is None: + return self.select_package_manager(None) - if self.helper: - with mock.patch.object(self.helper, '_path', new=None): - self.assertRaises(NotImplementedError, - self.helper.install) + self.assertRaises(NotImplementedError, self.helper._install) + + @mock.patch('distutils.spawn.find_executable', return_value=None) + @mock.patch('platform.system', return_value='Linux') + def test_install_linux_no_package_manager(self, *_): + """Unable to install on Linux without a package manager.""" + if self.helper is None: + return + self.select_package_manager(None) + self.assertRaises(RuntimeError, self.helper._install) class HelperGenericTest(HelperUT): diff --git a/COT/helpers/tests/test_vmdktool.py b/COT/helpers/tests/test_vmdktool.py index b7493f8..dc33b1d 100644 --- a/COT/helpers/tests/test_vmdktool.py +++ b/COT/helpers/tests/test_vmdktool.py @@ -174,4 +174,4 @@ def test_install_linux_need_compiler_no_package_manager(self, def test_install_helper_mac_no_package_manager(self, *_): """Mac installation requires port.""" self.select_package_manager(None) - self.assertRaises(NotImplementedError, self.helper.install) + self.assertRaises(RuntimeError, self.helper.install) diff --git a/COT/helpers/vmdktool.py b/COT/helpers/vmdktool.py index 9db9b04..fa64887 100644 --- a/COT/helpers/vmdktool.py +++ b/COT/helpers/vmdktool.py @@ -58,14 +58,14 @@ def installable(self): def _install(self): """Install ``vmdktool``.""" - if helpers['port']: - helpers['port'].install_package('vmdktool') + try: + super(VMDKTool, self)._install() return - elif helpers['brew']: - helpers['brew'].install_package('vmdktool') - return - elif platform.system() != 'Linux': - raise self.unsure_how_to_install() + except NotImplementedError: + # We have an alternative install method available for Linux, + # below - but if not Linux, you're out of luck! + if platform.system() != 'Linux': + raise # We don't have vmdktool in apt or yum yet, # but we can build it manually: diff --git a/docs/COT.helpers.helper.rst b/docs/COT.helpers.helper.rst index 7cf931d..892c448 100644 --- a/docs/COT.helpers.helper.rst +++ b/docs/COT.helpers.helper.rst @@ -2,5 +2,6 @@ ============================= .. automodule:: COT.helpers.helper + :private-members: _install :special-members: __init__ :exclude-members: TemporaryDirectory From 6d9ce37376066ec6acfae45e73b152990ae6c86c Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Sun, 12 Feb 2017 14:47:56 -0500 Subject: [PATCH 17/19] More test coverage improvement --- COT/helpers/helper.py | 4 ++- COT/helpers/tests/test_helper.py | 55 ++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/COT/helpers/helper.py b/COT/helpers/helper.py index 0a4422d..5d4726d 100644 --- a/COT/helpers/helper.py +++ b/COT/helpers/helper.py @@ -317,7 +317,9 @@ def install(self): """Install the helper program. Raises: - NotImplementedError: if not :attr:`installable` + NotImplementedError: if not :attr:`installable` on this platform + RuntimeError: if potentially :attr:`installable` on this platform + but required helpers (e.g., package managers) are not available. HelperError: if installation is attempted but fails. Subclasses should not override this method but instead should provide diff --git a/COT/helpers/tests/test_helper.py b/COT/helpers/tests/test_helper.py index b4cdaa7..7cc18e8 100644 --- a/COT/helpers/tests/test_helper.py +++ b/COT/helpers/tests/test_helper.py @@ -244,6 +244,12 @@ def setUp(self): self.helper = Helper("generic") super(HelperGenericTest, self).setUp() + def tearDown(self): + """Cleanup function called automatically prior to each test.""" + self.helper._installed = False + Helper._provider_package = {} + super(HelperGenericTest, self).tearDown() + def test_check_call_helpernotfounderror(self): """HelperNotFoundError if executable doesn't exist.""" self.assertRaises(HelperNotFoundError, @@ -344,10 +350,55 @@ def test_install_already_present(self, mock_install): @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.""" + def test_install_not_implemented(self, *_): + """If installable lies, default _install method should fail cleanly.""" + self.helper._installed = False self.assertRaises(NotImplementedError, self.helper.install) + @mock.patch('COT.helpers.Helper.installable', + new_callable=mock.PropertyMock, return_value=True) + @mock.patch('platform.system', return_value='Darwin') + def test_install_missing_package_manager_mac(self, *_): + """RuntimeError if Mac install supported but brew/port are absent.""" + self.helper._installed = False + self.helper._provider_package['brew'] = 'install-me-with-brew' + self.helper._provider_package['port'] = 'install-me-with-port' + self.select_package_manager(None) + with self.assertRaises(RuntimeError) as cm: + self.helper.install() + msg = str(cm.exception) + self.assertRegex(msg, "Unsure how to install generic.") + # Since both helpers are supported, we should see both messages + self.assertRegex(msg, "COT can use Homebrew") + self.assertRegex(msg, "COT can use MacPorts") + + del self.helper._provider_package['brew'] + with self.assertRaises(RuntimeError) as cm: + self.helper.install() + msg = str(cm.exception) + self.assertRegex(msg, "Unsure how to install generic.") + # Now we should only see the supported one + self.assertNotRegex(msg, "COT can use Homebrew") + self.assertRegex(msg, "COT can use MacPorts") + + del self.helper._provider_package['port'] + # Now we should fall back to NotImplementedError + with self.assertRaises(NotImplementedError) as cm: + self.helper.install() + msg = str(cm.exception) + self.assertRegex(msg, "Unsure how to install generic.") + self.assertNotRegex(msg, "COT can use Homebrew") + self.assertNotRegex(msg, "COT can use MacPorts") + + self.helper._provider_package['brew'] = 'install-me-with-brew' + with self.assertRaises(RuntimeError) as cm: + self.helper.install() + msg = str(cm.exception) + self.assertRegex(msg, "Unsure how to install generic.") + # Now we should only see the supported one + self.assertRegex(msg, "COT can use Homebrew") + self.assertNotRegex(msg, "COT can use MacPorts") + @mock.patch('COT.helpers.Helper._install') @mock.patch('COT.helpers.Helper.installable', new_callable=mock.PropertyMock, return_value=True) From 15ea8b7306a46fd70e10ebd1352c174cbe066f62 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Tue, 31 Jan 2017 17:00:06 -0500 Subject: [PATCH 18/19] Add support for Cisco Nexus 9000v platform (#60) --- CHANGELOG.rst | 2 + COT/platforms/__init__.py | 23 +++- COT/platforms/cisco_nexus_9000v.py | 102 ++++++++++++++++++ COT/platforms/tests/test_cisco_nexus_9000v.py | 77 +++++++++++++ COT/tests/test_doctests.py | 7 ++ docs/COT.platforms.cisco_nexus_9000v.rst | 4 + 6 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 COT/platforms/cisco_nexus_9000v.py create mode 100644 COT/platforms/tests/test_cisco_nexus_9000v.py create mode 100644 docs/COT.platforms.cisco_nexus_9000v.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0cb9be7..f55a23f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,7 @@ This project adheres to `Semantic Versioning`_. - Support for Python 3.6 - Support for `brew` package manager (`#55`_). +- Support for Cisco Nexus 9000v (NX-OSv 9000) platform (`#60`_). **Fixed** @@ -610,6 +611,7 @@ Initial public release. .. _#57: https://github.com/glennmatthews/cot/issues/57 .. _#58: https://github.com/glennmatthews/cot/issues/58 .. _#59: https://github.com/glennmatthews/cot/issues/59 +.. _#60: https://github.com/glennmatthews/cot/issues/60 .. _Semantic Versioning: http://semver.org/ .. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/ diff --git a/COT/platforms/__init__.py b/COT/platforms/__init__.py index f2ba159..6aafdf6 100644 --- a/COT/platforms/__init__.py +++ b/COT/platforms/__init__.py @@ -1,5 +1,5 @@ # October 2013, Glenn F. Matthews -# Copyright (c) 2013-2016 the COT project developers. +# Copyright (c) 2013-2017 the COT project developers. # See the COPYRIGHT.txt file at the top-level directory of this distribution # and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. # @@ -40,6 +40,7 @@ COT.platforms.cisco_iosv COT.platforms.cisco_iosxrv COT.platforms.cisco_iosxrv_9000 + COT.platforms.cisco_nexus_9000v COT.platforms.cisco_nxosv """ @@ -50,6 +51,7 @@ from .cisco_iosv import IOSv from .cisco_iosxrv import IOSXRv, IOSXRvRP, IOSXRvLC from .cisco_iosxrv_9000 import IOSXRv9000 +from .cisco_nexus_9000v import Nexus9000v from .cisco_nxosv import NXOSv logger = logging.getLogger(__name__) @@ -58,6 +60,7 @@ PRODUCT_PLATFORM_MAP = { 'com.cisco.csr1000v': CSR1000V, 'com.cisco.iosv': IOSv, + 'com.cisco.n9k': Nexus9000v, 'com.cisco.nx-osv': NXOSv, 'com.cisco.ios-xrv': IOSXRv, 'com.cisco.ios-xrv.rp': IOSXRvRP, @@ -78,6 +81,14 @@ def is_known_product_class(product_class): Returns: bool: Whether product_class is known. + + Examples: + :: + + >>> is_known_product_class("com.cisco.n9k") + True + >>> is_known_product_class("foobar") + False """ return product_class in PRODUCT_PLATFORM_MAP @@ -90,6 +101,16 @@ def platform_from_product_class(product_class): Returns: class: GenericPlatform or a subclass of it + + Examples: + :: + + >>> platform_from_product_class("com.cisco.n9k") + + >>> platform_from_product_class(None) + + >>> platform_from_product_class("frobozz") + """ if product_class is None: return GenericPlatform diff --git a/COT/platforms/cisco_nexus_9000v.py b/COT/platforms/cisco_nexus_9000v.py new file mode 100644 index 0000000..2ca5991 --- /dev/null +++ b/COT/platforms/cisco_nexus_9000v.py @@ -0,0 +1,102 @@ +# January 2017, Glenn F. Matthews +# Copyright (c) 2013-2017 the COT project developers. +# See the COPYRIGHT.txt file at the top-level directory of this distribution +# and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. +# +# This file is part of the Common OVF Tool (COT) project. +# It is subject to the license terms in the LICENSE.txt file found in the +# top-level directory of this distribution and at +# https://github.com/glennmatthews/cot/blob/master/LICENSE.txt. No part +# of COT, including this file, may be copied, modified, propagated, or +# distributed except according to the terms contained in the LICENSE.txt file. + +"""Platform logic for the Cisco Nexus 9000v virtual switch.""" + +import logging + +from COT.platforms.generic import GenericPlatform +from COT.data_validation import ( + ValueTooLowError, validate_int, +) + +logger = logging.getLogger(__name__) + + +class Nexus9000v(GenericPlatform): + """Platform-specific logic for Cisco Nexus 9000v.""" + + PLATFORM_NAME = "Cisco Nexus 9000v" + + CONFIG_TEXT_FILE = 'nxos_config.txt' + LITERAL_CLI_STRING = None + SUPPORTED_NIC_TYPES = ["E1000", "VMXNET3"] + + @classmethod + def guess_nic_name(cls, nic_number): + """The Nexus 9000v has a management NIC and some number of data NICs. + + Args: + nic_number (int): Nth NIC to name. + + Returns: + * mgmt0 + * Ethernet1/1 + * Ethernet1/2 + * ... + """ + if nic_number == 1: + return "mgmt0" + else: + return "Ethernet1/{0}".format(nic_number - 1) + + @classmethod + def validate_cpu_count(cls, cpus): + """The Nexus 9000v requires 1-4 vCPUs. + + Args: + cpus (int): Number of vCPUs + + Raises: + ValueTooLowError: if ``cpus`` is less than 1 + ValueTooHighError: if ``cpus`` is more than 4 + """ + validate_int(cpus, 1, 4, "CPUs") + + @classmethod + def validate_memory_amount(cls, mebibytes): + """The Nexus 9000v requires at least 8 GiB of RAM. + + Args: + mebibytes (int): RAM, in MiB. + + Raises: + ValueTooLowError: if ``mebibytes`` is less than 8192 + """ + if mebibytes < 8192: + raise ValueTooLowError("RAM", str(mebibytes) + " MiB", "8 GiB") + + @classmethod + def validate_nic_count(cls, count): + """The Nexus 9000v requires at least 1 and supports at most 65 NICs. + + Args: + count (int): Number of NICs. + + Raises: + ValueTooLowError: if ``count`` is less than 1 + ValueTooHighError: if ``count`` is more than 65 + """ + validate_int(count, 1, 65, "NICs") + + @classmethod + def validate_serial_count(cls, count): + """The Nexus 9000v requires exactly 1 serial port. + + Args: + count (int): Number of serial ports. + + Raises: + ValueTooLowError: if ``count`` is less than 1 + ValueTooHighError: if ``count`` is more than 1 + """ + validate_int(count, 1, 1, "serial ports") diff --git a/COT/platforms/tests/test_cisco_nexus_9000v.py b/COT/platforms/tests/test_cisco_nexus_9000v.py new file mode 100644 index 0000000..0fc4e26 --- /dev/null +++ b/COT/platforms/tests/test_cisco_nexus_9000v.py @@ -0,0 +1,77 @@ +# test_cisco_nxosv.py - Unit test cases for Cisco Nexus 9000v platform +# +# January 2017, Glenn F. Matthews +# Copyright (c) 2014-2017 the COT project developers. +# See the COPYRIGHT.txt file at the top-level directory of this distribution +# and at https://github.com/glennmatthews/cot/blob/master/COPYRIGHT.txt. +# +# This file is part of the Common OVF Tool (COT) project. +# It is subject to the license terms in the LICENSE.txt file found in the +# top-level directory of this distribution and at +# https://github.com/glennmatthews/cot/blob/master/LICENSE.txt. No part +# of COT, including this file, may be copied, modified, propagated, or +# distributed except according to the terms contained in the LICENSE.txt file. + +"""Unit test cases for Nexus 9000v platform.""" + +import unittest +from COT.platforms.cisco_nexus_9000v import Nexus9000v +from COT.data_validation import ( + ValueUnsupportedError, ValueTooLowError, ValueTooHighError +) + + +class TestNexus9000v(unittest.TestCase): + """Test cases for Cisco Nexus 9000v platform handling.""" + + cls = Nexus9000v + + 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), + "Ethernet1/1") + self.assertEqual(self.cls.guess_nic_name(3), + "Ethernet1/2") + self.assertEqual(self.cls.guess_nic_name(4), + "Ethernet1/3") + + def test_cpu_count(self): + """Test CPU count limits.""" + self.assertRaises(ValueTooLowError, self.cls.validate_cpu_count, 0) + self.cls.validate_cpu_count(1) + self.cls.validate_cpu_count(4) + self.assertRaises(ValueTooHighError, self.cls.validate_cpu_count, 5) + + def test_memory_amount(self): + """Test RAM allocation limits.""" + self.assertRaises(ValueTooLowError, + self.cls.validate_memory_amount, 8191) + self.cls.validate_memory_amount(8192) + self.cls.validate_memory_amount(16384) + # No upper bound known at present + + def test_nic_count(self): + """Test NIC range limits.""" + self.assertRaises(ValueTooLowError, self.cls.validate_nic_count, 0) + self.cls.validate_nic_count(1) + self.cls.validate_nic_count(65) + self.assertRaises(ValueTooHighError, self.cls.validate_nic_count, 66) + + 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.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.assertRaises(ValueTooHighError, self.cls.validate_serial_count, 2) diff --git a/COT/tests/test_doctests.py b/COT/tests/test_doctests.py index b43907a..12bf669 100644 --- a/COT/tests/test_doctests.py +++ b/COT/tests/test_doctests.py @@ -16,9 +16,15 @@ """Test runner for COT doctest tests.""" +import logging + from doctest import DocTestSuite from unittest import TestSuite +from COT.tests.ut import NullHandler + +logging.getLogger('COT').addHandler(NullHandler()) + def load_tests(*_): """Load doctests as unittest test suite. @@ -33,4 +39,5 @@ def load_tests(*_): suite.addTests(DocTestSuite('COT.edit_hardware')) suite.addTests(DocTestSuite('COT.edit_properties')) suite.addTests(DocTestSuite('COT.file_reference')) + suite.addTests(DocTestSuite('COT.platforms')) return suite diff --git a/docs/COT.platforms.cisco_nexus_9000v.rst b/docs/COT.platforms.cisco_nexus_9000v.rst new file mode 100644 index 0000000..5ccc302 --- /dev/null +++ b/docs/COT.platforms.cisco_nexus_9000v.rst @@ -0,0 +1,4 @@ +``COT.platforms.cisco_nexus_9000v`` module +========================================== + +.. automodule:: COT.platforms.cisco_nexus_9000v From 117357a6cfebbbd8ee553bad2fe98cb00aa73930 Mon Sep 17 00:00:00 2001 From: Glenn Matthews Date: Mon, 13 Feb 2017 12:18:05 -0500 Subject: [PATCH 19/19] Bump version --- CHANGELOG.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f55a23f..28890e5 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.9.0`_ - 2017-02-13 +--------------------- **Added** @@ -643,6 +643,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.9.0: https://github.com/glennmatthews/cot/compare/v1.8.2...v1.9.0 .. _1.8.2: https://github.com/glennmatthews/cot/compare/v1.8.1...v1.8.2 .. _1.8.1: https://github.com/glennmatthews/cot/compare/v1.8.0...v1.8.1 .. _1.8.0: https://github.com/glennmatthews/cot/compare/v1.7.4...v1.8.0