diff options
author | Michal Minar <miminar@redhat.com> | 2014-04-22 15:19:56 +0200 |
---|---|---|
committer | Michal Minar <miminar@redhat.com> | 2014-04-24 09:58:44 +0200 |
commit | 2a78da4f98a583a382c553f2e1fc68299210e7d5 (patch) | |
tree | 53029ae0a8474a9ddc464419da7337d0d88dea13 /lmi/scripts/common/command | |
parent | 9138628a62461fd8914d5b82ec6a4ace964a4b2b (diff) | |
download | openlmi-scripts-2a78da4f98a583a382c553f2e1fc68299210e7d5.tar.gz openlmi-scripts-2a78da4f98a583a382c553f2e1fc68299210e7d5.tar.xz openlmi-scripts-2a78da4f98a583a382c553f2e1fc68299210e7d5.zip |
moved lmi meta-command to openlmi-tools repository
Diffstat (limited to 'lmi/scripts/common/command')
-rw-r--r-- | lmi/scripts/common/command/__init__.py | 50 | ||||
-rw-r--r-- | lmi/scripts/common/command/base.py | 341 | ||||
-rw-r--r-- | lmi/scripts/common/command/checkresult.py | 140 | ||||
-rw-r--r-- | lmi/scripts/common/command/endpoint.py | 367 | ||||
-rw-r--r-- | lmi/scripts/common/command/helper.py | 137 | ||||
-rw-r--r-- | lmi/scripts/common/command/lister.py | 176 | ||||
-rw-r--r-- | lmi/scripts/common/command/meta.py | 839 | ||||
-rw-r--r-- | lmi/scripts/common/command/multiplexer.py | 155 | ||||
-rw-r--r-- | lmi/scripts/common/command/select.py | 195 | ||||
-rw-r--r-- | lmi/scripts/common/command/session.py | 206 | ||||
-rw-r--r-- | lmi/scripts/common/command/show.py | 112 | ||||
-rw-r--r-- | lmi/scripts/common/command/util.py | 109 |
12 files changed, 0 insertions, 2827 deletions
diff --git a/lmi/scripts/common/command/__init__.py b/lmi/scripts/common/command/__init__.py deleted file mode 100644 index a7c2df4..0000000 --- a/lmi/scripts/common/command/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of the FreeBSD Project. -# -# Authors: Michal Minar <miminar@redhat.com> -# -""" -This subpackage defines base classes and utility functions for declaring -commands. These serve as wrappers for functions in libraries specific to -particular provider. - -Tree of these commands build a command line interface for this library. -""" - -from lmi.scripts.common.command.base import LmiBaseCommand -from lmi.scripts.common.command.checkresult import LmiCheckResult -from lmi.scripts.common.command.endpoint import LmiEndPointCommand -from lmi.scripts.common.command.lister import LmiInstanceLister -from lmi.scripts.common.command.lister import LmiLister -from lmi.scripts.common.command.multiplexer import LmiCommandMultiplexer -from lmi.scripts.common.command.session import LmiSessionCommand -from lmi.scripts.common.command.select import LmiSelectCommand -from lmi.scripts.common.command.show import LmiShowInstance - -from lmi.scripts.common.command.helper import make_list_command -from lmi.scripts.common.command.helper import register_subcommands -from lmi.scripts.common.command.helper import select_command diff --git a/lmi/scripts/common/command/base.py b/lmi/scripts/common/command/base.py deleted file mode 100644 index cebf422..0000000 --- a/lmi/scripts/common/command/base.py +++ /dev/null @@ -1,341 +0,0 @@ -# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of the FreeBSD Project. -# -# Authors: Michal Minar <miminar@redhat.com> -# -""" -Module defining base command class for all possible commands of ``lmi`` -meta-command. -""" - -import abc -import re - -# regular expression matching leading whitespaces not until the first line -# containing non-white-spaces -RE_LSPACES = re.compile(r'\A(\s*$.)*', re.DOTALL | re.MULTILINE) - -#: Default formatting options overriden by options passed onc ommand-line and -#: set in configuration file. -DEFAULT_FORMATTER_OPTIONS = { - 'no_headings' : False, - 'padding' : 0, - 'human_friendly' : False, -} - -class LmiBaseCommand(object): - """ - Abstract base class for all commands handling command line arguments. - Instances of this class are organized in a tree with root element being the - ``lmi`` meta-command (if not running in interactive mode). Each such - instance can have more child commands if its - :py:meth:`LmiBaseCommand.is_multiplexer` method return ``True``. Each has - one parent command except for the top level one, whose :py:attr:`parent` - property returns ``None``. - - Set of commands is organized in a tree, where each command (except for the - root) has its own parent. :py:meth:`is_end_point` method distinguishes - leaves from nodes. The path from root command to the leaf is a sequence of - commands passed to command line. - - There is also a special command called selector. Its :py:meth:`is_selector` - method returns ``True``. It selects proper command that shall be passed all - the arguments based on expression with profile requirements. It shares its - name and parent with selected child. - - If the :py:meth:`LmiBaseCommand.has_own_usage` returns ``True``, the parent - command won't process the whole command line and the remainder will be - passed as a second argument to the :py:meth:`LmiBaseCommand.run` method. - - :param app: Main application object. - :param string cmd_name: Name of command. - :param parent: Parent command. - :type parent: :py:class:`LmiBaseCommand` - """ - - __metaclass__ = abc.ABCMeta - - @classmethod - def get_description(cls): - """ - Return description for this command. This is usually a first line - of documentation string of a class. - - :rtype: string - """ - if cls.__doc__ is None: - return "" - return cls.__doc__.strip().split("\n", 1)[0] - - @classmethod - def is_end_point(cls): - """ - :returns: ``True``, if this command parses the rest of command line and - can not have any child subcommands. - :rtype: boolean - """ - return True - - @classmethod - def is_multiplexer(cls): - """ - Is this command a multiplexer? Note that only one of - :py:meth:`is_end_point`, :py:meth:`is_selector` and this method can - evaluate to``True``. - - :returns: ``True`` if this command is not an end-point command and it's - a multiplexer. It contains one or more subcommands. It consumes the - first argument from command-line arguments and passes the rest to - one of its subcommands. - :rtype: boolean - """ - return not cls.is_end_point() - - @classmethod - def is_selector(cls): - """ - Is this command a selector? - - :returns: ``True`` if this command is a subclass of - :py:class:`lmi.scripts.common.command.select.LmiSelectCommand`. - :rtype: boolean - """ - return not cls.is_end_point() and not cls.is_multiplexer() - - @classmethod - def has_own_usage(cls): - """ - :returns: ``True``, if this command has its own usage string, which is - returned by :py:meth:`LmiBaseCommand.get_description`. Otherwise - the parent command must be queried. - :rtype: boolean - """ - return False - - @classmethod - def child_commands(cls): - """ - Abstract class method returning dictionary of child commands with - structure: :: - - { "command-name" : cmd_factory, ... } - - Dictionary contains just a direct children (commands, which - may immediately follow this particular command on command line). - """ - raise NotImplementedError("child_commands() method must be overriden" - " in a subclass") - - def __init__(self, app, cmd_name, parent=None): - if not isinstance(cmd_name, basestring): - raise TypeError('cmd_name must be a string') - if parent is not None and not isinstance(parent, LmiBaseCommand): - raise TypeError('parent must be an LmiBaseCommand instance') - self._app = app - self._cmd_name = cmd_name.strip() - self._parent = parent - - @property - def app(self): - """ Return application object. """ - return self._app - - @property - def parent(self): - """ Return parent command. """ - return self._parent - - @property - def cmd_name(self): - """ Name of this subcommand as a single word. """ - return self._cmd_name - - @property - def cmd_name_parts(self): - """ - Convenience property calling :py:meth:`get_cmd_name_parts` to obtain - command path as a list of all preceding command names. - - :rtype: list - """ - return self.get_cmd_name_parts() - - @property - def format_options(self): - """ - Compose formatting options. Parent commands are queried for defaults. If - command has no parent, default options will be taken from - :py:attr:`DEFAULT_FORMATTER_OPTIONS` which are overriden by config - settings. - - :returns: Arguments passed to formatter factory when formatter is - for current command is constructed. - :rtype: dictionary - """ - if self.parent is None: - options = DEFAULT_FORMATTER_OPTIONS.copy() - options['no_headings'] = self.app.config.no_headings - options['human_friendly'] = self.app.config.human_friendly - else: - options = self.parent.format_options - return options - - @property - def session(self): - """ - :returns: Session object. Session for command and all of its children - may be overriden with a call to :py:meth:`set_session_proxy`. - :rtype: :py:class:`lmi.scripts.common.session.Session` - """ - proxy = getattr(self, '_session_proxy', None) - if proxy: - return proxy - if self.parent is not None: - return self.parent.session - return self.app.session - - def get_cmd_name_parts(self, all_parts=False, demand_own_usage=True, - for_docopt=False): - """ - Get name of this command as a list composed of names of all preceding - commands since the top level one. When in interactive mode, only - commands following the active one will be present. - - :param boolean full: Take no heed to the active command or interactive - mode. Return all command names since top level node inclusive. This - is overriden with *for_docopt* flag. - :param boolean demand_own_usage: Wether to continue the upward - traversal through command hieararchy past the active command until - the command with its own usage is found. This is the default behaviour. - :param boolean for_docopt: Docopt parser needs to be given arguments list - without the first item compared to command names in usage string - it receives. Thus this option causes skipping the first item that would - be otherwise included. - :returns: Command path. Returned list will always contain at least the - name of this command. - :rtype: list - """ - parts = [self.cmd_name] - cmd = self - own_usage = cmd.has_own_usage() - while ( cmd.parent is not None - and (all_parts or self.app.active_command not in (cmd, cmd.parent)) - or (demand_own_usage and not own_usage)): - cmd = cmd.parent - parts.append(cmd.cmd_name) - own_usage = own_usage or cmd.has_own_usage() - if for_docopt and parts: - parts.pop() - return list(reversed(parts)) - - def get_usage(self, proper=False): - """ - Get command usage. Return value of this function is used by docopt - parser as usage string. Command tree is traversed upwards until command - with defined usage string is found. End point commands (leaves) require - manually written usage, so the first command in the sequence of parents - with own usage string is obtained and its usage returned. For nodes - missing own usage string this can be generated based on its - subcommands. - - :param boolean proper: Says, whether the usage string written - manually is required or not. It applies only to node (not a leaf) - commands without its own usage string. - """ - if self.is_end_point() or self.has_own_usage() or proper: - # get proper (manually written) usage, also referred as *own* - cmd = self - while not cmd.has_own_usage() and cmd.parent is not None: - cmd = cmd.parent - if cmd.__doc__ is None: - docstr = "Usage: %s\n" % " ".join(self.cmd_name_parts) - else: - docstr = ( ( cmd.__doc__.rstrip() - % {'cmd' : " ".join(cmd.cmd_name_parts)} - )) - - match = RE_LSPACES.match(docstr) - if match: # strip leading newlines - docstr = docstr[match.end(0):] - - match = re.match(r'^ +', docstr) - if match: # unindent help message - re_lspaces = re.compile(r'^ {%s}' % match.end(0)) - docstr = "\n".join(re_lspaces.sub('', l) - for l in docstr.splitlines()) - docstr += "\n" - - else: - # generate usage string from what is known, applies to nodes - # without own usage - hlp = [] - if self.get_description(): - hlp.append(self.get_description()) - hlp.append("") - hlp.append("Usage:") - hlp.append(" %s <command> [<args> ...]" - % " ".join(self.cmd_name_parts)) - hlp.append("") - hlp.append("Commands:") - cmd_max_len = max(len(c) for c in self.child_commands()) - for name, cmd in sorted(self.child_commands().items()): - hlp.append((" %%-%ds %%s" % cmd_max_len) - % (name, cmd.get_description())) - docstr = "\n".join(hlp) + "\n" - - return docstr - - @abc.abstractmethod - def run(self, args): - """ - Handle the command line arguments. If this is not an end point - command, it will pass the unhandled arguments to one of it's child - commands. So the arguments are processed recursively by the instances - of this class. - - :param list args: Arguments passed to the command line that were - not yet parsed. It's the contents of ``sys.argv`` (if in - non-interactive mode) from the current command on. - :returns: Exit code of application. This maybe also be a boolean value - or ``None``. ``None`` and ``True`` are treated as a success causing - exit code to be 0. - :rtype: integer - """ - raise NotImplementedError("run method must be overriden in subclass") - - def set_session_proxy(self, session): - """ - Allows to override session object. This is useful for especially for - conditional commands (subclasses of - :py:class:`~lmi.scripts.common.command.select.LmiSelectCommand`) that devide - connections to groups satisfying particular expression. These groups - are turned into session proxies containing just a subset of connections - in global session object. - - :param session: Session object. - """ - self._session_proxy = session diff --git a/lmi/scripts/common/command/checkresult.py b/lmi/scripts/common/command/checkresult.py deleted file mode 100644 index 27e9549..0000000 --- a/lmi/scripts/common/command/checkresult.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of the FreeBSD Project. -# -# Authors: Michal Minar <miminar@redhat.com> -# -""" -This module defines LmiCheckResult command class and related utilities. -""" - -import abc - -from lmi.scripts.common import Configuration -from lmi.scripts.common import get_logger -from lmi.scripts.common import formatter -from lmi.scripts.common import errors -from lmi.scripts.common.command import meta -from lmi.scripts.common.command.session import LmiSessionCommand - -LOG = get_logger(__name__) - -class LmiResultFailed(errors.LmiFailed): - """ - Exception raised when associated function returns unexpected result. This - is evaluated by :py:meth:`LmiCheckResult.check_result` method. - """ - pass - -def _make_result_failed(expected, result): - """ - Instantiate :py:exc:`LmiResultFailed` exception with descriptive message - composed of what was expected and what was returned instead. - - :rtype: :py:class:`LmiResultFailed` - """ - return LmiResultFailed('failed (%s != %s)' % (repr(expected), repr(result))) - -class LmiCheckResult(LmiSessionCommand): - """ - Run an associated action and check its result. It implicitly makes no - output if the invocation is successful and expected result matches. - - List of additional recognized properties: - - ``EXPECT`` : - Value, that is expected to be returned by invoked associated - function. This can also be a callable taking two arguments: - - 1. options - Dictionary with parsed command line options - returned by ``docopt``. - 2. result - Return value of associated function. - - Using metaclass: :py:class:`~.meta.CheckResultMetaClass`. - """ - __metaclass__ = meta.CheckResultMetaClass - - def __init__(self, *args, **kwargs): - LmiSessionCommand.__init__(self, *args, **kwargs) - - def formatter_factory(self): - return formatter.TableFormatter - - @abc.abstractmethod - def check_result(self, options, result): - """ - Check the returned value of associated function. - - :param dictionary options: Dictionary as returned by ``docopt`` parser - after running - :py:meth:`~.endpoint.LmiEndPointCommand.transform_options`. - :param result: Any return value that will be compared against what is - expected. - :returns: Whether the result is expected value or not. If ``tuple`` - is returned, it contains ``(passed_flag, error_description)``. - :rtype: boolean or tuple. - """ - raise NotImplementedError("check_result must be overriden in subclass") - - def take_action(self, connection, args, kwargs): - """ - Invoke associated method and check its return value for single host. - - :param list args: List of arguments to pass to the associated - function. - :param dictionary kwargs: Keyword arguments to pass to the associated - function. - :returns: Exit code (0 on success). - :rtype: integer - """ - try: - result = self.execute_on_connection(connection, *args, **kwargs) - passed = self.check_result(self._options, result) - if isinstance(passed, tuple): - if len(passed) != 2: - raise errors.LmiUnexpectedResult('check_result() must' - ' return either boolean or (passed_flag,' - ' error_description), not "%s"' % repr(passed)) - if not passed[0]: - raise LmiResultFailed(passed[1]) - elif not passed and hasattr(self.check_result, 'expected'): - err = _make_result_failed(self.check_result.expected, result) - raise err - except LmiResultFailed: - raise - except Exception as err: - LOG().debug("Failed to execute wrapped function.", exc_info=err) - raise - return 0 - - def process_host_result(self, hostname, success, result): - pass - - def process_session_results(self, session, results): - if len(self.session) > 1: - LOG().debug('Successful runs: %d\n', - len([r for r in results.values() if r[0]])) - LmiSessionCommand.process_session_results(self, session, results) diff --git a/lmi/scripts/common/command/endpoint.py b/lmi/scripts/common/command/endpoint.py deleted file mode 100644 index c744e34..0000000 --- a/lmi/scripts/common/command/endpoint.py +++ /dev/null @@ -1,367 +0,0 @@ -# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of the FreeBSD Project. -# -# Authors: Michal Minar <miminar@redhat.com> -# -""" -Defines base command class for all endpoint commands. Those having no children. -""" -import abc -import inspect -import re -from docopt import docopt - -from lmi.scripts.common import errors -from lmi.scripts.common import formatter -from lmi.scripts.common import get_logger -from lmi.scripts.common.formatter import command as fcmd -from lmi.scripts.common.command import base -from lmi.scripts.common.command import meta -from lmi.scripts.common.command import util - -LOG = get_logger(__name__) - -def opt_name_sanitize(opt_name): - """ - Make a function parameter name out of option name. This replaces any - character not suitable for python identificator with ``'_'`` and - make the whole string lowercase. - - :param string opt_name: Option name. - :returns: Modified option name. - :rtype: string - """ - return re.sub(r'[^a-zA-Z0-9]+', '_', opt_name).lower() - -def options_dict2kwargs(options): - """ - Convert option name from resulting ``docopt`` dictionary to a valid python - identificator token used as function argument name. - - :param dictionary options: Dictionary returned by docopt call. - :returns: New dictionary with keys passable to function as argument - names. - :rtype: dictionary - """ - # (new_name, value) for each pair in options dictionary - kwargs = {} - # (new_name, name) - orig_names = {} - for name, value in options.items(): - for (reg, func) in ( - (util.RE_OPT_BRACKET_ARGUMENT, lambda m: m.group('name')), - (util.RE_OPT_UPPER_ARGUMENT, lambda m: m.group('name')), - (util.RE_OPT_SHORT_OPTION, lambda m: m.group(0)), - (util.RE_OPT_LONG_OPTION, lambda m: m.group(0)), - (util.RE_COMMAND_NAME, lambda m: m.group(0))): - match = reg.match(name) - if match: - new_name = func(match) - break - else: - raise errors.LmiError( - 'Failed to convert argument "%s" to function option.' % - name) - if new_name == '--': - continue # ignore double dash - new_name = opt_name_sanitize(new_name) - if new_name in kwargs: - raise errors.LmiError('Option clash for "%s" and "%s", which both' - ' translate to "%s".' % (name, orig_names[new_name], new_name)) - kwargs[new_name] = value - orig_names[new_name] = name - return kwargs - -class LmiEndPointCommand(base.LmiBaseCommand): - """ - Base class for any leaf command. - - List of additional recognized properties: - - ``CALLABLE`` : ``tuple`` - Associated function. Will be wrapped in - :py:meth:`LmiEndPointCommand.execute` method and will be accessible - directly as a ``cmd.execute.dest`` property. It may be specified - either as a string in form ``"<module_name>:<callable>"`` or as a - reference to callable itself. - ``ARG_ARRAY_SUFFIX`` : ``str`` - String appended to every option parsed by ``docopt`` having list as - an associated value. It defaults to empty string. This modification - is applied before calling - :py:meth:`LmiEndPointCommand.verify_options` and - :py:meth:`LmiEndPointCommand.transform_options`. - ``FORMATTER`` : callable - Default formatter factory for instances of given command. This - factory accepts an output stream as the only parameter and returns - an instance of :py:class:`~lmi.scripts.common.formatter.Formatter`. - - Using metaclass: - :py:class:`.meta.EndPointCommandMetaClass`. - """ - __metaclass__ = meta.EndPointCommandMetaClass - - def __init__(self, *args, **kwargs): - super(LmiEndPointCommand, self).__init__(*args, **kwargs) - self._formatter = None - # saved options dictionary after call to transform_options() - self._options = None - - @abc.abstractmethod - def execute(self, *args, **kwargs): - """ - Subclasses must override this method to pass given arguments to - command library function. This function shall be specified in - ``CALLABLE`` property. - """ - raise NotImplementedError("execute method must be overriden" - " in subclass") - - def formatter_factory(self): - """ - Subclasses shall override this method to provide default formatter - factory for printing output. - - :returns: Subclass of basic formatter. - """ - return formatter.Formatter - - @classmethod - def dest_pos_args_count(cls): - """ - Number of positional arguments the associated function takes from - command. These arguments are created by the command alone -- they do - not belong to options in usage string. Function can take additional - positional arguments that need to be covered by usage string. - - :rtype: integer - """ - dest = getattr(cls.execute, "dest", cls.execute) - abstract = dest == cls.execute and util.is_abstract_method( - cls, 'execute', True) - # if the destination function is not yet defined (abstract is True) - # let's assume it's not a method => 0 positional arguments needed - return 1 if not abstract and inspect.ismethod(dest) else 0 - - def run_with_args(self, args, kwargs): - """ - Process end-point arguments and exit. - - :param list args: Positional arguments to pass to associated - function in command library. - :param dictionary kwargs: Keyword arguments as a dictionary. - :returns: Exit code of application. - :rtype: integer - """ - return self.execute(*args, **kwargs) - - @property - def formatter(self): - """ - Return instance of default formatter. - - :rtype: :py:class:`~lmi.scripts.common.formatter.Formatter` - """ - if self._formatter is None: - opts = self.format_options - factory = self.formatter_factory() - argspec = inspect.getargspec( - factory.__init__ if type(factory) is type else factory) - if not argspec.keywords: - kwargs = {k: v for k, v in opts.items() if k in argspec.args} - self._formatter = factory(self.app.stdout, **kwargs) - return self._formatter - - def _make_end_point_args(self, options): - """ - Creates a pair of positional and keyword arguments for a call to - associated function from command line options. All keyword - options not expected by target function are removed. - - :param dictionary options: Output of ``docopt`` parser. - :returns: Positional and keyword arguments as a pair. - :rtype: tuple - """ - # if execute method does not have a *dest* attribute, then it's - # itself a destination - dest = getattr(self.execute, "dest", self.execute) - argspec = inspect.getargspec(dest) - kwargs = options_dict2kwargs(options) - # number of positional arguments not covered by usage string - pos_args_count = self.dest_pos_args_count() - to_remove = [] - # if associated function takes keyword arguments in a single - # dictionary (kwargs), we can pass all options - if argspec.keywords is None: - # otherwise we need to remove any unhandled - for opt_name in kwargs: - if opt_name not in argspec.args[pos_args_count:]: - if opt_name not in self.cmd_name_parts: - LOG().debug('Option "%s" not handled in function "%s",' - ' ignoring.', opt_name, self.cmd_name) - to_remove.append(opt_name) - for opt_name in to_remove: - # remove options unhandled by function - del kwargs[opt_name] - args = [] - for arg_name in argspec.args[pos_args_count:]: - if arg_name not in kwargs: - raise errors.LmiCommandError( - self.__module__, self.__class__.__name__, - 'registered command "%s" expects option "%s", which' - ' is not covered in usage string' - % (self.cmd_name, arg_name)) - args.append(kwargs.pop(arg_name)) - return args, kwargs - - def _preprocess_options(self, options): - """ - This method may be overriden by - :py:class:`~.meta.EndPointCommandMetaClass` - as a result of processing ``ARG_ARRAY_SUFFIX`` and other properties - modifying names of parsed options. - - This should not be overriden in command class's body. - - :param dictionary options: The result of ``docopt`` parser invocation - which can be modified by this method. - """ - pass - - def _parse_args(self, args): - """ - Run ``docopt`` command line parser on given list of arguments. - Removes all unrelated commands from created dictionary of options. - - :param list args: List of command line arguments just after the - current command. - :returns: Dictionary with parsed options. Please refer to - docopt_ documentation for more informations. - :rtype: dictionary - - .. _docopt: http://docopt.org/ - """ - full_args = self.get_cmd_name_parts(for_docopt=True) + args - options = docopt(self.get_usage(), full_args, help=False) - self._preprocess_options(options) - - # remove all command names from options - cmd = self.parent - while cmd is not None and not cmd.has_own_usage(): - cmd = cmd.parent - if cmd is not None: - for scn in cmd.child_commands(): - try: - del options[scn] - except KeyError: - LOG().warn('Usage string of "%s.%s" command does not' - ' contain registered command "%s" command.', - cmd.__module__, cmd.__class__.__name__, scn) - # remove also the root command name from options - if cmd is not None and cmd.cmd_name in options: - del options[cmd.cmd_name] - return options - - def verify_options(self, options): - """ - This method can be overriden in subclasses to check, whether the - options given on command line are valid. If any flaw is discovered, an - :py:exc:`~lmi.scripts.common.errors.LmiInvalidOptions` exception shall - be raised. Any returned value is ignored. - - .. note:: - This is run before :py:meth:`transform_options()` method. - - :param dictionary options: Dictionary as returned by ``docopt`` parser. - """ - pass - - def transform_options(self, options): - """ - This method can be overriden in subclasses if options shall be somehow - modified before passing them associated function. - - .. note:: - Run after :py:meth:`verify_options()` method. - - :param dictionary options: Dictionary as returned by ``docopt`` parser. - """ - pass - - def produce_output(self, data): - """ - This method can be use to render and print results with default - formatter. - - :param data: Is an object expected by the - :py:meth:`~lmi.scripts.common.formatter.Formatter.produce_output` - method of formatter. - """ - self.formatter.produce_output(data) - - def run(self, args): - """ - Create options dictionary from input arguments, verify them, - transform them, make positional and keyword arguments out of them and - pass them to ``process_session()``. - - :param list args: List of command arguments. - :returns: Exit code of application. - :rtype: integer - """ - options = self._parse_args(args) - self.verify_options(options) - self.transform_options(options) - self._options = options.copy() - args, kwargs = self._make_end_point_args(options) - return self.run_with_args(args, kwargs) - - def _print_errors(self, error_list, new_line=True): - """ - Print list of errors. - - :param list errors: Errors to print. Each error is a ``tuple``: :: - - (hostname, [error, error]) - - Where ``error`` may be a test description or an instance of - exception. - :param new_line: Whether to print the new line before new error - table is printed. - """ - fmt = formatter.ErrorFormatter(self.app.stderr) - if new_line: - fmt.out.write('\n') - if error_list: - new_table_cmd = fcmd.NewTableCommand("There %s %d error%s" % - ( 'were' if len(error_list) > 1 else 'was' - , len(error_list) - , 's' if len(error_list) > 1 else '')) - fmt.produce_output((new_table_cmd, )) - for hostname, host_errors in error_list: - fmt.produce_output((fcmd.NewHostCommand(hostname), )) - fmt.produce_output(host_errors) - diff --git a/lmi/scripts/common/command/helper.py b/lmi/scripts/common/command/helper.py deleted file mode 100644 index 578fa8b..0000000 --- a/lmi/scripts/common/command/helper.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of the FreeBSD Project. -# -# Authors: Michal Minar <miminar@redhat.com> -# -""" -Module with convenient function for defining user commands. -""" - -from lmi.scripts.common.command import LmiLister -from lmi.scripts.common.command import LmiCommandMultiplexer -from lmi.scripts.common.command import LmiSelectCommand -from lmi.scripts.common.command import util - -def make_list_command(func, - name=None, - columns=None, - verify_func=None, - transform_func=None): - """ - Create a command subclassed from :py:class:`~.lister.LmiLister`. Please - refer to this class for detailed usage. - - :param func: Contents of ``CALLABLE`` property. - :type func: string or callable - :param string name: Optional name of resulting class. If not given, - it will be made from the name of associated function. - :param tuple columns: Contents of ``COLUMNS`` property. - :param callable verify_func: Callable overriding - py:meth:`~.endpoint.LmiEndPointCommand.verify_options` method. - :param callable transform_func: Callable overriding - :py:meth:`~.endpoint.LmiEndPointCommand.transform_options` method. - :returns: Subclass of :py:class:`~.lister.LmiLister`. - :rtype: type - """ - if name is None: - if isinstance(func, basestring): - name = func.split('.')[-1] - else: - name = func.__name__ - if not name.startswith('_'): - name = '_' + name.capitalize() - props = { 'COLUMNS' : columns - , 'CALLABLE' : func - , '__module__' : util.get_module_name() } - if verify_func: - props['verify_options'] = verify_func - if transform_func: - props['transform_options'] = transform_func - return LmiLister.__metaclass__(name, (LmiLister, ), props) - -def register_subcommands(command_name, usage, command_map, - fallback_command=None): - """ - Create a multiplexer command (a node in a tree of commands). - - :param string command_name: Name of created command. The same as will - be given on a command line. - :param string usage: Usage string parseable by ``docopt``. - :param dictionary command_map: Dictionary of subcommands. Associates - command names to their factories. It's assigned to ``COMMANDS`` - property. - :param fallback_command: Command factory used when no command is given - on command line. - :type fallback_command: :py:class:`~.endpoint.LmiEndPointCommand` - :returns: Subclass of :py:class:`~.multiplexer.LmiCommandMultiplexer`. - :rtype: type - """ - props = { 'COMMANDS' : command_map - , 'OWN_USAGE' : True - , '__doc__' : usage - , '__module__' : util.get_module_name() - , 'FALLBACK_COMMAND' : fallback_command } - return LmiCommandMultiplexer.__metaclass__(command_name, - (LmiCommandMultiplexer, ), props) - -def select_command(command_name, *args, **kwargs): - """ - Create command selector that loads command whose requirements are met. - - Example of invocation: :: - - Hardware = select_command('Hardware', - ("Openlmi-Hardware >= 0.4.2", "lmi.scripts.hardware.current.Cmd"), - ("Openlmi-Hardware < 0.4.2" , "lmi.scripts.hardware.pre042.Cmd"), - default=HwMissing - ) - - Above example checks remote broker for OpenLMI-Hardware provider. If it is - installed and its version is equal or higher than 0.4.2, command from - ``current`` module will be used. For older registered versions command - contained in ``pre042`` module will be loaded. If hardware provider is not - available, HwMissing command will be loaded instead. - - .. seealso:: - Check out the grammer describing language used in these conditions at - :py:mod:`lmi.scripts.common.versioncheck.parser`. - - :param args: List of pairs ``(condition, command)`` that are inspected in - given order until single condition is satisfied. Associated command is - then loaded. Command is either a reference to command class or path to - it given as string. In latter case last dot divides module's import - path and command name. - :param default: This command will be loaded when no condition from *args* - is satisfied. - """ - props = { 'SELECT' : args - , 'DEFAULT' : kwargs.get('default', None) - , '__module__' : util.get_module_name() - } - return LmiSelectCommand.__metaclass__(command_name, - (LmiSelectCommand, ), props) - diff --git a/lmi/scripts/common/command/lister.py b/lmi/scripts/common/command/lister.py deleted file mode 100644 index 6b36168..0000000 --- a/lmi/scripts/common/command/lister.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of the FreeBSD Project. -# -# Authors: Michal Minar <miminar@redhat.com> -# -""" -Defines command classes producing tablelike output. -""" - -import abc -from itertools import chain - -from lmi.scripts.common import errors -from lmi.scripts.common import formatter -from lmi.scripts.common import get_logger -from lmi.scripts.common.command import meta -from lmi.scripts.common.command.session import LmiSessionCommand -from lmi.scripts.common.configuration import Configuration -from lmi.scripts.common.formatter import command as fcmd - -LOG = get_logger(__name__) - -class LmiBaseListerCommand(LmiSessionCommand): - """ - Base class for all lister commands. - """ - - @classmethod - def get_columns(cls): - """ - :returns: Column names for resulting table. ``COLUMNS`` property - will be converted to this class method. If ``None``, the associated - function shall return column names as the first tuple of returned - list. If empty tuple or list, no header shall be printed and associated - function returns just data rows. - :rtype: list or tuple or None - """ - return None - - def formatter_factory(self): - if self.app.config.lister_format == Configuration.LISTER_FORMAT_CSV: - return formatter.CsvFormatter - else: - return formatter.TableFormatter - -class LmiLister(LmiBaseListerCommand): - """ - End point command outputting a table for each host. Associated function - shall return a list of rows. Each row is represented as a tuple holding - column values. - - List of additional recognized properties: - - ``COLUMNS`` : ``tuple`` - Column names. It's a tuple with name for each column. Each row - shall then contain the same number of items as this tuple. If - omitted, associated function is expected to provide them in the - first row of returned list. It's translated to ``get_columns()`` - class method. - - Using metaclass: :py:class:`~.meta.ListerMetaClass`. - """ - __metaclass__ = meta.ListerMetaClass - - def take_action(self, connection, args, kwargs): - """ - Collects results of single host. - - :param connection: Connection to a single host. - :type connection: :py:class:`lmi.shell.LMIConnection` - :param list args: Positional arguments for associated function. - :param dictionary kwargs: Keyword arguments for associated function. - :returns: Column names and item list as a pair. - :rtype: tuple - """ - res = self.execute_on_connection(connection, *args, **kwargs) - columns = self.get_columns() - if isinstance(columns, (tuple, list)) and columns: - command = fcmd.NewTableHeaderCommand(columns) - res = chain((command, ), res) - elif columns is None: - resi = iter(res) - command = fcmd.NewTableHeaderCommand(resi.next()) - res = chain((command, ), resi) - return res - -class LmiInstanceLister(LmiBaseListerCommand): - """ - End point command outputting a table of instances for each host. - Associated function shall return a list of instances. They may be - prepended with column names depending on value of ``DYNAMIC_PROPERTIES``. - Each instance will occupy single row of table with property values being a - content of cells. - - List of additional recognized properties is the same as for - :py:class:`~.show.LmiShowInstance`. There is just one difference. Either - ``DYNAMIC_PROPERTIES`` must be ``True`` or ``PROPERTIES`` must be filled. - - Using metaclass: :py:class:`~.meta.InstanceListerMetaClass`. - """ - __metaclass__ = meta.InstanceListerMetaClass - - @abc.abstractmethod - def render(self, result): - """ - This method can either be overriden in a subclass or left alone. In the - latter case it will be generated by - :py:class:`~.meta.InstanceListerMetaClass` metaclass with regard to - ``PROPERTIES`` and ``DYNAMIC_PROPERTIES``. - - :param result: Either an instance to render or pair of properties and - instance. - :type result: :py:class:`lmi.shell.LMIInstance` or tuple - :returns: List of pairs, where the first item is a label and second a - value to render. - :rtype: list - """ - raise NotImplementedError( - "render method must be overriden in subclass") - - def take_action(self, connection, args, kwargs): - """ - Collects results of single host. - - :param connection: Connection to a single host. - :type connection: :py:class:`lmi.shell.LMIConnection` - :param list args: Positional arguments for associated function. - :param dictionary kwargs: Keyword arguments for associated function. - :returns: Column names and item list as a pair. - :rtype: tuple - """ - cols = self.get_columns() - if cols is None: - result = self.execute_on_connection( - connection, *args, **kwargs) - if not isinstance(result, tuple) or len(result) != 2: - raise errors.LmiUnexpectedResult( - self.__class__, "(properties, instances)", result) - cols, data = result - if not isinstance(cols, (tuple, list)): - raise errors.LmiUnexpectedResult( - self.__class__, "(tuple, ...)", (cols, '...')) - header = [c if isinstance(c, basestring) else c[0] for c in cols] - cmd = fcmd.NewTableHeaderCommand(columns=header) - return chain((cmd, ), (self.render((cols, inst)) for inst in data)) - else: - data = self.execute_on_connection(connection, *args, **kwargs) - if not hasattr(data, '__iter__'): - raise errors.LmiUnexpectedResult( - self.__class__, 'list or generator', data) - cmd = fcmd.NewTableHeaderCommand(columns=cols) - return chain((cmd, ), (self.render(inst) for inst in data)) diff --git a/lmi/scripts/common/command/meta.py b/lmi/scripts/common/command/meta.py deleted file mode 100644 index 7d8213e..0000000 --- a/lmi/scripts/common/command/meta.py +++ /dev/null @@ -1,839 +0,0 @@ -# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of the FreeBSD Project. -# -# Authors: Michal Minar <miminar@redhat.com> -# -""" -Meta classes simplifying declaration of user commands. - -Each command is defined as a class with a set of properties. Some are -mandatory, the others have some default values. Each of them is transformed by -metaclasse to some function, class method or other property depending on -command type and semantic of property. Property itself is removed from -resulting class after being processed by meta class. -""" - -import abc -import inspect -import re - -from lmi.scripts.common import Configuration -from lmi.scripts.common import get_logger -from lmi.scripts.common import errors -from lmi.scripts.common.command import base -from lmi.scripts.common.command import util -from lmi.shell import LMIInstance -from lmi.shell.LMIReturnValue import LMIReturnValue - -RE_CALLABLE = re.compile( - r'^(?P<module>[a-z_]+(?:\.[a-z_]+)*):(?P<func>[a-z_]+)$', - re.IGNORECASE) -RE_ARRAY_SUFFIX = re.compile(r'^(?:[a-z_]+[a-z0-9_]*)?$', re.IGNORECASE) -RE_OPTION = re.compile(r'^-+(?P<name>[^-+].*)$') -RE_MODULE_PATH = re.compile(r'([a-zA-z_]\w+\.)+[a-zA-z_]\w+') - -FORMAT_OPTIONS = ('no_headings', 'human_friendly') - -LOG = get_logger(__name__) - -def _handle_usage(name, bases, dcl): - """ - Take care of ``OWN_USAGE`` property. Supported values: - - `True`` : - Means that documentation string of class is a usage string. - ``False`` : - No usage string for this command is defined. - ``"usage string"`` : - This property is a usage string. - - Defaults to ``False``. - - Usage string is an input parameter to ``docopt`` command-line options - parser. - - :param string name: Name o command class. - :param dictionary dcl: Class dictionary, which is modified by this - function. - """ - has_own_usage = False - hlp = dcl.pop('OWN_USAGE', False) - if hlp is True: - if dcl['__doc__'] is None: - raise errors.LmiCommandInvalidProperty(dcl['__module__'], name, - "OWN_USAGE set to True, but no __doc__ string present!") - has_own_usage = True - elif isinstance(hlp, basestring): - if not '__doc__' in dcl: - dcl['__doc__'] = hlp - else: - if not 'get_usage' in dcl: - def _new_get_usage(_self, proper=False): - """ Get the usage string for ``doctopt`` parser. """ - return hlp - dcl['get_usage'] = _new_get_usage - has_own_usage = True - elif ( dcl.get('__node__', None) is None - and any(getattr(b, 'has_own_usage', lambda: False)() for b in bases)): - # inherit doc string of base class - dcl['__doc__'] = ( b.__doc__ for b in bases - if getattr(b, 'has_own_usage', lambda: False)()).next() - has_own_usage = True - if has_own_usage: - if not 'has_own_usage' in dcl: - dcl['has_own_usage'] = classmethod(lambda _cls: True) - -def _make_execute_method(bases, dcl, func): - """ - Creates ``execute()`` method of a new end point command. - - :param tuple bases: Base classes of new command. - :param dictionary dcl: Class dictionary being modified by this method. - :param callable func: A callable wrapped by this new command. It's usually - being referred to as *associated function*. If ``None``, no function - will be created -- ``dcl`` won't be modified. - """ - if func is not None and util.is_abstract_method( - bases, 'execute', missing_is_abstract=True): - del dcl['CALLABLE'] - def _execute(__self__, __connection__, *args, **kwargs): - """ Invokes associated function with given arguments. """ - return func(__connection__, *args, **kwargs) - _execute.dest = func - dcl['execute'] = _execute - -def _handle_namespace(dcl): - """ - Overrides ``cim_namespace()`` class method if ``NAMESPACE`` property - is given. - - :param dictionary dcl: Class dictionary being modified by this method. - """ - if 'NAMESPACE' in dcl: - namespace = dcl.pop('NAMESPACE') - def _new_cim_namespace(_cls): - """ Returns cim namespace used to modify connection object. """ - return namespace - dcl['cim_namespace'] = classmethod(_new_cim_namespace) - -def _handle_callable(name, bases, dcl): - """ - Process the ``CALLABLE`` property of end-point command. Create the - ``execute()`` method based on it. - - :param string name: Name of command class to create. - :param tuple bases: Base classes of new command. - :param dictionary dcl: Class dictionary being modified by this method. - """ - try: - func = dcl.get('CALLABLE') - if isinstance(func, basestring): - match = RE_CALLABLE.match(func) - if not match: - raise errors.LmiCommandInvalidCallable( - dcl['__module__'], name, - 'Callable "%s" has invalid format (\':\' expected)' - % func) - mod_name = match.group('module') - try: - func = getattr(__import__(mod_name, globals(), locals(), - [match.group('func')], 0), - match.group('func')) - except (ImportError, AttributeError): - raise errors.LmiImportCallableFailed( - dcl['__module__'], name, func) - except KeyError: - raise errors.LmiCommandMissingCallable(dcl['__module__'], name) - if func is not None and not callable(func): - raise errors.LmiCommandInvalidCallable( - '"%s" is not a callable object or function.' % ( - func.__module__ + '.' + func.__name__)) - - _make_execute_method(bases, dcl, func) - -def _make_render_all_properties(bases): - """ - Creates ``render()`` method, rendering all properties of instance. - - :param tuple bases: Base classes of new command class. - :returns: Rendering method taking CIM instance as an - argument. - :rtype: function - """ - if util.is_abstract_method(bases, 'render', missing_is_abstract=True): - def _render(_self, inst): - """ - Return tuple of ``(column_names, values)`` ready for output by - formatter. - """ - column_names, values = [], [] - for prop_name, value in sorted(inst.properties_dict().iteritems()): - column_names.append(prop_name) - if value is None: - value = '' - values.append(value) - return (column_names, values) - - return _render - -def _make_render_with_properties(properties, target_formatter_lister=False): - """ - Creates ``render()`` method, rendering given instance properties. - - :param properties: (``list``) List of properties to render. - :param target_formatter_lister: (``bool``) Whether the output is targeted - for Show command or Lister. The former expects a pair of column_names - and values. The latter expects just values. - :rtype: (``function``) Rendering method taking CIM instance as an - argument. - """ - def _process_property(prop, inst): - """ - Takes a single property and instance. Returns computed value. - - :rtype: ``(str, any)`` A pair of property name and value. - """ - if isinstance(prop, basestring): - prop_name = prop - if not prop in inst.properties(): - LOG().warn('Property "%s" not present in instance of "%s".', - prop, inst.path) - value = "UNKNOWN" - else: - value = getattr(inst, prop) - else: - if not isinstance(prop, (tuple, list)): - raise TypeError("prop must be a string or tuple, not %s" % - repr(prop)) - prop_name = prop[0] - try: - if callable(prop[1]): - value = prop[1](inst) - else: - value = getattr(inst, prop[1]) - except Exception as exc: - LOG().exception('Failed to render property "%s": %s', - prop[0], exc) - value = "ERROR" - if value is None: - value = '' - return prop_name, value - - if target_formatter_lister: - def _render(self, inst): - """ - Renders a limited set of properties and returns a row for instance - table composed of property values. - """ - if not isinstance(inst, LMIInstance): - raise errors.LmiUnexpectedResult( - self.__class__, 'LMIInstance object', inst) - return tuple(_process_property(p, inst)[1] for p in properties) - - else: - def _render(self, inst): - """ - Renders a limited set of properties and returns a pair of - column names and values. - """ - if not isinstance(inst, LMIInstance): - raise errors.LmiUnexpectedResult( - self.__class__, 'LMIInstance object', inst) - column_names, values = [], [] - for prop in properties: - prop_name, value = _process_property(prop, inst) - column_names.append(prop_name) - values.append(value) - return (column_names, values) - - return _render - -def _check_render_properties(name, dcl, props): - """ - Make sanity check for ``PROPERTIES`` class property. Exception will be - raised when any flaw discovered. - - :param string name: Name of class to be created. - :param dictionary dcl: Class dictionary. - :param list props: List of properties or ``None``. - """ - if props is not None: - for prop in props: - if not isinstance(prop, (basestring, tuple, list)): - raise errors.LmiCommandInvalidProperty( - dcl['__module__'], name, - 'PROPERTIES must be a list of strings or tuples') - if isinstance(prop, (tuple, list)): - if ( len(prop) != 2 - or not isinstance(prop[0], basestring) - or ( not callable(prop[1]) - and not isinstance(prop[1], basestring))): - raise errors.LmiCommandInvalidProperty( - dcl['__module__'], name, - 'tuples in PROPERTIES must be: ("name",' - ' callable or property_name)') - -def _handle_render_properties(name, bases, dcl, target_formatter_lister=False): - """ - Process properties related to rendering function for commands operating - on CIM instances. Result of this function a ``render()`` and - ``get_columns()`` functions being added to class's dictionary with - regard to handled properties. - - Currently handled properties are: - - ``DYNAMIC_PROPERTIES`` : ``bool`` - Whether the associated function itself provides list of - properties. Optional property. - ``PROPERTIES`` : ``bool`` - List of instance properties to print. Optional property. - - :param string name: Name of class to be created. - :param tuple bases: Base classes of new command. - :param dictionary dcl: Class dictionary being modified by this method. - :param boolean target_formatter_lister: Whether the output is targeted - for *Show* command or *Lister*. The former expects a pair of - column_names and values. The latter expects just values. - """ - dynamic_properties = dcl.pop('DYNAMIC_PROPERTIES', False) - if dynamic_properties and 'PROPERTIES' in dcl: - raise errors.LmiCommandError( - dcl['__module__'], name, - 'DYNAMIC_PROPERTIES and PROPERTIES are mutually exclusive') - - properties = dcl.pop('PROPERTIES', None) - _check_render_properties(name, dcl, properties) - - renderer = None - get_columns = lambda cls: None - if properties is None and not dynamic_properties: - if ( target_formatter_lister - and dcl.get('__metaclass__', None) is not InstanceListerMetaClass): - raise errors.LmiCommandError(dcl['__module__'], name, - "either PROPERTIES must be declared or" - " DYNAMIC_PROPERTIES == True for InstanceLister" - " commands") - renderer = _make_render_all_properties(bases) - elif properties is None and dynamic_properties: - def _render_dynamic(self, return_value): - """ Renderer of dynamic properties. """ - properties, inst = return_value - return _make_render_with_properties(properties, - target_formatter_lister)(self, inst) - renderer = _render_dynamic - elif properties is not None: - renderer = _make_render_with_properties(properties, - target_formatter_lister) - get_columns = (lambda cls: - tuple((p[0] if isinstance(p, tuple) else p) - for p in properties)) - if renderer is not None: - dcl['render'] = classmethod(renderer) - if target_formatter_lister: - dcl['get_columns'] = get_columns - -def _handle_opt_preprocess(name, dcl): - """ - Process properties, that cause modification of parsed argument names before - passing them to ``verify_options()`` or ``transform_options()``. If any of - handled properties is supplied, it causes ``_preprocess_options()`` to be - overriden, where all of desired name modifications will be made. - Currently handled properties are: - - ``OPT_NO_UNDERSCORES`` : ``bool`` - When making a function's parameter name out of option, the leading - dashes are replaced with underscore. If this property is True, - dashes will be removed completely with no replacement. - ``ARG_ARRAY_SUFFIX`` : ``bool`` - Add given suffix to all arguments resulting in list objects. - - :param string name: Command class name. - :param dictionary dcl: Class dictionary being modified by this method. - """ - if ( dcl.get('__metaclass__', None) is not EndPointCommandMetaClass - and '_preprocess_options' in dcl): - raise errors.LmiCommandError(dcl['__module__'], name, - '_preprocess_options() method must not be overriden in the' - 'body of command class; use transform_options() instead') - arr_suffix = dcl.pop('ARG_ARRAY_SUFFIX', '') - if ( not isinstance(arr_suffix, str) - or not RE_ARRAY_SUFFIX.match(arr_suffix)): - raise errors.LmiCommandInvalidProperty(dcl['__module__'], name, - 'ARG_ARRAY_SUFFIX must be a string matching regular' - ' expression "%s"' % RE_ARRAY_SUFFIX.pattern) - opt_no_underscores = dcl.pop('OPT_NO_UNDERSCORES', False) - if arr_suffix or opt_no_underscores: - def _new_preprocess_options(_self, options): - """ Modify (in-place) given options dictionary by renaming keys. """ - for do_it, cond, transform in ( - ( arr_suffix - , lambda _, v: isinstance(v, list) - , lambda n : - ('<' + util.RE_OPT_BRACKET_ARGUMENT.match(n) - .group(1) + arr_suffix + '>') - if util.RE_OPT_BRACKET_ARGUMENT.match(n) - else (n + arr_suffix)) - , ( opt_no_underscores - , lambda n, _: RE_OPTION.match(n) - , lambda n : RE_OPTION.match(n).group('name')) - ): - if not do_it: - continue - to_rename = ( name for name, value in options.items() - if cond(name, value)) - for name in to_rename: - new_name = transform(name) - LOG().debug('Renaming option "%s" to "%s".', name, new_name) - if new_name in options: - LOG().warn( - 'Existing option named "%s" replaced with "%s".', - new_name, name) - options[new_name] = options.pop(name) - - dcl['_preprocess_options'] = _new_preprocess_options - -def _handle_fallback_command(name, bases, dcl): - """ - Process ``FALLBACK_COMMAND`` property of multiplexer command. It's turned - into a :py:meth:`~.multiplexer.LmiCommandMultiplexer.fallback_command` - class method. It needs to be called after the usage string is handled. - - .. seealso:: - :py:func:`_handle_usage` - """ - fallback = dcl.pop('FALLBACK_COMMAND', None) - if fallback is not None: - if not issubclass(type(fallback), EndPointCommandMetaClass): - raise errors.LmiCommandInvalidProperty(dcl['__module__'], name, - "FALLBACK_COMMAND must be a command class" - " (subclass of LmiEndPointCommand) not %s" % repr(fallback)) - if not fallback.has_own_usage(): - usage_string = dcl.get('__doc__', None) - if not usage_string: - for base_cls in bases: - if not issubclass(base_cls, base.LmiBaseCommand): - continue - cmd = base_cls - while not cmd.has_own_usage() and cmd.parent is not None: - cmd = cmd.parent - usage_string = cmd.__doc__ - if not usage_string: - errors.LmiCommandError(dcl['__module__'], name, - "Missing usage string.") - fallback.__doc__ = usage_string - fallback.has_own_usage = lambda cls: True - dcl['fallback_command'] = staticmethod(lambda: fallback) - -def _handle_format_options(name, bases, dcl): - """ - Process any ``FMT_*`` properties. This overrides ``format_options`` - property which returns dictionary of arguments passed to formatter factory. - - These properties are removed from class' dictionary. - """ - format_options = {} - for key, value in dcl.items(): - if key.startswith("FMT_"): - opt_name = key[4:].lower() - if opt_name not in FORMAT_OPTIONS: - raise errors.LmiCommandInvalidProperty( - dcl['__module__'], name, - 'Formatting option "%s" is not supported.' % - opt_name) - if ( opt_name in ('no_headings', 'human_friendly') - and not isinstance(value, bool)): - raise errors.LmiCommandInvalidProperty( - dcl['__module__'], name, - '"%s" property must be a boolean') - format_options[opt_name] = value - - if format_options: - def _new_format_options(self): - """ :returns: Dictionary of options for formatter object. """ - basecls = [b for b in bases if issubclass(b, base.LmiBaseCommand)][0] - opts = basecls.format_options.fget(self) - opts.update(format_options) - return opts - if 'format_options' in dcl: - raise errors.LmiCommandError(dcl['__module__'], name, - 'can not define both FMT_ options and "format_options" in' - ' the same class, choose just one of them') - dcl['format_options'] = property(_new_format_options) - - for key in format_options: - dcl.pop('FMT_' + key.upper()) - -def _handle_select(name, dcl): - """ - Process properties of :py:class:`.select.LmiSelectCommand`. - Currently handled properties are: - - ``SELECT`` : ``list`` - Is a list of pairs ``(condition, command)`` where ``condition`` is - an expression in *LMIReSpL* language. And ``command`` is either a - string with absolute path to command that shall be loaded or the - command class itself. - - Small example: :: - - SELECT = [ - ( 'OpenLMI-Hardware < 0.4.2' - , 'lmi.scripts.hardware.pre042.PreCmd' - ) - , ('OpenLMI-Hardware >= 0.4.2 & class LMI_Chassis == 0.3.0' - , HwCmd - ) - ] - - It says: Let the ``PreHwCmd`` command do the job on brokers having - ``openlmi-hardware`` package older than ``0.4.2``. Use the - ``HwCmd`` anywhere else where also the ``LMI_Chassis`` CIM class in - version ``0.3.0`` is available. - - First matching condition wins and assigned command will be passed - all the arguments. - - ``DEFAULT`` : ``str`` or :py:class:`~.base.LmiBaseCommand` - Defines fallback command used in case no condition can be - satisfied. - - They will be turned into ``get_conditionals()`` method. - """ - module_name = dcl.get('__module__', name) - if not 'SELECT' in dcl: - raise errors.LmiCommandError(module_name, name, - "Missing SELECT property.") - def inv_prop(msg, *args): - return errors.LmiCommandInvalidProperty(module_name, name, msg % args) - expressions = dcl.pop('SELECT') - if not isinstance(expressions, (list, tuple)): - raise inv_prop('SELECT must be list or tuple.') - if len(expressions) < 1: - raise inv_prop('SELECT must contain at least one condition!') - for index, item in enumerate(expressions): - if not isinstance(item, tuple): - raise inv_prop('Items of SELECT must be tuples, not %s!' % - getattr(type(item), '__name__', 'UNKNOWN')) - if len(item) != 2: - raise inv_prop('Expected pair in SELECT on index %d!' % index) - expr, cmd = item - if not isinstance(expr, basestring): - raise inv_prop('Expected expression string on index %d' - ' in SELECT!' % index) - if isinstance(cmd, basestring) and not RE_MODULE_PATH.match(cmd): - raise inv_prop('Second item of conditional pair on index %d' - ' in SELECT does not look as an importable path!' % cmd) - if ( not isinstance(cmd, basestring) - and not issubclass(cmd, (basestring, base.LmiBaseCommand))): - raise inv_prop('Expected subclass of LmiBaseCommand (or its import' - ' path) as a second item of a pair on index %d in SELECT!' - % index) - - default = dcl.pop('DEFAULT', None) - if isinstance(default, basestring) and not RE_MODULE_PATH.match(default): - raise inv_prop('DEFAULT "%s" does not look as an importable path!' - % default) - if ( default is not None and not isinstance(default, basestring) - and not issubclass(default, (basestring, base.LmiBaseCommand))): - raise inv_prop('Expected subclass of LmiBaseCommand' - ' (or its import path) as a value of DEFAULT!') - def _new_get_conditionals(self): - return expressions, default - - dcl['get_conditionals'] = _new_get_conditionals - -class EndPointCommandMetaClass(abc.ABCMeta): - """ - End point command does not have any subcommands. It's a leaf of - command tree. It wraps some function in command library being - referred to as an *associated function*. It handles following class - properties: - - ``CALLABLE`` : ``str`` or callable - An associated function. Mandatory property. - ``OWN_USAGE`` : ``bool`` or ``str`` - Usage string. Optional property. - ``ARG_ARRAY_SUFFIX`` : ``str`` - Suffix added to argument names containing array of values. - Optional property. - ``FMT_NO_HEADINGS`` : ``bool`` - Allows to force printing of table headers on and off for - this command. Default is to print them. - ``FMT_HUMAN_FRIENDLY`` : ``bool`` - Tells formatter to make the output more human friendly. The result - is dependent on the type of formatter used. - """ - - def __new__(mcs, name, bases, dcl): - _handle_usage(name, bases, dcl) - _handle_callable(name, bases, dcl) - _handle_opt_preprocess(name, dcl) - _handle_format_options(name, bases, dcl) - - cls = super(EndPointCommandMetaClass, mcs).__new__( - mcs, name, bases, dcl) - - # make additional check for arguments count - dest = getattr(cls.execute, "dest", cls.execute) - argspec = inspect.getargspec(dest) - if ( not argspec.varargs - and len(argspec.args) < cls.dest_pos_args_count()): - raise errors.LmiCommandInvalidCallable( - dcl['__module__'], name, - 'Callable must accept at least %d positional arguments' % - cls.dest_pos_args_count()) - - return cls - -class SessionCommandMetaClass(EndPointCommandMetaClass): - """ - Meta class for commands operating upon a session object. - All associated functions take as first argument an namespace abstraction - of type ``lmi.shell``. - - Handles following class properties: - - ``NAMESPACE`` : ``str`` - CIM namespace abstraction that will be passed to associated - function. Defaults to ``"root/cimv2"``. If ``False``, raw - :py:class:`lmi.shell.LMIConnection` object will be passed to - associated function. - """ - def __new__(mcs, name, bases, dcl): - _handle_usage(name, bases, dcl) - _handle_namespace(dcl) - _handle_callable(name, bases, dcl) - - return EndPointCommandMetaClass.__new__(mcs, name, bases, dcl) - -class ListerMetaClass(SessionCommandMetaClass): - """ - Meta class for end-point lister commands. Handles following class - properties: - - ``COLUMNS`` : ``tuple`` - List of column names. Optional property. There are special values - such as: - - ``None`` or omitted - Associated function provides column names in a first row of - returned list or generator. - - empty list, empty tuple or ``False`` - They mean that no headers shall be printed. It is simalar - to using ``FMT_NO_HEADINGS = True``. But in this case all - the rows returned from associated functions are treated as - data. - """ - - def __new__(mcs, name, bases, dcl): - cols = dcl.pop('COLUMNS', None) - if cols is not None: - if not isinstance(cols, (list, tuple)): - raise errors.LmiCommandInvalidProperty(dcl['__module__'], name, - 'COLUMNS class property must be either list or tuple') - if len(cols) < 1 or cols is False: - dcl['FMT_NO_HEADINGS'] = True - cols = tuple() - elif not all(isinstance(c, basestring) for c in cols): - raise errors.LmiCommandInvalidProperty(dcl['__module__'], name, - 'COLUMNS must contain just column names as strings') - def _new_get_columns(_cls): - """ Return column names. """ - return cols - dcl['get_columns'] = classmethod(_new_get_columns) - - return super(ListerMetaClass, mcs).__new__(mcs, name, bases, dcl) - -class ShowInstanceMetaClass(SessionCommandMetaClass): - """ - Meta class for end-point show instance commands. Additional handled - properties: - - ``DYNAMIC_PROPERTIES`` : ``bool`` - Whether the associated function itself provides list of - properties. Optional property. - ``PROPERTIES`` : ``tuple`` - List of instance properties to print. Optional property. - - These are translated in a :py:meth:`~.show.LmiShowInstance.render`, which - should be marked as abstract in base lister class. - """ - - def __new__(mcs, name, bases, dcl): - _handle_render_properties(name, bases, dcl) - - return super(ShowInstanceMetaClass, mcs).__new__( - mcs, name, bases, dcl) - -class InstanceListerMetaClass(SessionCommandMetaClass): - """ - Meta class for instance lister command handling the same properties - as :py:class:`ShowInstanceMetaClass`. - """ - - def __new__(mcs, name, bases, dcl): - _handle_render_properties(name, bases, dcl, True) - - return super(InstanceListerMetaClass, mcs).__new__( - mcs, name, bases, dcl) - -class CheckResultMetaClass(SessionCommandMetaClass): - """ - Meta class for end-point command "check result". Additional handled - properties: - - ``EXPECT`` : - Value to compare against the return value. Mandatory property. - - ``EXPECT`` property is transformed into a - :py:meth:`.checkresult.LmiCheckResult.check_result` method taking two - arguments ``(options, result)`` and returning a boolean. - """ - - def __new__(mcs, name, bases, dcl): - try: - expect = dcl['EXPECT'] - if callable(expect): - def _new_expect(_self, options, result): - """ - Comparison function testing return value with *expect* - function. - """ - if isinstance(result, LMIReturnValue): - result = result.rval - passed = expect(options, result) - if not passed: - LOG().info('Got unexpected result "%s".') - return passed - else: - def _new_expect(_self, _options, result): - """ Comparison function testing by equivalence. """ - if isinstance(result, LMIReturnValue): - result = result.rval - passed = expect == result - if not passed: - LOG().info('Expected "%s", got "%s".', expect, result) - return (False, '%s != %s' % (expect, result)) - return passed - _new_expect.expected = expect - del dcl['EXPECT'] - dcl['check_result'] = _new_expect - except KeyError: - # EXPECT might be defined in some subclass - pass - - return super(CheckResultMetaClass, mcs).__new__(mcs, name, bases, dcl) - -class MultiplexerMetaClass(abc.ABCMeta): - """ - Meta class for node command (not an end-point command). It handles - following class properties: - - ``COMMANDS`` : ``dict`` - Command names with assigned command classes. Each of them is a - direct subcommands of command with this property. Mandatory - property. - - ``FALLBACK_COMMAND`` : :py:class:`~.endpoint.LmiEndPointCommand` - Command factory to use in case that no command is passed on command - line. - - Formatting options (starting with ``FMT_`` are also accepted, and may used - to set defaults for all subcommands. - """ - - def __new__(mcs, name, bases, dcl): - if dcl.get('__metaclass__', None) is not MultiplexerMetaClass: - module_name = dcl.get('__module__', name) - # check COMMANDS property and make it a classmethod - if not 'COMMANDS' in dcl: - raise errors.LmiCommandError(module_name, name, - 'Missing COMMANDS property.') - cmds = dcl.pop('COMMANDS') - if not isinstance(cmds, dict): - raise errors.LmiCommandInvalidProperty(module_name, name, - 'COMMANDS must be a dictionary') - if not all(isinstance(c, basestring) for c in cmds.keys()): - raise errors.LmiCommandInvalidProperty(module_name, name, - 'Keys of COMMANDS dictionary must contain command' - ' names as strings.') - for cmd_name, cmd in cmds.items(): - if not util.RE_COMMAND_NAME.match(cmd_name): - raise errors.LmiCommandInvalidName( - module_name, name, cmd_name) - if not issubclass(cmd, base.LmiBaseCommand): - raise errors.LmiCommandError(module_name, name, - 'COMMANDS dictionary must be composed of' - ' LmiBaseCommand subclasses, failed class: "%s"' - % cmd.__name__) - if cmd.is_multiplexer() and not cmd.has_own_usage(): - LOG().warn('Command "%s.%s" is missing usage string.' - ' It will be inherited from parent command.', - cmd.__module__, cmd.__name__) - cmd.__doc__ = dcl['__doc__'] - def _new_child_commands(_cls): - """ Returns list of subcommands. """ - return cmds - dcl['child_commands'] = classmethod(_new_child_commands) - - _handle_usage(name, bases, dcl) - _handle_fallback_command(name, bases, dcl) - _handle_format_options(name, bases, dcl) - - return super(MultiplexerMetaClass, mcs).__new__(mcs, name, bases, dcl) - -class SelectMetaClass(abc.ABCMeta): - """ - Meta class for select commands with guarded commands. Additional handled - properties: - - ``SELECT`` : ``list`` - List of commands guarded with expressions representing requirements - on server's side that need to be met. - ``DEFAULT`` : ``str`` or :py:class:`~.base.LmiBaseCommand` - Defines fallback command used in case no condition can is - satisfied. - """ - - def __new__(mcs, name, bases, dcl): - if dcl.get('__metaclass__', None) is not SelectMetaClass: - module_name = dcl.get('__module__', name) - if not '__doc__' in dcl: - LOG().warn('Command selector "%s.%s" is missing short' - ' description string (__doc__).', - module_name, name) - default = dcl.get('DEFAULT', None) - if ( default is not None - and issubclass(default, base.LmiBaseCommand) - and getattr(dcl['DEFAULT'], '__doc__', None)): - LOG().warn('Using __doc__ string from default command for' - ' selector "%s.%s".', module_name, name) - dcl['__doc__'] = dcl['DEFAULT'].__doc__ - _handle_select(name, dcl) - return super(SelectMetaClass, mcs).__new__(mcs, name, bases, dcl) - diff --git a/lmi/scripts/common/command/multiplexer.py b/lmi/scripts/common/command/multiplexer.py deleted file mode 100644 index 61ebcd7..0000000 --- a/lmi/scripts/common/command/multiplexer.py +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of the FreeBSD Project. -# -# Authors: Michal Minar <miminar@redhat.com> -# -""" -Defines command class used to nest multiple commands under one. -""" - -from docopt import docopt - -from lmi.scripts.common import get_logger -from lmi.scripts.common.command import base -from lmi.scripts.common.command import meta - -LOG = get_logger(__name__) - -class LmiCommandMultiplexer(base.LmiBaseCommand): - """ - Base class for node commands. It consumes just part of command line - arguments and passes the remainder to one of its subcommands. - - Example usage: :: - - class MyCommand(LmiCommandMultiplexer): - ''' - My command description. - - Usage: %(cmd)s mycommand (subcmd1 | subcmd2) - ''' - COMMANDS = {'subcmd1' : Subcmd1, 'subcmd2' : Subcmd2} - - Where ``Subcmd1`` and ``Subcmd2`` are some other ``LmiBaseCommand`` - subclasses. Documentation string must be parseable with ``docopt``. - - Recognized properties: - - ``COMMANDS`` : ``dictionary`` - property will be translated to - :py:meth:`LmiCommandMultiplexer.child_commands` class method by - :py:class:`~.meta.MultiplexerMetaClass`. - - Using metaclass: :py:class:`.meta.MultiplexerMetaClass`. - """ - __metaclass__ = meta.MultiplexerMetaClass - - @classmethod - def child_commands(cls): - """ - Abstract class method, that needs to be implemented in subclass. - This is done by associated meta-class, when defining a command with - assigned ``COMMANDS`` property. - - :returns: Dictionary of subcommand names with assigned command - factories. - :rtype: dictionary - """ - raise NotImplementedError("child_commands must be implemented in" - " a subclass") - - @classmethod - def fallback_command(cls): - """ - This is overriden by :py:class:`~.meta.MultiplexerMetaClass` when - the ``FALLBACK_COMMAND`` gets processed. - - :returns: Command factory invoked for missing command on command line. - :rtype: :py:class:`~.endpoint.LmiEndPointCommand` - """ - return None - - @classmethod - def is_end_point(cls): - return False - - def run_subcommand(self, cmd_name, args): - """ - Pass control to a subcommand identified by given name. - - :param string cmd_name: Name of direct subcommand, whose - :py:meth:`~.base.LmiBaseCommand.run` method shall be invoked. - :param list args: List of arguments for particular subcommand. - :returns: Application exit code. - :rtype: integer - """ - if not isinstance(cmd_name, basestring): - raise TypeError("cmd_name must be a string, not %s" % - repr(cmd_name)) - if not isinstance(args, (list, tuple)): - raise TypeError("args must be a list, not %s" % repr(args)) - try: - cmd_cls = self.child_commands()[cmd_name] - cmd = cmd_cls(self.app, cmd_name, self) - except KeyError: - self.app.stderr.write(self.get_usage()) - LOG().critical('Unexpected command "%s".', cmd_name) - return 1 - return cmd.run(args) - - def run(self, args): - """ - Handle optional parameters, retrieve desired subcommand name and - pass the remainder of arguments to it. - - :param list args: List of arguments with at least subcommand name. - """ - if not isinstance(args, (list, tuple)): - raise TypeError("args must be a list") - full_args = self.get_cmd_name_parts(for_docopt=True) + args - docopt_kwargs = { - # check the --help ourselves (the default docopt behaviour checks - # also for --version) - 'help' : False, - # let's ignore options following first command for generated - # usage string and when a height of this branch is > 2 - 'options_first' : not self.has_own_usage() - or any( not cmd.is_end_point() - for cmd in self.child_commands().values() - if not args or args[0] not in self.child_commands() - or self.child_commands()[args[0]] is cmd) - } - options = docopt(self.get_usage(), full_args, **docopt_kwargs) - if options.pop('--help', False) or (args and args[0] == '--help'): - self.app.stdout.write(self.get_usage(proper=True)) - return 0 - if ( self.fallback_command() is not None - and (not args or args[0] not in self.child_commands())): - cmd_cls = self.fallback_command() - cmd = cmd_cls(self.app, self.cmd_name, self.parent) - return cmd.run(args) - return self.run_subcommand(args[0], args[1:]) diff --git a/lmi/scripts/common/command/select.py b/lmi/scripts/common/command/select.py deleted file mode 100644 index 5f89994..0000000 --- a/lmi/scripts/common/command/select.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of the FreeBSD Project. -# -# Authors: Michal Minar <miminar@redhat.com> -# -""" -Defines command class used to choose other commands depending on -profile and class requirements. -""" - -from docopt import docopt -from pyparsing import ParseException - -from lmi.scripts.common import get_logger -from lmi.scripts.common import errors -from lmi.scripts.common.command import base -from lmi.scripts.common.command import meta -from lmi.scripts.common.session import SessionProxy -from lmi.scripts.common.versioncheck import eval_respl - -class LmiSelectCommand(base.LmiBaseCommand): - """ - Base class for command selectors. It does not process command line - arguments. Thery are passed unchanged to selected command whose - requirements are met. Its doc string is not interpreted in any way. - - If there are more hosts, conditions are evaluated per each. They are then - split into groups, each fulfilling particular condition. Associated - commands are then invoked on these groups separately. - - Example usage: :: - - class MySelect(LmiSelectCommand): - SELECT = [ - ( 'OpenLMI-Hardware >= 0.4.2' - , 'lmi.scripts.hardware.current.Cmd'), - ('OpenLMI-Hardware', 'lmi.scripts.hardware.pre042.Cmd') - ] - DEFAULT = MissingHwProviderCmd - - Using metaclass: :py:class:`.meta.SelectMetaClass`. - """ - __metaclass__ = meta.SelectMetaClass - - @classmethod - def is_end_point(cls): - return False - - @classmethod - def is_multiplexer(cls): - return False - - @classmethod - def get_conditionals(cls): - """ - Get the expressions with associated commands. This shall be overriden - by a subclass. - - :returns: Pair of ``(expressions, default)``. - Where ``expressions`` is a list of pairs ``(condition, command)``. - And ``default`` is the same as ``command`` used in case no - ``condition`` is satisfied. - :rtype: list - """ - raise NotImplementedError( - "get_conditionals needs to be defined in subclass") - - def eval_expr(self, expr, hosts, cache=None): - """ - Evaluate expression on group of hosts. - - :param string expr: Expression to evaluate. - :param list hosts: Group of hosts that shall be checked. - :param dictionary cache: Optional cache object speeding up evaluation - by reducing number of queries to broker. - :returns: Subset of hosts satisfying *expr*. - :rtype: list - """ - if cache is None: - cache = dict() - session = self.session - satisfied = [] - try: - for host in hosts: # TODO: could be done concurrently - conn = session[host] - if not conn: - continue - if eval_respl(expr, conn, cache=cache): - satisfied.append(host) - except ParseException: - raise errors.LmiBadSelectExpression(self.__class__.__module__, - self.__class__.__name__, "Bad select expression: %s" % expr) - return satisfied - - def select_cmds(self, cache=None): - """ - Generator of command factories with associated groups of hosts. It - evaluates given expressions on session. In this process all expressions - from :py:meth:`get_conditionals` are checked in a row. Host satisfying - some expression is added to group associated with it and is excluded - from processing following expressions. - - :param dictionary cache: Optional cache object speeding up the evaluation - by reducing number of queries to broker. - :returns: Pairs in form ``(command_factory, session_proxy)``. - :rtype: generator - :raises: - * :py:class:`~lmi.scripts.common.errors.LmiUnsatisfiedDependencies` - if no condition is satisfied for at least one host. Note that - this exception is raised at the end of evaluation. This lets - you choose whether you want to process satisfied hosts - by - processing the generator at once. Or whether you want to be - sure it is satisfied by all of them - you turn the generator - into a list. - * :py:class:`~lmi.scripts.common.errors.LmiNoConnections` - if no successful connection was done. - """ - if cache is None: - cache = dict() - conds, default = self.get_conditionals() - def get_cmd_factory(cmd): - if isinstance(cmd, basestring): - i = cmd.rindex('.') - module = __import__(cmd[:i], fromlist=cmd[i+1:]) - return getattr(module, cmd[i+1:]) - else: - return cmd - - session = self.session - unsatisfied = set(session.hostnames) - - for expr, cmd in conds: - hosts = self.eval_expr(expr, unsatisfied, cache) - if hosts: - yield get_cmd_factory(cmd), SessionProxy(session, hosts) - hosts = set(hosts).union(set(session.get_unconnected())) - unsatisfied.difference_update(hosts) - if not unsatisfied: - break - if default is not None and unsatisfied: - yield get_cmd_factory(default), SessionProxy(session, unsatisfied) - unsatisfied.clear() - if len(unsatisfied): - raise errors.LmiUnsatisfiedDependencies(unsatisfied) - if len(session) == len(session.get_unconnected()): - raise errors.LmiNoConnections("No successful connection!") - - def get_usage(self, proper=False): - """ - Try to get usage of any command satisfying some expression. - - :raises: Same exceptions as :py:meth:`select_cmds`. - """ - for cmd_cls, _ in self.select_cmds(): - cmd = cmd_cls(self.app, self.cmd_name, self.parent) - return cmd.get_usage(proper) - - def run(self, args): - """ - Iterate over command factories with associated sessions and - execute them with unchanged *args*. - """ - result = 0 - for cmd_cls, session in self.select_cmds(): - cmd = cmd_cls(self.app, self.cmd_name, self.parent) - cmd.set_session_proxy(session) - ret = cmd.run(args) - if result == 0: - result = ret - return result - diff --git a/lmi/scripts/common/command/session.py b/lmi/scripts/common/command/session.py deleted file mode 100644 index 9d88bbe..0000000 --- a/lmi/scripts/common/command/session.py +++ /dev/null @@ -1,206 +0,0 @@ -# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of the FreeBSD Project. -# -# Authors: Michal Minar <miminar@redhat.com> -# -""" -Defines a base class for all command classes operating upon a -:py:class:`~lmi.scripts.common.session.Session` object. -""" -import abc -from collections import OrderedDict - -from lmi.scripts.common import get_logger -from lmi.scripts.common.command import meta -from lmi.scripts.common.command.endpoint import LmiEndPointCommand -from lmi.scripts.common.configuration import Configuration -from lmi.scripts.common.session import Session -from lmi.shell import LMIConnection -from lmi.shell import LMIUtil - -LOG = get_logger(__name__) - -class LmiSessionCommand(LmiEndPointCommand): - """ - Base class for end-point commands operating upon a session object. - - Using metaclass: :py:class:`~.meta.SessionCommandMetaClass`. - """ - __metaclass__ = meta.SessionCommandMetaClass - - @classmethod - def cim_namespace(cls): - """ - This returns default cim namespace, the connection object will be - nested into before being passed to associated function. - It can be overriden in few ways: - - 1. by setting ``[CIM] Namespace`` option in configuration - 2. by giving ``--namespace`` argument on command line to the - ``lmi`` meta-command - 3. by setting ``NAMESPACE`` property in declaration of command - - Higher number means higher priority. - """ - return Configuration.get_instance().namespace - - @classmethod - def dest_pos_args_count(cls): - """ - There is a namespace/connection object passed as the first positional - argument. - """ - return LmiEndPointCommand.dest_pos_args_count.im_func(cls) + 1 - - def process_session(self, session, args, kwargs): - """ - Process each host of given session, call the associated command - function, collect results and print it to standard output. - - :param session: Session object with set of hosts. - :type session: :py:class:`~lmi.scripts.common.session.Session` - :param list args: Positional arguments to pass to associated function - in command library. - :param dictionary kwargs: Keyword arguments as a dictionary. - :returns: Exit code of application. - :rtype: integer - """ - if not isinstance(session, Session): - raise TypeError("session must be an object of Session, not %s" - % repr(session)) - # { ( hostname : (passed, result) ), ... } - # where passed is a boolean and result is returned value if passed is - # True and exception otherwise - results = OrderedDict() - for connection in session: - try: - result = self.take_action(connection, args, kwargs) - # result may be a generator which may throw in the following - # function - results[connection.uri] = (True, result) - self.process_host_result(connection.uri, True, result) - except Exception as exc: - if len(session) > 1: - LOG().exception('Invocation failed for host "%s": %s', - connection.uri, exc) - else: - LOG().exception(exc) - results[connection.uri] = (False, exc) - self.process_host_result(connection.uri, False, exc) - self.process_session_results(session, results) - return all(r[0] for r in results.values()) - - def process_host_result(self, hostname, success, result): - """ - Called from :py:meth:`process_session` after single host gets - processed. By default this prints obtained *result* with default - formatter if the execution was successful. Children of this class may - want to override this. - - :param string hostname: Name of host involved. - :param boolean success: Whether the action on host succeeded. - :param result: Either the value returned by associated function upon a - successful invocation or an exception. - """ - if success: - if len(self.session) > 1: - self.formatter.print_host(hostname) - self.produce_output(result) - - def process_session_results(self, session, results): - """ - Called at the end of :py:meth:`process_session`'s execution. It's - supposed to do any summary work upon results from all hosts. By default - it just prints errors in a form of list. - - :param session: Session object. - :type session: :py:class:`lmi.scripts.common.session.Session` - :param dictionary results: Dictionary of form: :: - - { 'hostname' : (success_flag, result), ... } - - where *result* is either an exception or returned value of - associated function, depending on *success_flag*. See the - :py:meth:`process_host_result`. - """ - if not isinstance(session, Session): - raise TypeError("session must be a Session object") - if not isinstance(results, dict): - raise TypeError("results must be a dictionary") - # check whether any output has been produced - if ( len(session.get_unconnected()) - or any(not r[0] for r in results.values())): - data = [] - for hostname in session.get_unconnected(): - data.append((hostname, ['failed to connect'])) - for hostname, (success, error) in results.items(): - if not success: - data.append((hostname, [error])) - if len(session) > 1: - self._print_errors(data, - new_line=any(r[0] for r in results.values())) - - @abc.abstractmethod - def take_action(self, connection, args, kwargs): - """ - Executes an action on single host and collects results. - - :param connection: Connection to a single host. - :type connection: :py:class:`lmi.shell.LMIConnection` - :param list args: Positional arguments for associated function. - :param dictionary kwargs: Keyword arguments for associated function. - :returns: Column names and item list as a pair. - :rtype: tuple - """ - raise NotImplementedError("take_action must be implemented in subclass") - - def execute_on_connection(self, connection, *args, **kwargs): - """ - Wraps the :py:meth:`~.endpoint.LmiEndPointCommand.execute` method with - connection adjustments. Connection object is not usually passed - directly to associated function. Mostly it's the namespace object that - is expected. This method checks, whether the namespace object is - desired and modifies connection accordingly. - - :param connection: Connection to single host. - :type connection: :py:class:`lmi.shell.LMIConnection` - :param list args: Arguments handed over to associated function. - :param dictionary kwargs: Keyword arguments handed over to associated - function. - """ - if not isinstance(connection, LMIConnection): - raise TypeError("expected an instance of LMIConnection for" - " connection argument, not %s" % repr(connection)) - namespace = self.cim_namespace() - if namespace is not None: - connection = LMIUtil.lmi_wrap_cim_namespace( - connection, namespace) - return self.execute(connection, *args, **kwargs) - - def run_with_args(self, args, kwargs): - return self.process_session(self.session, args, kwargs) - diff --git a/lmi/scripts/common/command/show.py b/lmi/scripts/common/command/show.py deleted file mode 100644 index 1698fec..0000000 --- a/lmi/scripts/common/command/show.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of the FreeBSD Project. -# -# Authors: Michal Minar <miminar@redhat.com> -# -""" -Contains command classes producing key-value pairs to output. -""" -import abc - -from lmi.scripts.common import formatter -from lmi.scripts.common import get_logger -from lmi.scripts.common.command import meta -from lmi.scripts.common.command.session import LmiSessionCommand - -LOG = get_logger(__name__) - -class LmiShowInstance(LmiSessionCommand): - """ - End point command producing a list of properties of particular CIM - instance. Either reduced list of properties to print can be specified, or - the associated function alone can decide, which properties shall be - printed. Associated function is expected to return CIM instance as a - result. - - List of additional recognized properties: - - ``DYNAMIC_PROPERTIES`` : ``bool`` - A boolean saying, whether the associated function alone shall - specify the list of properties of rendered instance. If ``True``, - the result of function must be a pair: :: - - (props, inst) - - Where props is the same value as can be passed to ``PROPERTIES`` - property. Defaults to ``False``. - ``PROPERTIES`` : ``tuple`` - May contain list of instance properties, that will be produced in - the same order as output. Each item of list can be either: - - name : ``str`` - Name of property to render. - pair : ``tuple`` - A tuple ``(Name, render_func)``, where former item an - arbitraty name for rendered value and the latter is a - function taking as the only argument particular instance - and returning value to render. - - ``DYNAMIC_PROPERTIES`` and ``PROPERTIES`` are mutually exclusive. If none - is given, all instance properties will be printed. - - Using metaclass: :py:class:`~.meta.ShowInstanceMetaClass`. - """ - __metaclass__ = meta.ShowInstanceMetaClass - - def formatter_factory(self): - return formatter.SingleFormatter - - @abc.abstractmethod - def render(self, result): - """ - This method can either be overriden in a subclass or left alone. In the - latter case it will be generated by - :py:class:`~.meta.ShowInstanceMetaClass` metaclass with regard to - ``PROPERTIES`` and ``DYNAMIC_PROPERTIES``. - - :param result: Either an instance to - render or pair of properties and instance. - :type: :py:class:`lmi.shell.LMIInstance` or ``tuple`` - :returns: List of pairs, where the first item is a label and second a - value to render. - :rtype: list - """ - raise NotImplementedError( - "render method must be overriden in subclass") - - def take_action(self, connection, args, kwargs): - """ - Process single connection to a host, render result and return a value - to render. - - :returns: List of pairs, where the first item is a label and - second a value to render. - :rtype: list - """ - res = self.execute_on_connection(connection, *args, **kwargs) - return self.render(res) - diff --git a/lmi/scripts/common/command/util.py b/lmi/scripts/common/command/util.py deleted file mode 100644 index 914850a..0000000 --- a/lmi/scripts/common/command/util.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# -# The views and conclusions contained in the software and documentation are -# those of the authors and should not be interpreted as representing official -# policies, either expressed or implied, of the FreeBSD Project. -# -# Authors: Michal Minar <miminar@redhat.com> -# -""" -Utility functions used in command sub-package. -""" - -import inspect -import os -import re - -#: Regular expression matching bracket argument such as ``<arg_name>``. -RE_OPT_BRACKET_ARGUMENT = re.compile('^<(?P<name>[^>]+)>$') -#: Regular expression matching argument written in upper case such as -#:``ARG_NAME``. -RE_OPT_UPPER_ARGUMENT = re.compile('^(?P<name>[A-Z0-9]+(?:[_-][A-Z0-9]+)*)$') -#: Regular expression matching showt options. They are one character -#: long, prefixed with single dash. -RE_OPT_SHORT_OPTION = re.compile('^-(?P<name>[a-z])$', re.IGNORECASE) -#: Regular expression matching long options (prefixed with double dash). -RE_OPT_LONG_OPTION = re.compile('^--(?P<name>[a-z0-9_-]+)$', re.IGNORECASE) -#: Command name can also be a single or double dash. -RE_COMMAND_NAME = re.compile(r'^([a-z]+(-[a-z0-9]+)*|--?)$') - -def is_abstract_method(clss, method, missing_is_abstract=False): - """ - Check, whether the given method is abstract in given class or list of - classes. May be used to check, whether we should override particular - abstract method in a meta-class in case that no non-abstract - implementation is defined. - - :param clss: Class or list of classes that is - searched for non-abstract implementation of particular method. - If the first class having particular method in this list contain - non-abstract implementation, ``False`` is returned. - :type clss: type or tuple - :param string method: Name of method to look for. - :param boolean missing_is_abstract: This is a value returned, when - not such method is defined in a set of given classes. - :returns: Are all occurences of given method abstract? - :rtype: boolean - """ - if ( not isinstance(clss, (list, tuple, set)) - and not isinstance(clss, type)): - raise TypeError("clss must be either a class or a tuple of classes") - if not isinstance(method, basestring): - raise TypeError("method must be a string") - if isinstance(clss, type): - clss = [clss] - for cls in clss: - if hasattr(cls, method): - if getattr(getattr(cls, method), "__isabstractmethod__", False): - return True - else: - return False - return missing_is_abstract - -def get_module_name(frame_level=2): - """ - Get a module name of caller from particular outer frame. - - :param integer frame_level: Number of nested frames to skip when searching - for called function scope by inspecting stack upwards. When the result - of this function is applied directly on the definition of function, - it's value should be 1. When used from inside of some other factory, it - must be increased by 1. - - Level 0 returns name of this module. Level 1 returns module name of - caller. Level 2 returns module name of caller's caller. - :returns: Module name. - :rtype: string - """ - frame = inspect.currentframe() - while frame_level > 0 and frame.f_back: - frame = frame.f_back - frame_level -= 1 - module = getattr(frame, 'f_globals', {}).get('__name__', None) - if module is None: - if hasattr(frame, 'f_code'): - module = os.path.basename(frame.f_code.co_filename.splitext())[0] - else: - module = '_unknown_' - return module |