Skip to content

Commit

Permalink
Version 1.3.0
Browse files Browse the repository at this point in the history
Features

* Installation of helper programs is now provided by a 'cot
  install-helpers' subcommand rather than a separate script.
* COT now has man pages ('man cot', 'man cot-edit-hardware', etc.)
  The man pages are also installed by 'cot install-helpers'.
* Improved documentation of the CLI on readthedocs.org as well.
* COT no longer supports Python 3.2.

Infrastructure changes

* Refactored COT.helper_tools module into COT.helpers subpackage.
  This package has an API (COT.helpers.api) for the rest of COT to
  access it; the helper-specific logic (qemu-img, fatdisk, etc.) is split
  into individual helper modules that are abstracted away by the API.
* Similarly, logic from COT.tests.helper_tools has been refactored and
  enhanced under COT.helpers.tests.
* Renamed all test code files from "foo.py" to "test_foo.py" to
  facilitate test case discovery.
* cot_unittest is no more - use 'tox' or 'unit2 discover' to run tests.
* As noted above, the installation script check_and_install_helpers.py
  no longer exists - this functionality is now provided by the
  COT.install_helpers module.
* CLI help strings are dynamically rendered to ReST when docs are built,
  providing cleaner output for both readthedocs.org and the manpages.
  • Loading branch information
glennmatthews committed Mar 27, 2015
2 parents 6781860 + 61b17a9 commit 250307f
Show file tree
Hide file tree
Showing 93 changed files with 4,372 additions and 2,673 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
[run]
omit =
COT/tests/*
COT/helpers/tests/*
COT/_version.py
setup.py
versioneer.py
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,12 @@ coverage.xml

# Sphinx documentation
docs/_build/
docs/_autogenerated/
COT/docs/doctrees/
COT/docs/man/

# PyBuilder
target/

# OS X crud
.DS_Store
.DS_Store
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ python:
- "2.7"

install:
- pip install -r requirements.txt
- pip install tox
- pip install coveralls
- sudo python setup.py install_helpers --force

script: tox

Expand Down
3 changes: 1 addition & 2 deletions COT/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@
COT.help
COT.info
COT.inject_config
COT.install_helpers
Helper library modules
----------------------
.. autosummary::
:toctree:
COT.data_validation
COT.helper_tools
COT.platforms
User interface modules
Expand All @@ -56,7 +56,6 @@
COT.ui_shared
COT.cli
"""

from ._version import get_versions
Expand Down
9 changes: 5 additions & 4 deletions COT/add_disk.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,14 @@ def run(self):
diskname=self.diskname,
description=self.description)

def create_subparser(self, parent):
def create_subparser(self, parent, storage):
"""Add subparser for the CLI of this submodule.
:param object parent: Subparser grouping object returned by
:func:`ArgumentParser.add_subparsers`
:meth:`ArgumentParser.add_subparsers`
:returns: ``('add-disk', subparser)``
:param dict storage: Dict of { 'label': subparser } to be updated with
subparser(s) created, if any.
"""
p = parent.add_parser(
'add-disk', add_help=False,
Expand Down Expand Up @@ -240,7 +241,7 @@ def create_subparser(self, parent):
help="""OVF descriptor or OVA file to edit""")
p.set_defaults(instance=self)

return 'add-disk', p
storage['add-disk'] = p


def add_disk_worker(vm,
Expand Down
9 changes: 5 additions & 4 deletions COT/add_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,14 @@ def run(self):

vm.add_file(self.file, self.file_id, file)

def create_subparser(self, parent):
def create_subparser(self, parent, storage):
"""Add subparser for the CLI of this submodule.
:param object parent: Subparser grouping object returned by
:func:`ArgumentParser.add_subparsers`
:meth:`ArgumentParser.add_subparsers`
:returns: ``('add-file', subparser)``
:param dict storage: Dict of { 'label': subparser } to be updated with
subparser(s) created, if any.
"""
p = parent.add_parser(
'add-file',
Expand All @@ -133,4 +134,4 @@ def create_subparser(self, parent):
help="""Package, OVF descriptor or OVA file to edit""")
p.set_defaults(instance=self)

return 'add-file', p
storage['add-file'] = p
123 changes: 70 additions & 53 deletions COT/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class CLI(UI):
terminal_width
"""

def __init__(self):
def __init__(self, terminal_width=None):
"""Create CLI handler instance."""
super(CLI, self).__init__(force=True)
# In python 2.7, we want raw_input, but in python 3 we want input.
Expand All @@ -78,14 +78,29 @@ def __init__(self):
self.getpass = getpass.getpass
self.handler = None
self.master_logger = None
self.wrapper = textwrap.TextWrapper(width=self.terminal_width() - 1)
self._terminal_width = terminal_width
self.wrapper = textwrap.TextWrapper(width=self.terminal_width - 1)

self.create_parser()
self.create_subparsers()

import COT.helpers.helper
COT.helpers.helper.confirm = self.confirm

@property
def terminal_width(self):
"""Get the width of the terminal in columns."""
return get_terminal_size().columns
"""The width of the terminal in columns."""
if self._terminal_width is None:
try:
self._terminal_width = get_terminal_size().columns
except ValueError:
# sometimes seen in unit tests:
# ValueError: underlying buffer has been detached
# Easy enough to work around...
self._terminal_width = 80
if self._terminal_width <= 0:
self._terminal_width = 80
return self._terminal_width

def fill_usage(self, subcommand, usage_list):
"""Pretty-print a list of usage strings for a COT subcommand.
Expand Down Expand Up @@ -123,7 +138,7 @@ def fill_usage(self, subcommand, usage_list):
-\S+\s+\S+ | # Dashed arg followed by metavar
\S+ # Positional arg
""", re.VERBOSE)
width = self.terminal_width()
width = self.terminal_width
for line in usage_list:
usage_groups = re.findall(splitter, line)

Expand Down Expand Up @@ -162,30 +177,29 @@ def fill_examples(self, example_list):
::
>>> fill_examples([
... ('cot deploy foo.ova esxi 192.0.2.100 -u admin -p admin'
... ' -n test_vm',
... "Deploy to vSphere/ESXi server 192.0.2.100 with credentials"
... " admin/admin, creating a VM named 'test_vm' from foo.ova."),
... ('cot deploy foo.ova esxi 192.0.2.100 -u admin -c 1CPU-2.5GB',
... "Deploy to vSphere/ESXi server 192.0.2.100, with username"
... ("Deploy to vSphere/ESXi server 192.0.2.100 with credentials"
... " admin/admin, creating a VM named 'test_vm' from foo.ova.",
... 'cot deploy foo.ova esxi 192.0.2.100 -u admin -p admin'
... ' -n test_vm'),
... ("Deploy to vSphere/ESXi server 192.0.2.100, with username"
... " admin (prompting the user to input a password at runtime),"
... " creating a VM based on profile '1CPU-2.5GB' in foo.ova.")
... " creating a VM based on profile '1CPU-2.5GB' in foo.ova.",
... 'cot deploy foo.ova esxi 192.0.2.100 -u admin -c 1CPU-2.5GB')
... ])
Examples:
cot deploy foo.ova esxi 192.0.2.100 -u admin -p admin \
Deploy to vSphere/ESXi server 192.0.2.100 with credentials
admin/admin, creating a VM named 'test_vm' from foo.ova.
cot deploy foo.ova esxi 192.0.2.100 -u admin -p admin \
-n test_vm
Deploy to vSphere/ESXi server 192.0.2.100 with
credentials admin/admin, creating a VM named 'test_vm'
from foo.ova.
cot deploy foo.ova esxi 192.0.2.100 -u admin \
-c 1CPU-2.5GB
Deploy to vSphere/ESXi server 192.0.2.100, with
username admin (prompting the user to input a password
at runtime), creating a VM based on profile
'1CPU-2.5GB' in foo.ova.
:param list example_list: List of (cli_example, example_description)
Deploy to vSphere/ESXi server 192.0.2.100, with username admin
(prompting the user to input a password at runtime), creating a VM
based on profile '1CPU-2.5GB' in foo.ova.
cot deploy foo.ova esxi 192.0.2.100 -u admin -c 1CPU-2.5GB
:param list example_list: List of (description, CLI example)
tuples.
:return: Examples wrapped appropriately to the :func:`terminal_width`
Expand All @@ -201,23 +215,30 @@ def fill_examples(self, example_list):
-\S+[ =]".*?" | # Dashed arg followed by quoted value
\S+ # Positional arg
""", re.VERBOSE)
width = self.terminal_width()
width = self.terminal_width
self.wrapper.width = width - 1
self.wrapper.initial_indent = ' '
self.wrapper.subsequent_indent = ' '
self.wrapper.initial_indent = ' '
self.wrapper.subsequent_indent = ' '
self.wrapper.break_on_hyphens = False
for (example, desc) in example_list:
for (desc, example) in example_list:
if len(output_lines) > 1:
output_lines.append("")
wrapped_line = " "
for param in re.findall(splitter, example):
if len(wrapped_line) + len(param) >= (width - 2):
wrapped_line += " \\"
output_lines.append(wrapped_line)
wrapped_line = " "
wrapped_line += " " + param
output_lines.append(wrapped_line)
output_lines.extend(self.wrapper.wrap(desc))
output_lines.append("")
example_lines = example.splitlines()
if len(example_lines) > 1:
# Don't wrap multiline examples, just indent
for line in example_lines:
output_lines.append(" "+line)
else:
wrapped_line = " "
for param in re.findall(splitter, example):
if len(wrapped_line) + len(param) >= (width - 4):
wrapped_line += " \\"
output_lines.append(wrapped_line)
wrapped_line = " "
wrapped_line += " " + param
output_lines.append(wrapped_line)
return "\n".join(output_lines)

def formatter(self, verbosity=logging.INFO):
Expand Down Expand Up @@ -295,7 +316,7 @@ def confirm(self, prompt):

# Wrap prompt to screen
prompt_w = []
self.wrapper.width = self.terminal_width() - 1
self.wrapper.width = self.terminal_width - 1
self.wrapper.initial_indent = ''
self.wrapper.subsequent_indent = ''
self.wrapper.break_on_hyphens = False
Expand Down Expand Up @@ -356,8 +377,8 @@ def create_parser(self):
"""
# Argparse checks the environment variable COLUMNS to control
# its line-wrapping
os.environ['COLUMNS'] = str(self.terminal_width())
self.wrapper.width = self.terminal_width() - 1
os.environ['COLUMNS'] = str(self.terminal_width)
self.wrapper.width = self.terminal_width - 1
self.wrapper.initial_indent = ''
self.wrapper.subsequent_indent = ''
parser = argparse.ArgumentParser(
Expand All @@ -373,14 +394,6 @@ def create_parser(self):
"virtual appliances, with a focus on virtualized network "
"appliances such as the Cisco CSR 1000V and Cisco IOS XRv "
"platforms.")),
epilog=("""
Note: some subcommands rely on external software tools, including:
* qemu-img (http://www.qemu.org/)
* mkisofs (http://cdrecord.org/)
* ovftool (https://www.vmware.com/support/developer/ovf/)
* fatdisk (http://github.com/goblinhack/fatdisk)
* vmdktool (http://www.freshports.org/sysutils/vmdktool/)
"""),
formatter_class=argparse.RawDescriptionHelpFormatter)

parser.add_argument('-V', '--version', action='version',
Expand All @@ -401,7 +414,7 @@ def create_parser(self):
action='store_const', const=logging.VERBOSE,
help="Verbose output and logging")
debug_group.add_argument(
'-vv', '-d', '--debug', dest='_verbosity',
'-d', '-vv', '--debug', dest='_verbosity',
action='store_const', const=logging.DEBUG,
help="Debug (most verbose) output and logging")

Expand Down Expand Up @@ -431,19 +444,23 @@ def create_subparsers(self):
from COT.help import COTHelp
from COT.info import COTInfo
from COT.inject_config import COTInjectConfig
from COT.install_helpers import COTInstallHelpers
for klass in [
COTAddDisk,
COTAddFile,
COTDeployESXi,
COTEditHardware,
COTEditProduct,
COTEditProperties,
COTHelp,
COTInfo,
COTInjectConfig,
COTHelp, # last so it can be aware of all of the above
COTInstallHelpers,
]:
name, subparser = klass(self).create_subparser(self.subparsers)
self.subparser_lookup[name] = subparser
instance = klass(self)
# the subparser stores a reference to the instance (args.instance)
# so we don't need to persist it here...
instance.create_subparser(self.subparsers, self.subparser_lookup)

def parse_args(self, argv):
"""Parse the given CLI arguments into a namespace object.
Expand All @@ -456,7 +473,7 @@ def parse_args(self, argv):

# If being run non-interactively, treat as if --force is set, in order
# to avoid hanging while trying to read input that will never come.
if not sys.stdin.isatty():
if not (sys.stdin.isatty() and sys.stdout.isatty()):
args._force = True

return args
Expand Down

0 comments on commit 250307f

Please sign in to comment.