diff options
Diffstat (limited to 'lmi')
32 files changed, 0 insertions, 6812 deletions
diff --git a/lmi/__init__.py b/lmi/__init__.py deleted file mode 100644 index ca65877..0000000 --- a/lmi/__init__.py +++ /dev/null @@ -1,30 +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> -# -__import__('pkg_resources').declare_namespace(__name__) diff --git a/lmi/scripts/__init__.py b/lmi/scripts/__init__.py deleted file mode 100644 index ca65877..0000000 --- a/lmi/scripts/__init__.py +++ /dev/null @@ -1,30 +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> -# -__import__('pkg_resources').declare_namespace(__name__) diff --git a/lmi/scripts/_metacommand/__init__.py b/lmi/scripts/_metacommand/__init__.py deleted file mode 100644 index affea7b..0000000 --- a/lmi/scripts/_metacommand/__init__.py +++ /dev/null @@ -1,239 +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> -# -""" -Subpackage containing functionality of lmi meta-command. -""" - -import argparse -import logging -import sys - -from lmi.scripts import common -from lmi.scripts.common import errors -from lmi.scripts._metacommand import util -from lmi.scripts._metacommand import exit -from lmi.scripts._metacommand.help import Help -from lmi.scripts._metacommand.manager import CommandManager -from lmi.scripts._metacommand.interactive import Interactive -from lmi.scripts._metacommand.toplevel import TopLevelCommand -from lmi.scripts.common.command import LmiCommandMultiplexer, LmiBaseCommand -from lmi.scripts.common.configuration import Configuration -from lmi.scripts.common.session import Session -from lmi.shell import LMIUtil - -LOG = common.get_logger(__name__) - -# write errors to stderr until logging is configured -logging.getLogger('').addHandler(logging.StreamHandler()) - -class MetaCommand(object): - """ - Main application class. It instantiates configuration object, logging and - then it passes control to commands. - - Example usage: - - MetaCommand().run() - """ - - def __init__(self): - # allow exceptions in lmi shell - LMIUtil.lmi_set_use_exceptions(True) - # instance of CommandManager, created when first needed - self._command_manager = None - self.stdout = sys.stdout - self.stderr = sys.stderr - self.stdin = sys.stdin - # instance of Session, created when needed - self._session = None - # instance of Configuration, created in setup() - self.config = None - # dictionary of not yet processed options, it's created in setup() - self._options = None - self._active_command = None - - def _configure_logging(self): - """ - Setup logging. It expects Configuration object to be already - initialized. - - Logging can be tuned in various ways: - - * In configuration file with options: - * [Main] Verbosity - * [Log] OutputFile - * [Log] FileFormat - * [Log] ConsoleFormat - * [Log] ConsoleInfoFormat - * [Log] LogToConsole - * With command line options: - ``-v`` flags : - Each such flag increases logging level of what is logged - into console. This overrides `[Main] Verbosity` option. - ``-q`` : - Causes supression of any output made to stdout except for - error messages. This overrides ``[Main] Verbosity``. - option and ``-v`` flags. - ``--log-file`` : - Output file for logging messages. This overrides ``[Log] - OutputFile`` option. - - Implicitly only warnings and errors are logged to the standard error - stream without any tracebacks. - """ - util.setup_logging(self.config, self.stderr) - - @property - def command_manager(self): - """ - Return instance of ``CommandManager``. It's initialized when first - needed. - - :rtype: (``CommandManager``) - """ - if self._command_manager is None: - self._command_manager = CommandManager() - self._command_manager.add_command('help', Help) - return self._command_manager - - @property - def session(self): - """ - Return instance of Session. Instantiated when first needed. - - :rtype: (``Session``) - """ - if self._session is None: - if ( not self._options['--host'] - and not self._options['--hosts-file']): - self._options['--host'] = [util.get_default_hostname()] - LOG().info('No hosts given, using "%s".', - self._options['--host'][0]) - hostnames = [] - # credentials loaded from file - credentials = {} - def add_hosts(hosts, creds): - """ Update hostnames and credentials for new data. """ - hostnames.extend(hosts) - credentials.update(creds) - if self._options['--hosts-file']: - hosts_path = self._options['--hosts-file'] - try: - with open(hosts_path, 'r') as hosts_file: - add_hosts(*util.parse_hosts_file(hosts_file)) - except (OSError, IOError) as err: - LOG().critical('Could not read hosts file "%s": %s', - hosts_path, err) - sys.exit(1) - add_hosts(*util.get_hosts_credentials(self._options['--host'])) - if self._options['--user']: - credentials.update({ - # credentials in file has precedence over --user option - h : credentials.get(h, (self._options['--user'], '')) - for h in hostnames if h not in credentials - }) - self._session = Session(self, hostnames, credentials, - same_credentials=self._options['--same-credentials']) - return self._session - - @property - def active_command(self): - return self._active_command - @active_command.setter - def active_command(self, cmd): - if not isinstance(cmd, LmiBaseCommand): - raise TypeError("cmd must be an instance of LmiBaseCommand") - self._active_command = cmd - - def print_version(self): - """ Print version of this egg to stdout. """ - self.stdout.write("%s\n" % util.get_version()) - - def setup(self, options): - """ - Initialise global Configuration object and set up logging. - - :param options: (``dict``) Dictionary of options parsed from command - line by docopt. - """ - conf_kwargs = {} - if options['--config-file']: - conf_kwargs['user_config_file_path'] = options.pop('--config-file') - self.config = Configuration.get_instance(**conf_kwargs) - # two mutually exclusive options - if options['--trace'] or options['--notrace']: - self.config.trace = bool(options['--trace']) - if options.pop('--quiet', False): - self.config.verbosity = Configuration.OUTPUT_SILENT - elif options['-v'] and options['-v'] > 0: - self.config.verbosity = options['-v'] - if options.pop('--noverify', False): - self.config.verify_server_cert = False - self.config.log_file = options.pop('--log-file', None) - self._configure_logging() - del options['--trace'] - del options['--notrace'] - del options['-v'] - self.config.namespace = options.pop('--namespace', None) - self.config.human_friendly = options.pop('--human-friendly', None) - self.config.no_headings = options.pop('--no-headings', None) - self.config.lister_format = options.pop('--lister-format', None) - # unhandled options may be used later (for session creation), - # so let's save them - self._options = options - - def run(self, argv): - """ - Equivalent to the main program for the application. - - :param argv: (``list``) Input arguments and options. - Contains all arguments but the application name. - """ - retval = exit.EXIT_CODE_FAILURE - cmd = TopLevelCommand(self) - try: - retval = cmd.run(argv) - except Exception as exc: - if isinstance(exc, errors.LmiUnsatisfiedDependencies): - retval = exit.EXIT_CODE_UNSATISFIED_DEPENDENCIES - LOG().exception(str(exc)) - if isinstance(retval, bool) or not isinstance(retval, (int, long)): - return ( exit.EXIT_CODE_SUCCESS if bool(retval) or retval is None - else exit.EXIT_CODE_FAILURE) - - return retval - -def main(argv=sys.argv[1:]): - """ - Main entry point function. It just passes arguments to instantiated - ``MetaCommand``. - """ - return MetaCommand().run(argv) - diff --git a/lmi/scripts/_metacommand/cmdutil.py b/lmi/scripts/_metacommand/cmdutil.py deleted file mode 100644 index 986e835..0000000 --- a/lmi/scripts/_metacommand/cmdutil.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2013, 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 for command inspection. -""" -from lmi.scripts.common import errors -from lmi.scripts.common.command import LmiBaseCommand -from lmi.scripts.common.command import LmiCommandMultiplexer - -def get_subcommand_names(command): - """ - :param command: Either a multiplexer command or top-level one. - :returns: Names of children commands. - :rtype: list - """ - if not isinstance(command, LmiBaseCommand): - raise TypeError("command must be an instance of LmiBaseCommand") - if isinstance(command, LmiCommandMultiplexer): - return command.child_commands().keys() - if command.parent is None: # top level command - return command.app.command_manager.command_names - raise ValueError("command must be either multiplexer or top-level command") - -def get_subcommand_factory(command, name): - """ - :param command: Either a multiplexer command or top-level one. - :returns: Callable returning an instance of - :py:class:`~lmi.scripts.common.command.multiplexer.LmiCommandMultiplexer` - :rtype: callable - """ - if isinstance(command, LmiCommandMultiplexer): - try: - return command.child_commands()[name] - except KeyError: - cmd_path = command.cmd_name_parts - cmd_path.append(name) - raise errors.LmiCommandNotFound(" ".join(cmd_path)) - if command.parent is None: # top level command - return command.app.command_manager.find_command(name) - raise ValueError("command must be either multiplexer or top-level command") - diff --git a/lmi/scripts/_metacommand/exit.py b/lmi/scripts/_metacommand/exit.py deleted file mode 100644 index e876073..0000000 --- a/lmi/scripts/_metacommand/exit.py +++ /dev/null @@ -1,74 +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 containing help command. -""" - -from lmi.scripts.common import get_logger -from lmi.scripts.common.command import LmiEndPointCommand -from lmi.scripts.common import errors - -LOG = get_logger(__name__) - -EXIT_CODE_SUCCESS = 0 -EXIT_CODE_FAILURE = 1 -EXIT_CODE_KEYBOARD_INTERRUPT = 2 -EXIT_CODE_COMMAND_NOT_FOUND = 3 -EXIT_CODE_INVALID_SYNTAX = 4 -EXIT_CODE_UNSATISFIED_DEPENDENCIES = 5 - -def _execute_exit(exit_code): - """ Associated function with ``Exit`` command. """ - raise errors.LmiTerminate(exit_code) - -class Exit(LmiEndPointCommand): - """ - Terminate the shell. - - Usage: %(cmd)s [<exit_code>] - """ - CALLABLE = _execute_exit - OWN_USAGE = True - - def verify_options(self, options): - code = options['<exit_code>'] - if code is not None: - try: - int(code) - except ValueError: - raise errors.LmiInvalidOptions( - "<exit_code> must be an integer not \"%s\"" % code) - - def transform_options(self, options): - code = options.get('<exit_code>', None) - if code is None: - code = EXIT_CODE_SUCCESS - options['<exit_code>'] = int(code) - diff --git a/lmi/scripts/_metacommand/help.py b/lmi/scripts/_metacommand/help.py deleted file mode 100644 index b88f6b5..0000000 --- a/lmi/scripts/_metacommand/help.py +++ /dev/null @@ -1,101 +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 containing help command. -""" - -from lmi.scripts.common import errors -from lmi.scripts.common import get_logger -from lmi.scripts.common.command import LmiEndPointCommand -from lmi.scripts._metacommand import cmdutil -from lmi.scripts._metacommand import exit - -LOG = get_logger(__name__) - -class Help(LmiEndPointCommand): - """ - Print the list of supported commands with short description. - If a subcommand is given, its detailed help will be printed. - - Usage: %(cmd)s [<subcommand>...] - """ - OWN_USAGE = True - - def execute(self, subcommand): - mgr = self.app.command_manager - node = self.app.active_command - toplevel = self - while toplevel.parent is not None: - toplevel = toplevel.parent - - if node or subcommand: - # Help for some subcommand will be printed. - if node is None: - node = toplevel - if subcommand: - index = 0 - try: - while index < len(subcommand) and not node.is_end_point(): - while node.is_selector(): - cmd_factory, _ = node.select_cmds().next() - node = cmd_factory(self.app, node.cmd_name, - node.parent) - # selector may return either multiplexer or end-point - if node.is_end_point(): - break - cmd_factory = cmdutil.get_subcommand_factory(node, - subcommand[index]) - node = cmd_factory(self.app, subcommand[index], node) - index += 1 - except errors.LmiCommandNotFound as err: - LOG().error(str(err)) - if node is not toplevel: - self.app.stdout.write(node.get_usage(True)) - if node is self.app.active_command: - # show additional information only when no command given - self.app.stdout.write('\nTo get help for built-in commands,' - ' type:\n :help\n') - return exit.EXIT_CODE_SUCCESS - - # let's print the summary of available commands - self.app.stdout.write("Commands:\n") - max_cmd_len = max(len(n) for n in mgr) - cmd_line = " %%-%ds - %%s\n" % max_cmd_len - for cmd in sorted(mgr): - self.app.stdout.write(cmd_line - % (cmd, mgr[cmd].get_description() - .strip().split("\n", 1)[0])) - self.app.stdout.write( - "\nFor more informations about particular command type:\n" - " help <command>\n") - - return exit.EXIT_CODE_SUCCESS - diff --git a/lmi/scripts/_metacommand/interactive.py b/lmi/scripts/_metacommand/interactive.py deleted file mode 100644 index 349d622..0000000 --- a/lmi/scripts/_metacommand/interactive.py +++ /dev/null @@ -1,475 +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 interactive application. -""" - -import cmd -import docopt -import itertools -import os -import readline -import shlex - -from lmi.scripts.common import errors -from lmi.scripts.common import get_logger -from lmi.scripts.common.command import LmiBaseCommand, LmiCommandMultiplexer -from lmi.scripts._metacommand import cmdutil -from lmi.scripts._metacommand import exit - -LOG = get_logger(__name__) - -BUILT_INS_USAGE = """ -Built-ins. - -Usage: - : cd [<path>] - : .. - : pwd - : (help | -h | --help) - -Description: - cd Nest to a subcommand. Accepts a sequence of subcommands separated - with "/". If "/" is given, top-level command becomes the active one. - Other supported special symbols are ".." and ".". - .. Make parent command the active one. Same as issuing ":cd ..". - pwd Print current command path. - help Show this help. -""" - -BUILT_INS = ('..', 'cd', 'pwd', 'help') - -class LmiBuiltInError(errors.LmiError): - """ General exception concerning the use of built-in commands. """ - pass - -class LmiCanNotNest(LmiBuiltInError): - """ Raised upon invalid use of *cd* built-in command. """ - pass - -class Interactive(cmd.Cmd): - """ - Launched by the main application. It enters *shell* mode. Allows to launch - set of commands interactively on all given hosts. Session object stays the - same for the whole life of interactive mode. - - :param top_level_cmd: Top-level command. - :type top_level_cmd: :py:class:`.toplevel.TopLevelCommand` - """ - - def __init__(self, top_level_cmd): - self._top_level_cmd = top_level_cmd - cmd.Cmd.__init__(self, - stdin=top_level_cmd.app.stdin, stdout=top_level_cmd.app.stdout) - self._last_exit_code = exit.EXIT_CODE_SUCCESS - self.doc_header = 'Static commands' - self.app.active_command = top_level_cmd - self.load_history() - - # ************************************************************************* - # Properties - # ************************************************************************* - @property - def app(self): - """ :returns: Application object. """ - return self._top_level_cmd.app - - @property - def command_manager(self): - """ :returns: An instance of :py:class:`~.manager.CommandManager`. """ - return self.app.command_manager - - @property - def on_top_level_node(self): - """ - :returns: Whether the current node is a top-level one. In other words - we're not nested in any subcommand namespace. - :rtype: boolean - """ - return self._top_level_cmd is self.app.active_command - - @property - def prompt(self): - """ - :returns: Dynamically computed shell prompt. - :rtype: string - """ - if self.app.stdin.isatty(): - parents_num = 0 - node = self.app.active_command - while node.parent is not None: - parents_num += 1 - node = node.parent - return '>'*parents_num + self.app.active_command.cmd_name + '> ' - return '' - - # ************************************************************************* - # Private methods - # ************************************************************************* - def _change_to_node(self, path): - """ - Handles any command path. It constructs an object of command - corresponding to given path. Path can be absolute or relative. - - :param str path: Path to command. Looks similar to unix file path. - Command names are separated with ``'/'``. - :returns: Multiplexer command corresponding to path. - :rtype: :py:class:`~lmi.scripts.common.command.multiplexer.LmiCommandMultiplexer` - """ - cur_node = self.app.active_command - if path.startswith('/'): - if path.startswith('/lmi'): - path = path[4:] - else: - path = path[1:] - cur_node = self._top_level_cmd - - cmd_chain = os.path.normpath(path).split('/') - for subcmd in cmd_chain: - if subcmd == '..': - cur_node = ( cur_node.parent - if cur_node.parent is not None else cur_node) - elif subcmd and subcmd != '.': - if not subcmd in cmdutil.get_subcommand_names(cur_node): - raise LmiCanNotNest('No such subcommand "%s".' % - "/".join(cmd_chain)) - cmd_cls = cmdutil.get_subcommand_factory(cur_node, subcmd) - if not issubclass(cmd_cls, LmiCommandMultiplexer): - raise LmiCanNotNest('Can not nest to subcommand "%s" which' - " is not a multiplexer." % "/".join(cmd_chain)) - if not cmd_cls.has_own_usage(): - raise LmiCanNotNest('Can not nest to subcommand "%s" which' - ' lacks any help message.' % "/".join(cmd_chain)) - cur_node = cmd_cls(self.app, subcmd, cur_node) - self.app.active_command = cur_node - return exit.EXIT_CODE_SUCCESS - - def _do_built_in_cmd(self, args): - """ - Execute built-in command. - - :param list args: Command arguments including command name. - """ - options = docopt.docopt(BUILT_INS_USAGE, args, help=False) - if options['cd']: - path = '.' if not options.get('<path>', None) else options['<path>'] - return self._change_to_node(path) - elif options['..']: - return self._change_to_node('..') - elif options['pwd']: - self.app.stdout.write("/" - + "/".join(self.app.active_command.get_cmd_name_parts( - all_parts=True)) + "\n") - elif options['help'] or options['-h'] or options['--help']: - self.app.stdout.write(BUILT_INS_USAGE[1:]) - return exit.EXIT_CODE_SUCCESS - - def _execute_line_parts(self, line_parts): - """ - Try to execute given line. This method can throw various exceptions - that needs to be handled by a caller, otherwise interactive mode will - be terminated. - - :param list line_parts: Parsed command line arguments. - :returns: Command's exit code. - """ - if line_parts[0][0] == ':': - if len(line_parts[0]) > 1: - line_parts[0] = line_parts[0][1:] - else: - line_parts = line_parts[1:] - return self._do_built_in_cmd(line_parts) - - else: - # let's try to run registered subcommand - retval = self.run_subcommand(line_parts) - if isinstance(retval, bool) or not isinstance(retval, (int, long)): - retval = ( exit.EXIT_CODE_SUCCESS - if bool(retval) or retval is None - else exit.EXIT_CODE_FAILURE) - return retval - - # ************************************************************************* - # Public methods - # ************************************************************************* - def complete(self, text, state): - """ - Overrides parent's method so that registered commands can be also - completed. - """ - if state == 0: - import readline - origline = readline.get_line_buffer() - line = origline.lstrip() - stripped = len(origline) - len(line) - begidx = readline.get_begidx() - stripped - endidx = readline.get_endidx() - stripped - command, _, _ = self.parseline(line) - compfunc = self.completedefault - if command and hasattr(self, 'complete_' + command): - compfunc = getattr(self, 'complete_' + command) - self.completion_matches = compfunc(text, line, begidx, endidx) - try: - return self.completion_matches[state] - except IndexError: - return None - - def completedefault(self, text, line, *_args, **_kwargs): - """ - Tab-completion for commands known to the command manager and subcommands - in current command namespace. Does not handle command options. - """ - if line.startswith(':'): - commands = BUILT_INS - else: - commands = set(self.completenames(text)) - commands.update(set(cmdutil.get_subcommand_names(self.app.active_command))) - completions = sorted(n for n in commands - if not text or n.startswith(text)) - return completions - - def clear_history(self): - """ Clear readline history. """ - readline.clear_history() - - def default(self, line): - """ - This is run, when line contains unknown command to ``cmd.Cmd``. It - expects us to handle it if we know it, or print an error. - - :param str line: Line given to our shell. - """ - try: - line_parts = shlex.split(line) - except ValueError as err: - LOG().error(str(err)) - return exit.EXIT_CODE_INVALID_SYNTAX - - try: - self._execute_line_parts(line_parts) - - except docopt.DocoptExit as err: - # command found, but options given to it do not comply with its - # usage string - if '--help' in line_parts: - return self.do_help(" ".join( - line_parts[:line_parts.index('--help')])) - LOG().warn("Wrong options given: %s", line.strip()) - self.stdout.write(str(err)) - if ( line_parts[0] in cmdutil.get_subcommand_names( - self.app.active_command) - and cmdutil.get_subcommand_factory(self.app.active_command, - line_parts[0]).is_end_point()): - self.stdout.write("\n\nTo see a full usage string, type:\n" - " help %s\n" % line_parts[0]) - else: - self.stdout.write("\n\nTo see a list of available commands," - " type:\n help\n") - self._last_exit_code = exit.EXIT_CODE_FAILURE - - except errors.LmiCommandNotFound as err: - LOG().error(str(err)) - self._last_exit_code = exit.EXIT_CODE_COMMAND_NOT_FOUND - - except errors.LmiUnsatisfiedDependencies as err: - LOG().error(str(err)) - self._last_exit_code = exit.EXIT_CODE_UNSATISFIED_DEPENDENCIES - - except errors.LmiError as err: - LOG().error(str(err)) - self._last_exit_code = exit.EXIT_CODE_FAILURE - - except KeyboardInterrupt as err: - LOG().debug('%s: %s', err.__class__.__name__, str(err)) - self._last_exit_code = exit.EXIT_CODE_KEYBOARD_INTERRUPT - - return self._last_exit_code - - def do_EOF(self, _arg): #pylint: disable=C0103,R0201 - """ - Exit on End-Of-File if we are on top-level command. Otherwise change - to parent command. - """ - if self.app.stdin.isatty(): - self.app.stdout.write('\n') - if self.app.stdin.isatty() and not self.on_top_level_node: - self._change_to_node('..') - else: - raise errors.LmiTerminate(self._last_exit_code) - - def do_exit(self, arg): - """ - This makes the exit command work in both (non-)interactive modes. - """ - command = ["exit"] - if arg: - command.append(arg) - return self.run_subcommand(command) - - def do_help(self, arg): - """ Handle help subcommand. """ - if arg: - try: - arg_parts = shlex.split(arg) - except ValueError as err: - LOG().error(str(err)) - return exit.EXIT_CODE_INVALID_SYNTAX - - method_name = '_'.join( - itertools.chain(['do'], - itertools.takewhile(lambda x: not x.startswith('-'), - arg_parts))) - if hasattr(self, method_name): - return cmd.Cmd.do_help(self, arg) - else: - arg_parts = [] - - if ( self.on_top_level_node - and ( not arg - or ( len(arg_parts) == 1 - and arg not in self.app.command_manager.command_names))): - if not self.completenames(arg): - LOG().error(str(errors.LmiCommandNotFound(arg))) - cmd.Cmd.do_help(self, '') - else: - cmd.Cmd.do_help(self, arg) - cmd_names = set(self.command_manager) - cmd_names.difference_update(set(self.completenames(''))) - self.print_topics( - "Application commands (type help <topic>):", - sorted(cmd_names), 15, 80) - self.print_topics( - "Built-in commands (type :help):", - [':'+bi for bi in BUILT_INS], 15, 80) - return exit.EXIT_CODE_SUCCESS - - return self.run_subcommand(['help'] + arg_parts) - - def emptyline(self): #pylint: disable=R0201 - """ Do nothing for empty line. """ - pass - - def help_exit(self): - """ Provide help for exit command. """ - cur_node = self.app.active_command - # temporarily change to top level command where help cmd is registered - self.app.active_command = self._top_level_cmd - try: - return self.run_subcommand(["help", "exit"]) - finally: - self.app.active_command = cur_node - - def help_help(self): - """ - Use the command manager to get instructions for "help". - """ - cur_node = self.app.active_command - # temporarily change to top level command where help cmd is registered - self.app.active_command = self._top_level_cmd - try: - return self.run_subcommand(["help", "help"]) - finally: - self.app.active_command = cur_node - - def load_history(self): - """ - Load a readline history file. - """ - if ( self.app.config.history_max_length == 0 - or not os.path.exists(self.app.config.history_file)): - return - LOG().debug('Reading history file "%s"', self.app.config.history_file) - try: - readline.read_history_file(self.app.config.history_file) - if ( self.app.config.history_max_length > 0 - and readline.get_current_history_length() - > self.app.config.history_max_length): - readline.set_history_length(self.app.config.history_max_length) - readline.write_history_file(self.app.config.history_file) - readline.read_history_file(self.app.config.history_file) - except (IOError, OSError) as err: - LOG().warn('Failed to read history file "%s".', - self.app.config.history_file, exc_info=err) - - def postcmd(self, stop, _line): - """ - This is called after the ``do_*`` command to postprocess its result and - decide whether to stop the shell. We want to stop only when - :py:class:`lmi.scripts.common.errors.LmiError` is raised. This - exception is catched upwards in call chain. - - :returns: Whether to stop the shell. - :rtype: bool - """ - return False - - def run_subcommand(self, args): - """ - Run a subcommand given as a first item of ``args``. It must be one of - commands registered in manager. Returns the return value of invoked - command. - - :param list args: List of commands. - """ - if not isinstance(args, (list, tuple)): - raise TypeError("args must be a list") - if len(args) < 1: - raise ValueError("args must not be empty") - try: - cmd_factory = cmdutil.get_subcommand_factory( - self.app.active_command, args[0]) - parent = self.app.active_command - except errors.LmiCommandNotFound: - # When nested into a subcommand, let it handle the args if known. - # If not known, try one of static commands. - if not self.on_top_level_node and hasattr(self, 'do_' + args[0]): - cmd_factory = self.app.command_manager.find_command(args[0]) - parent = self._top_level_cmd - else: - raise - cmd_inst = cmd_factory(self.app, args[0], parent) - return cmd_inst.run(args[1:]) - - def save_history(self): - """ - Saves current history of commands into the history file. If the length - of history exceeds a maximum history file length, the history will be - truncated. - """ - if self.app.config.history_max_length != 0: - LOG().debug('Writing history file "%s"', - self.app.config.history_file) - if self.app.config.history_max_length > 0: - readline.set_history_length(self.app.config.history_max_length) - try: - readline.write_history_file(self.app.config.history_file) - except (IOError, OSError), err: - LOG().warn('Failed to write history file "%s".', - self.app.config.history_file, exc_info=err) diff --git a/lmi/scripts/_metacommand/manager.py b/lmi/scripts/_metacommand/manager.py deleted file mode 100644 index 773979b..0000000 --- a/lmi/scripts/_metacommand/manager.py +++ /dev/null @@ -1,182 +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> -# -""" -Manager module for direct subcommands of lmi metacommand. Most of them are -loaded from entry_points of installed python eggs. -""" - -import pkg_resources - -from lmi.scripts.common import Configuration -from lmi.scripts.common import errors -from lmi.scripts.common import get_logger -from lmi.scripts.common.command import base -from lmi.scripts.common.command import util - -LOG = get_logger(__name__) - -class _CustomCommandWrapper(object): - """ - Provide an interface mocking an entry_point object for custom commands - added by lmi metacommand application. - - :param name: (``str``) Name of command. - :param cmd_class: (``LmiBaseCommand``) Factory for custom commands. - """ - - def __init__(self, name, cmd_class): - if not isinstance(name, basestring): - raise TypeError("name must be a string") - if not issubclass(cmd_class, base.LmiBaseCommand): - raise TypeError("cmd_class must be a LmiBaseCommand") - self._name = name - self._cmd_class = cmd_class - - @property - def name(self): - """ Return command name. """ - return self._name - - def load(self): - """ Return command class. """ - return self._cmd_class - -class CommandManager(object): - """ - Manager of direct subcommands of lmi metacommand. It manages commands - registered with entry_points under particular namespace installed by - python eggs. Custom commands may also be added. - - :param namespace: (``str``) Namespace, where commands are registered. - For example ``lmi.scripts.cmd``. - """ - - def __init__(self, namespace=None): - if namespace is not None and not isinstance(namespace, basestring): - raise TypeError("namespace must be a string") - if namespace is None: - namespace = Configuration.get_instance().get_safe( - "Main", "CommandNamespace") - self._namespace = namespace - self._commands = {} - self._load_commands() - - @property - def command_names(self): - """ Returns list of command names. """ - return self._commands.keys() - - def __len__(self): - return len(self._commands) - - def __iter__(self): - """ Yields command names. """ - return iter(self._commands) - - def __getitem__(self, cmd_name): - """ Gets command factory for name. """ - return self.find_command(cmd_name) - - def _load_commands(self): - """ Loads commands from entry points under provided namespace. """ - def _add_entry_point(epoint): - """ - Convenience function taking an entry point, making some name - checks and adding it to registered commands. - """ - if not util.RE_COMMAND_NAME.match(epoint.name): - LOG().error('Invalid command name: %s, ignoring.', epoint.name) - return - if epoint.name in self._commands: - LOG().warn('Command "%s" already registered, ignoring.', - epoint.name) - else: - LOG().debug('Found registered command "%s".', epoint.name) - self._commands[epoint.name] = epoint - - for entry_point in pkg_resources.iter_entry_points(self._namespace): - if isinstance(entry_point, dict): - for epoint in entry_point.values(): - _add_entry_point(epoint) - else: - _add_entry_point(entry_point) - - - def add_command(self, name, cmd_class): - """ - Registers custom command. May be used for example for *help* command. - - :param name: (``str``) Name of command. - :param cmd_class: (``LmiBaseCommand``) Factory for commands. - """ - if not isinstance(name, basestring): - raise TypeError("name must be a string") - if not issubclass(cmd_class, base.LmiBaseCommand): - raise TypeError("cmd_class must be a LmiBaseCommand") - if not util.RE_COMMAND_NAME.match(name): - raise errors.LmiCommandInvalidName( - cmd_class.__module__, cmd_class.__class__.__name__, name) - if name in self._commands: - LOG().warn('Command "%s" already managed, overwriting with "%s:%s".', - name, cmd_class.__module__, cmd_class.__name__) - self._commands[name] = _CustomCommandWrapper(name, cmd_class) - - def find_command(self, cmd_name): - """ - Loads a command associated with given name and returns it. - - :param cmd_name: (``str``) Name of command to load. - :rtype: (``LmiBaseCommand``) Factory for commands. - """ - try: - return self._commands[cmd_name].load() - except KeyError: - raise errors.LmiCommandNotFound(cmd_name) - except ImportError as err: - LOG().debug('Failed to import command "%s".', cmd_name, exc_info=err) - raise errors.LmiCommandImportError( - cmd_name, self._commands[cmd_name].module_name, err) - - def reload_commands(self, keep_custom=True): - """ - Flushes all commands and reloads entry points. - - :param keep_custom: (``bool``) Custom commands -- not loaded from - entry points -- are preserved. - """ - if keep_custom: - keep = { n: c for n, c in self._commands.items() - if isinstance(c, _CustomCommandWrapper)} - else: - keep = {} - self._commands = {} - self._load_commands() - self._commands.update(keep) - diff --git a/lmi/scripts/_metacommand/toplevel.py b/lmi/scripts/_metacommand/toplevel.py deleted file mode 100644 index 1e654a3..0000000 --- a/lmi/scripts/_metacommand/toplevel.py +++ /dev/null @@ -1,184 +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 the root command (``lmi`` binary). -""" - -USAGE_STRING = \ -""" -OpenLMI command line interface for CIM providers. It's functionality is -composed of registered subcommands, operating on top of simple libraries, -interfacing with particular OpenLMI profile providers. -Works also in interactive mode which is entered, when <command> argument is -omitted. - -Usage: - %(cmd)s [options] [(--trace | --notrace)] [-v]... [-h <host>]... - <command> [<args> ...] - %(cmd)s [options] [(--trace | --notrace)] [-v]... [-h <host>]... - %(cmd)s (--help | --version) - -Options: - -c --config-file <config> Path to a user configuration file. Options - specified here override any settings of global - configuration file. - -h --host <host> Hostname of target system. - --hosts-file <hosts> Path to a file containing target hostnames. - Each hostname must be listed on a single line. - --user <user> Username used in connection to any target host. - --same-credentials Use the first credentials given for all hosts. - -n --noverify Do not verify cimom's ssl certificate. - -v Increase verbosity of output. - --trace Show tracebacks on errors. - --notrace Suppress tracebacks for exceptions. - -q --quiet Supress output except for errors. - --log-file <log_file> Output file for logging messages. - --namespace <namespace> Default CIM namespace to use. - -N --no-headings Don't print table headings. - -H --human-friendly Print large values in human friendly units (i.e. - MB, GB, TB etc.) - -L --lister-format (table | csv) - Print output of lister commands in CSV or table - format. CSV format is more suitable for machine - processing. Defaults to table. - --help Show this text and quit. - --version Print version of '%(cmd)s' in use and quit. - -Handling hosts: - If no --host or --hosts-file given the "localhost" is tried. When running - under root with Pegasus CIMOM, this results in a connection over unix - socket (without the need for credentials). - - Hosts may contain embedded credentials e.g.: - http://user:passwd@hostname:5988 - Avoid supplying them on command line though since arguments are visible in - process table. Use --hosts-file option instead. -""" - -import docopt - -from lmi.scripts._metacommand import exit -from lmi.scripts._metacommand import util -from lmi.scripts._metacommand import Interactive -from lmi.scripts.common import get_logger -from lmi.scripts.common import errors -from lmi.scripts.common.command import base - -LOG = get_logger(__name__) - -class TopLevelCommand(base.LmiBaseCommand): - """ - Top level (instance, without any parent) command handling application - parameters and passing work to registered subcommands. - """ - - @classmethod - def has_own_usage(cls): - return True - - @classmethod - def child_commands(cls): - return [] - - @classmethod - def is_end_point(cls): - return False - - def __init__(self, app, cmd_name='lmi'): - base.LmiBaseCommand.__init__(self, app, cmd_name) - - def get_usage(self, proper=False): - return USAGE_STRING[1:] % { 'cmd' : " ".join(self.cmd_name_parts) } - - def run_subcommand(self, cmd_name, args): - """ - Finds a command factory, instantiates it and passes the control. - """ - cmd_factory = self.app.command_manager[cmd_name] - cmd = cmd_factory(self.app, cmd_name, parent=self) - return cmd.run(args) - - def start_interactive_mode(self): - """ Run the command line loop of interactive application. """ - self.app.command_manager.add_command("exit", exit.Exit) - iapp = Interactive(self) - while True: - try: - ret = iapp.cmdloop() - break - except errors.LmiTerminate as err: - ret = err.args[0] - break - except KeyboardInterrupt as err: - LOG().debug('%s: %s', err.__class__.__name__, str(err)) - self.app.stdout.write('\n') - iapp.save_history() - return ret - - def run(self, args): - """ - Handle program arguments, set up the application and call - a subcommand or enter interactive mode. Return exit code. - - :param args: (``list``) Arguments without the binary name. - """ - if not isinstance(args, (tuple, list)): - raise TypeError("args must be a list") - try: - options = docopt.docopt(self.get_usage(), args, - version=util.get_version(), help=False, options_first=True) - except docopt.DocoptLanguageError as exc: - self.app.stderr.write("%s\n" % str(exc)) - return exit.EXIT_CODE_FAILURE - if options.pop('--help', False): - self.app.stdout.write(self.get_usage()) - self.app.stdout.write("\nCommands:\n") - self.app.stdout.write(" %s\n" % " ".join( - n for n in sorted(self.app.command_manager))) - return exit.EXIT_CODE_SUCCESS - if options.pop('--version', False): - self.app.print_version() - return exit.EXIT_CODE_SUCCESS - self.app.setup(options) - if options['<command>'] is None: - return self.start_interactive_mode() - - try: - LOG().debug('Running command "%s".', options['<command>']) - return self.run_subcommand(options['<command>'], options['<args>']) - except docopt.DocoptExit as err: - if '--help' in args: - cmd_args = options['<args>'] - cmd_args = cmd_args[:cmd_args.index('--help')] - return self.run_subcommand( - 'help', [options['<command>']] + cmd_args) - raise - - diff --git a/lmi/scripts/_metacommand/util.py b/lmi/scripts/_metacommand/util.py deleted file mode 100644 index 15686a3..0000000 --- a/lmi/scripts/_metacommand/util.py +++ /dev/null @@ -1,264 +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-command utility module. -""" - -import logging -import logging.config -import os -import pkg_resources -import platform -import re -import socket -import sys -import urlparse - -from lmi.scripts.common import Configuration -from lmi.scripts.common import get_logger -from lmi.scripts.common import lmi_logging - -PYTHON_EGG_NAME = "openlmi-scripts" -#: Service name identifying tcp port used to connect to CIMOM. -DEFAULT_BROKER_SERVICE_NAME = 'wbem-https' - -RE_NETLOC = re.compile(r'^((?P<username>[^:@]+)(:(?P<password>[^@]+))?@)?' - r'(?P<hostname>[^:]+)(:(?P<port>\d+))?$') - -VERBOSITY_2_LOG_LEVEL = { - Configuration.OUTPUT_SILENT : logging.ERROR, - Configuration.OUTPUT_WARNING : logging.WARNING, - Configuration.OUTPUT_INFO : logging.INFO, - Configuration.OUTPUT_DEBUG : logging.DEBUG, -} - -DEFAULT_LOGGING_CONFIG = { - 'version' : 1, - 'disable_existing_loggers': True, - 'formatters' : { - 'console' : { - "()" : "lmi.scripts.common.lmi_logging.LevelDispatchingFormatter", - "formatters" : { - logging.INFO : - Configuration.default_options()['ConsoleInfoFormat'] - }, - 'default': Configuration.default_options()['ConsoleFormat'], - 'datefmt' : '%Y-%m-%d %H:%M:%S' - }, - 'file' : { - 'format' : Configuration.default_options()['FileFormat'], - 'datefmt' : '%Y-%m-%d %H:%M:%S' - } - }, - 'handlers' : { - 'console': { - 'class' : "logging.StreamHandler", - 'level' : logging.ERROR, - 'formatter': 'console', - }, - 'console_shell': { - 'class' : "logging.StreamHandler", - 'level' : logging.CRITICAL, - 'formatter': 'console', - }, - 'file' : { - 'class' : "logging.FileHandler", - 'level' : Configuration.default_options()['Level'].upper(), - 'formatter': 'file', - } - }, - 'loggers' : { - 'lmi.shell' : { - 'handlers' : ['console_shell'], - 'level' : logging.DEBUG, - 'propagate' : False - } - }, - 'root' : { - 'level' : logging.DEBUG, - 'handlers' : ['console'] - } -} - -LOG = get_logger(__name__) - -def setup_logging(app_config, stderr=sys.stderr): - """ - Setup logging to console and optionally to the file. - - :param app_config: (``lmi.scripts.common.Configuration``) - Configuration object. - :param stderr: (``file``) Output stream, where console handler should - dispatch logging messages. - """ - cfg = DEFAULT_LOGGING_CONFIG.copy() - - # Set up logging to a file - if app_config.log_file: - cfg['handlers']['file']['filename'] = app_config.log_file - cfg['formatters']['file']['format'] = app_config.get_safe( - 'Log', 'FileFormat', raw=True) - try: - cfg['handlers']['file']['level'] = \ - getattr(logging, app_config.logging_level.upper()) - except KeyError: - LOG().error('Unsupported logging level: "%s".', - app_config.logging_level) - cfg['root']['handlers'].append('file') - cfg['loggers']['lmi.shell']['handlers'].append('file') - else: - del cfg['formatters']['file'] - del cfg['handlers']['file'] - - if app_config.get_safe('Log', "LogToConsole", bool): - # Set up logging to console - if stderr is not sys.stderr: - cfg['handlers']['console']['stream'] = stderr - cfg['handlers']['console']['level'] = \ - VERBOSITY_2_LOG_LEVEL[app_config.verbosity] - - # make the verbosity of lmi shell one level less - lmi_shell_verbosity = app_config.verbosity - if ( app_config.verbosity < app_config.OUTPUT_DEBUG - and app_config.verbosity > app_config.OUTPUT_SILENT): - lmi_shell_verbosity -= 1 - cfg['handlers']['console_shell']['level'] = \ - VERBOSITY_2_LOG_LEVEL[lmi_shell_verbosity] - - # use ConsoleInfoFormat for INFO and less severe levels - cfg['formatters']['console']['formatters'] = { - logging.INFO : - app_config.get_safe('Log', 'ConsoleInfoFormat', raw=True) - } - # use ConsoleFormat for any other level - cfg['formatters']['console']['default'] = app_config.get_safe( - 'Log', 'ConsoleFormat', raw=True) - else: - cfg['root']['handlers'].remove('console') - cfg['loggers']['lmi.shell']['handlers'].remove('console_shell') - del cfg['handlers']['console'] - del cfg['handlers']['console_shell'] - - use_colors = platform.system() != 'Windows' and stderr.isatty() - lmi_logging.setup_logger(use_colors = use_colors) - logging.config.dictConfig(cfg) - -def get_version(egg_name=PYTHON_EGG_NAME): - """ - Gets version string of any python egg. Defaults to the egg of current - application. - """ - return pkg_resources.get_distribution(egg_name).version - -def get_hosts_credentials(hostnames): - """ - Parse list of hostnames, get credentials out of them and return - ``(hostnames, creds)``, where ``hostnames`` is a list of ``hostnames`` - with credentials removed and ``creds`` is a dictionary with a pair - ``(username, password)`` for every hostname, that supplied it. - - :param hostnames: (``list``) List of hostnames with optional credentials. - For example: ``http://root:password@hostname:5988``. - """ - if not hasattr(hostnames, '__iter__'): - raise TypeError("hostnames must be a list of hosts") - new_hostnames = [] - credentials = {} - for hostname in hostnames: - parsed = urlparse.urlparse(hostname) - if not parsed.netloc and parsed.path: - # got something like [user[:pass]@]hostname[:port] (no scheme) - match = RE_NETLOC.match(hostname) - if match: - hostname = match.group('hostname') - if match.group('port'): - hostname += ':' + match.group('port') - if match.group('username') or match.group('password'): - credentials[hostname] = ( - match.group('username'), match.group('password')) - elif parsed.username or parsed.password: - hostname = parsed.scheme - if parsed.scheme: - hostname += "://" - hostname += parsed.hostname - if parsed.port: - hostname += ":" + str(parsed.port) - hostname += parsed.path - credentials[hostname] = (parsed.username, parsed.password) - new_hostnames.append(hostname) - return (new_hostnames, credentials) - -def parse_hosts_file(hosts_file): - """ - Parse file with hostnames to connect to. Return list of parsed hostnames. - - :param hosts_file: (``file``) File object openned for read. - It containes hostnames. Each hostname occupies single line. - :rtype: (``tuple``) A pair of ``(hosts, creds)``, where ``hosts`` is a list - of string with hostnames and ``creds`` is a dictionary mapping - ``(username, password)`` to each hostname if supplied. - """ - hostnames = [] - for line in hosts_file.readlines(): - hostnames.append(line.strip()) - return get_hosts_credentials(hostnames) - -def get_default_hostname(port=None): - """ - Choose default hostname to connect to. If logged as root, this will default - to localhost, which results in unix socket being used for connection. - Otherwise use full qualified domain name and hostname will be tried in this - order. If they are not address-resolvable, '127.0.0.1' is returned. - - This function shall be used only if no uri is specified on command line. - - :param port: Port of desired service running on host (CIMOM broker). - This defaults to :py:attr:`DEFAULT_BROKER_SERVICE_NAME` - :type port: string or int - """ - if port is None: - port = DEFAULT_BROKER_SERVICE_NAME - # Functions used to get hostname. first resolvable result will be used. - name_getters = [] - if os.getuid() != 0: - # Use non-'localhost' name only if we'are not logged in as root - # for it prevents the use of unix socket. - name_getters.extend([socket.getfqdn, socket.gethostname]) - name_getters.append(lambda: 'localhost') - for name_func in name_getters: - try: - hostname = name_func() - socket.getaddrinfo(hostname, port) - break - except socket.gaierror: - pass - else: - hostname = '127.0.0.1' - return hostname diff --git a/lmi/scripts/common/__init__.py b/lmi/scripts/common/__init__.py deleted file mode 100644 index cda4266..0000000 --- a/lmi/scripts/common/__init__.py +++ /dev/null @@ -1,73 +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> -# -""" -Package with client-side python modules and command line utilities. -""" - -import logging - -from lmi.shell import LMINamespace -from lmi.shell import LMIExceptions -from lmi.scripts.common.configuration import Configuration -from lmi.scripts.common.lmi_logging import get_logger - -LOG = get_logger(__name__) - -def get_computer_system(ns): - """ - Obtain an instance of ``CIM_ComputerSystem`` or its subclass. Preferred - class name can be configured in configuration file. If such class does - not exist, a base class (``CIM_ComputerSystem``) is enumerated instead. - First feasible instance is cached and returned. - - :param ns: Namespace object where to look for computer system class. - :type ns: :py:class:`lmi.shell.LMINamespace` - :returns: Instance of ``CIM_ComputerSystem``. - :rtype: :py:class:`lmi.shell.LMIInstance`. - """ - if not isinstance(ns, LMINamespace): - raise TypeError("ns must be an instance of LMINamespace") - if not hasattr(get_computer_system, '_cs_cache'): - get_computer_system._cs_cache = {} - ns_path = ns.connection.uri + '/' + ns.name - if not ns_path in get_computer_system._cs_cache: - config = Configuration.get_instance() - try: - get_computer_system._cs_cache[ns_path] = cs = \ - getattr(ns, config.system_class_name).first_instance() - except LMIExceptions.LMIClassNotFound: - LOG().warn('Failed to get instance of %s on host "%s"' - ' - falling back to CIM_ComputerSystem.', - config.system_class_name, ns.connection.uri) - get_computer_system._cs_cache[ns_path] = cs = \ - ns.CIM_ComputerSystem.first_instance_name() - LOG().debug('Loaded instance of %s:%s for host "%s".', - ns.name, cs.classname, ns.connection.uri) - return get_computer_system._cs_cache[ns_path] 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 diff --git a/lmi/scripts/common/configuration.py b/lmi/scripts/common/configuration.py deleted file mode 100644 index 97ebebe..0000000 --- a/lmi/scripts/common/configuration.py +++ /dev/null @@ -1,311 +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 for Configuration class. - -Configuration ---------------------- - -.. autoclass:: Configuration - :members: - -""" - -import os -from lmi.base.BaseConfiguration import BaseConfiguration - -LISTER_FORMATS = ['csv', 'table'] - -#: Default format string to use in stderr handlers. -DEFAULT_FORMAT_STRING = "%(cseq)s%(levelname_)-8s:%(creset)s %(message)s" - -class Configuration(BaseConfiguration): - """ - Configuration class specific to software providers. - *OpenLMI* configuration file should reside in: :: - - /etc/openlmi/scripts/lmi.conf - - :param string user_config_file_path: Path to the user configuration - options. - """ - - USER_CONFIG_FILE_PATH = "~/.lmirc" - HISTORY_FILE = "~/.lmi_history" - - OUTPUT_SILENT = -1 - OUTPUT_WARNING = 0 - OUTPUT_INFO = 1 - OUTPUT_DEBUG = 2 - - # indexes to LISTER_FORMATS - LISTER_FORMAT_CSV = 0 - LISTER_FORMAT_TABLE = 1 - - def __init__(self, user_config_file_path=USER_CONFIG_FILE_PATH, **kwargs): - self._user_config_file_path = os.path.expanduser(user_config_file_path) - BaseConfiguration.__init__(self, **kwargs) - self._verbosity = None - self._trace = None - self._verify_server_cert = None - self._cim_namespace = None - self._human_friendly = None - self._lister_format = None - self._no_headings = None - self._log_file = None - self._history_max_length = None - - @classmethod - def provider_prefix(cls): - return "scripts" - - @classmethod - def default_options(cls): - """ - :returns: Dictionary of default values. - :rtype: dictionary - """ - defaults = BaseConfiguration.default_options().copy() - # [Main] options - defaults["CommandNamespace"] = 'lmi.scripts.cmd' - defaults["Trace"] = "False" - defaults["Verbosity"] = "0" - defaults["HistoryMaxLength"] = "4000" - # [Log] options - defaults['ConsoleFormat'] = DEFAULT_FORMAT_STRING - defaults['ConsoleInfoFormat'] = '%(message)s' - defaults['FileFormat'] = \ - "%(asctime)s:%(levelname)-8s:%(name)s:%(lineno)d - %(message)s" - defaults['LogToConsole'] = 'True' - defaults['OutputFile'] = '' - # [SSL] options - defaults['VerifyServerCertificate'] = 'True' - # [Format] options - defaults['HumanFriendly'] = 'False' # be ugly by default - defaults['ListerFormat'] = 'table' - defaults['NoHeadings'] = 'False' - return defaults - - @classmethod - def mandatory_sections(cls): - sects = set(BaseConfiguration.mandatory_sections()) - sects.add('Main') - sects.add('SSL') - sects.add('Format') - return list(sects) - - def load(self): - """ Read additional user configuration file if it exists. """ - BaseConfiguration.load(self) - self.config.read(self._user_config_file_path) - - # ************************************************************************* - # [CIM] options - # ************************************************************************* - @property - def namespace(self): - if self._cim_namespace is None: - return BaseConfiguration.namespace.fget(self) - return self._cim_namespace - @namespace.setter - def namespace(self, namespace): - if not isinstance(namespace, basestring) and namespace is not None: - raise TypeError("namespace must be a string") - self._cim_namespace = namespace - - # ************************************************************************* - # [Main] options - # ************************************************************************* - @property - def history_file(self): - """ Path to a file with history of interactive mode. """ - return os.path.expanduser(self.HISTORY_FILE) - - @property - def history_max_length(self): - """ Maximum number of lines kept in history file. """ - return self.get_safe('Main', 'HistoryMaxLength', int) - - @property - def silent(self): - """ Whether to suppress all output messages except for errors. """ - return self.verbosity <= self.OUTPUT_SILENT - - @property - def trace(self): - """ Whether the tracebacks shall be printed upon errors. """ - if self._trace is not None: - return self._trace - return self.get_safe('Main', 'Trace', bool) - - @trace.setter - def trace(self, trace): - """ Allow to override configuration option Trace. """ - if trace is not None: - trace = bool(trace) - self._trace = trace - - @property - def verbose(self): - """ Whether to output more details. """ - return self.verbosity >= self.OUTPUT_INFO - - @property - def verbosity(self): - """ Return integer indicating verbosity level of output to console. """ - if self._verbosity is None: - return self.get_safe('Main', 'Verbosity', int) - return self._verbosity - - @verbosity.setter - def verbosity(self, level): - """ Allow to set verbosity without modifying configuration values. """ - if not isinstance(level, (long, int)) and level is not None: - raise TypeError("level must be integer") - if level is not None: - if level < self.OUTPUT_SILENT: - level = self.OUTPUT_SILENT - elif level > self.OUTPUT_DEBUG: - level = self.OUTPUT_DEBUG - self._verbosity = level - - # ************************************************************************* - # [Log] options - # ************************************************************************* - @property - def log_file(self): - """ Path to a file, where logging messages shall be written. """ - if self._log_file is None: - return self.get_safe('Log', 'OutputFile') - return self._log_file - @log_file.setter - def log_file(self, log_file): - """ Override logging file path. """ - if log_file is not None and not isinstance(log_file, basestring): - raise TypeError("log_file must be a string") - self._log_file = log_file - - # ************************************************************************* - # [SSL] options - # ************************************************************************* - @property - def verify_server_cert(self): - """ - Return boolean saying, whether the server-side certificate should be - checked. - """ - if self._verify_server_cert is None: - return self.get_safe('SSL', 'VerifyServerCertificate', bool) - return self._verify_server_cert - @verify_server_cert.setter - def verify_server_cert(self, value): - """ Allows to override configuration option value. """ - if value is not None: - value = bool(value) - self._verify_server_cert = value - - # ************************************************************************* - # [Format] options - # ************************************************************************* - @property - def human_friendly(self): - """ Whether to print human-friendly values. """ - if self._human_friendly is None: - return self.get_safe('Format', 'HumanFriendly', bool) - return self._human_friendly - @human_friendly.setter - def human_friendly(self, value): - """ Allows to override configuration option value. """ - if value is not None: - value = bool(value) - self._human_friendly = value - - @property - def lister_format(self): - """ - Output format used for lister commands. Returns one of - * LISTER_FORMAT_CSV - * LISTER_FORMAT_TABLE - - :rtype: integer - """ - if self._lister_format is None: - value = self.get_safe('Format', 'ListerFormat') - try: - return LISTER_FORMATS.index(value.lower()) - except ValueError: - value = self.default_options()['ListerFormat'] - return LISTER_FORMATS.index(value.lower()) - return self._lister_format - @lister_format.setter - def lister_format(self, value): - """ - Allows to override configuration option. - - :param value: One of items from ``LISTER_FORMATS`` array or an index - to it. - :type value: integer or string - """ - if ( value is not None - and ( not isinstance(value, int) - or (value < 0 or value >= len(LISTER_FORMATS))) - and ( not isinstance(value, basestring) - or value.lower() not in LISTER_FORMATS)) : - raise TypeError("value must be an integer or one of: %s" % - LISTER_FORMATS) - if isinstance(value, basestring): - value = LISTER_FORMATS.index(value.lower()) - self._lister_format = value - - @property - def no_headings(self): - """ Whether to print headings of tables. """ - if self._no_headings is None: - return self.get_safe('Format', 'NoHeadings', bool) - return self._no_headings - @no_headings.setter - def no_headings(self, value): - """ Allows to override configuration option. """ - if value is not None: - value = bool(value) - self._no_headings = value - -# There were some path changes in BaseConfiguration after 0.2.0 release. -# Let's fallback to older variable name when the new one is not present. -if hasattr(BaseConfiguration, 'CONFIG_DIRECTORY_TEMPLATE_PROVIDER'): - setattr( Configuration - , 'CONFIG_FILE_PATH_TEMPLATE_PROVIDER' - , getattr(BaseConfiguration, 'CONFIG_DIRECTORY_TEMPLATE_PROVIDER') - + 'lmi.conf') -else: # fallback - setattr( Configuration - , 'CONFIG_FILE_PATH_TEMPLATE' - , getattr(BaseConfiguration, 'CONFIG_DIRECTORY_TEMPLATE') - + 'lmi.conf') diff --git a/lmi/scripts/common/errors.py b/lmi/scripts/common/errors.py deleted file mode 100644 index 5162cf8..0000000 --- a/lmi/scripts/common/errors.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> -# -""" -Module with predefined exceptions for use in scripts. -""" - -class LmiError(Exception): - """ - The base Lmi scripts error. - All the other exceptions inherit from it. - """ - pass - -class LmiFailed(LmiError): - """ - Raised, when operation on remote host failes. - It's supposed to be used especially in command libraries. - """ - pass - -class LmiUnsatisfiedDependencies(LmiFailed): - """ - Raised when no guarded command in - :py:class:`~.command.select.LmiSelectCommand` can be loaded due to - unsatisfied requirements. - """ - def __init__(self, uris): - LmiFailed.__init__(self, "Profile and class dependencies were not" - " satisfied for this session (%s)." % ', '.join(uris)) - -class LmiUnexpectedResult(LmiError): - """ - Raised, when command's associated function returns something unexpected. - """ - def __init__(self, command_class, expected, result): - LmiError.__init__(self, - 'Got unexpected result from associated function of' - ' "%s.%s", expected "%s", got: "%s".' % - (command_class.__module__, command_class.__name__, - expected, repr(result))) - -class LmiInvalidOptions(LmiError): - """ - Raised in :py:meth:`~.command.endpoint.LmiEndPointCommand.verify_options` - method if the options given are not valid. - """ - pass - -class LmiCommandNotFound(LmiError): - """ Raised, when user requests not registered command. """ - def __init__(self, cmd_name): - LmiError.__init__(self, 'No such command "%s".' % cmd_name) - -class LmiNoConnections(LmiError): - """ Raised, when no connection to remote hosts could be made. """ - pass - -class LmiCommandImportError(LmiError): - """ Exception raised when command can not be imported. """ - def __init__(self, cmd_name, cmd_path, reason): - LmiError.__init__(self, 'Failed to import command "%s" (%s): %s' % ( - cmd_name, cmd_path, reason)) - -class LmiCommandError(LmiError): - """ Generic exception related to command declaration. """ - def __init__(self, module_name, class_name, msg): - LmiError.__init__(self, 'Wrong declaration of command "%s": %s' - % ( ".".join([module_name, class_name]) - if module_name else class_name - , msg)) - -class LmiCommandInvalidName(LmiCommandError): - """ Raised, when command gets invalid name. """ - def __init__(self, module_name, class_name, cmd_name): - LmiCommandError.__init__(self, module_name, class_name, - 'Invalid command name "%s".' % cmd_name) - -class LmiCommandMissingCallable(LmiCommandError): - """ Raised, when command declaration is missing callable object. """ - def __init__(self, module_name, class_name): - LmiCommandError.__init__(self, module_name, class_name, - 'Missing CALLABLE property.') - -class LmiCommandInvalidProperty(LmiCommandError): - """ Raised, when any command property contains unexpected value. """ - pass - -class LmiImportCallableFailed(LmiCommandInvalidProperty): - """ Raised, when callable object of command could not be imported. """ - def __init__(self, module_name, class_name, callable_prop): - LmiCommandInvalidProperty.__init__(self, module_name, class_name, - 'Failed to import callable "%s".' % callable_prop) - -class LmiCommandInvalidCallable(LmiCommandInvalidProperty): - """ Raised, when given callback is not callable. """ - def __init__(self, module_name, class_name, msg): - LmiCommandInvalidProperty.__init__(self, module_name, class_name, msg) - -class LmiBadSelectExpression(LmiCommandError): - """ - Raised, when expression of :py:class:`~.command.select.LmiSelectCommand` - could not be evaluated. - """ - def __init__(self, module_name, class_name, expr): - LmiCommandError.__init__(self, module_name, class_name, - "Bad select expression: %s" % expr) - -class LmiTerminate(Exception): - """ - Raised to cleanly terminate interavtive shell. - """ - def __init__(self, exit_code=0): - Exception.__init__(self, exit_code) diff --git a/lmi/scripts/common/formatter/__init__.py b/lmi/scripts/common/formatter/__init__.py deleted file mode 100644 index db7b7e3..0000000 --- a/lmi/scripts/common/formatter/__init__.py +++ /dev/null @@ -1,484 +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> -# -""" -Subpackage with formatter classes used to render and output results. - -Each formatter has a :py:meth:`Formatter.produce_output` method taking one -argument which gets rendered and printed to output stream. Each formatter -expects different argument, please refer to doc string of particular class. -""" - -import itertools -import locale -import os -import pywbem - -from lmi.scripts.common import errors -from lmi.scripts.common.formatter import command as fcmd - -def get_terminal_width(): - """ - Get the number of columns of current terminal if attached to it. It - defaults to 79 characters. - - :returns: Number of columns of attached terminal. - :rtype: integer - """ - try: - term_cols = int(os.popen('stty size', 'r').read().split()[1]) - except (IOError, OSError, ValueError): - term_cols = 79 # fallback - return term_cols - -class Formatter(object): - """ - Base formatter class. - - It produces string representation of given argument and prints it. - - This formatter supports following commands: - - * :py:class:`~.command.NewHostCommand`. - - :param file stream: Output stream. - :param integer padding: Number of leading spaces to print at each line. - :param boolean no_headings: If table headings should be omitted. - """ - - def __init__(self, stream, padding=0, no_headings=False): - if not isinstance(padding, (int, long)): - raise TypeError("padding must be an integer") - if padding < 0: - padding = 0 - self.out = stream - self.padding = padding - self.no_headings = no_headings - #: counter of hosts printed - self.host_counter = 0 - #: counter of tables produced for current host - self.table_counter = 0 - #: counter of lines producted for current table - self.line_counter = 0 - - @property - def encoding(self): - """ - Try to determine encoding for output terminal. - - :returns: Encoding used to encode unicode strings. - :rtype: string - """ - enc = getattr(self.out, 'encoding') - if not enc: - enc = locale.getpreferredencoding() - return enc - - def render_value(self, val): - """ - Rendering function for single value. - - :param val: Any value to render. - :returns: Value converted to its string representation. - :rtype: str - """ - if isinstance(val, unicode): - return val.encode(self.encoding) - if not isinstance(val, str): - val = str(val) - return val - - def print_line(self, line, *args, **kwargs): - """ - Prints single line. Output message is prefixed with ``padding`` spaces, - formated and printed to output stream. - - :param string line: Message to print, it can contain markers for - other arguments to include according to ``format_spec`` language. - Please refer to ``Format Specification Mini-Language`` in python - documentation. - :param list args: Positional arguments to ``format()`` function of - ``line`` argument. - :param dictionary kwargs: Keyword arguments to ``format()`` function. - """ - line = ' ' * self.padding + line.format(*args, **kwargs) - self.out.write(line.encode(self.encoding)) - self.out.write("\n") - self.line_counter += 1 - - def print_host(self, hostname): - """ - Prints header for new host. - - :param string hostname: Hostname to print. - """ - if ( self.host_counter > 0 - or self.table_counter > 0 - or self.line_counter > 0): - self.out.write("\n") - term_width = get_terminal_width() - self.out.write("="*term_width + "\n") - self.out.write("Host: %s\n" % hostname) - self.out.write("="*term_width + "\n") - self.host_counter += 1 - self.table_counter = 0 - self.line_counter = 0 - - def produce_output(self, data): - """ - Render and print given data. - - Data can be also instance of - :py:class:`~.command.FormatterCommand`, see - documentation of this class for list of - allowed commands. - - This shall be overridden by subclasses. - - :param data: Any value to print. Subclasses may specify their - requirements for this argument. It can be also am instance of - :py:class:`~.command.FormatterCommand`. - """ - self.print_line(str(data)) - self.line_counter += 1 - -class ListFormatter(Formatter): - """ - Base formatter taking care of list of items. It renders input data in a - form of table with mandatory column names at the beginning followed by - items, one occupying single line (row). - - This formatter supports following commands: - * :py:class:`~.command.NewHostCommand` - * :py:class:`~.command.NewTableCommand` - * :py:class:`~.command.NewTableHeaderCommand` - - The command must be provided as content of one row. This row is then not - printed and the command is executed. - - This class should be subclassed to provide nice output. - """ - def __init__(self, stream, padding=0, no_headings=False): - super(ListFormatter, self).__init__(stream, padding, no_headings) - self.column_names = None - - def print_text_row(self, row): - """ - Print data row without any header. - - :param tuple row: Data to print. - """ - self.out.write(" "*self.padding + self.render_value(row) + "\n") - self.line_counter += 1 - - def print_row(self, data): - """ - Print data row. Optionaly print header, if requested. - - :param tuple data: Data to print. - """ - if self.line_counter == 0 and not self.no_headings: - self.print_header() - self.print_text_row(data) - - def print_table_title(self, title): - """ - Prints title of next tale. - - :param string title: Title to print. - """ - if self.table_counter > 0 or self.line_counter > 0: - self.out.write('\n') - self.out.write("%s:\n" % title) - self.table_counter += 1 - self.line_counter = 0 - - def print_header(self): - """ Print table header. """ - if self.no_headings: - return - if self.column_names: - self.print_text_row(self.column_names) - - def produce_output(self, rows): - """ - Prints list of rows. - - There can be a :py:class:`~.command.FormatterCommand` instance instead - of a row. See documentation of this class for list of allowed commands. - - :param rows: List of rows to print. - :type rows: list, generator or :py:class:`.command.FormatterCommand` - """ - for row in rows: - if isinstance(row, fcmd.NewHostCommand): - self.print_host(row.hostname) - elif isinstance(row, fcmd.NewTableCommand): - self.print_table_title(row.title) - elif isinstance(row, fcmd.NewTableHeaderCommand): - self.column_names = row.columns - else: - self.print_row(row) - -class TableFormatter(ListFormatter): - """ - Print nice human-readable table to terminal. - - To print the table nicely aligned, the whole table must be populated first. - Therefore this formatter stores all rows locally and does not print - them until the table is complete. Column sizes are computed afterwards - and the table is printed at once. - - This formatter supports following commands: - * :py:class:`~.command.NewHostCommand` - * :py:class:`~.command.NewTableCommand` - * :py:class:`~.command.NewTableHeaderCommand` - - The command must be provided as content of one row. This row is then not - printed and the command is executed. - """ - def __init__(self, stream, padding=0, no_headings=False): - super(TableFormatter, self).__init__(stream, padding, no_headings) - self.stash = [] - - def print_text_row(self, row, column_sizes): - for i in xrange(len(row)): - size = column_sizes[i] - # Convert to unicode to compute correct length of utf-8 strings - # (e.g. with fancy trees with utf-8 graphics). - item = ( unicode(row[i]) if not isinstance(row[i], str) - else row[i].decode(self.encoding)) - if i < len(row) - 1: - item = item.ljust(size) - self.out.write(self.render_value(item)) - if i < len(row) - 1: - self.out.write(" ") - self.out.write("\n") - self.line_counter += 1 - - def print_stash(self): - # Compute column sizes - column_sizes = [] - rows = iter(self.stash) - try: - if self.column_names is None: - row = rows.next() - else: - row = self.column_names - for i in xrange(len(row)): - column_sizes.append(len(row[i])) - except StopIteration: - pass # empty stash - - for row in rows: - for i in xrange(len(row)): - row_length = len( - unicode(row[i]) if not isinstance(row[i], str) - else row[i].decode(self.encoding)) - if column_sizes[i] < row_length: - column_sizes[i] = row_length - - # print headers - if not self.no_headings and self.column_names: - self.print_text_row(self.column_names, column_sizes) - # print stashed rows - for row in self.stash: - self.print_text_row(row, column_sizes) - self.stash = [] - - def print_row(self, data): - """ - Print data row. - - :param tuple data: Data to print. - """ - self.stash.append(data) - - def print_host(self, hostname): - """ - Prints header for new host. - - :param string hostname: Hostname to print. - """ - if len(self.stash): - # without a check, this would print headers for empty stash - self.print_stash() - super(TableFormatter, self).print_host(hostname) - - def print_table_title(self, title): - """ - Prints title of next tale. - - :param string title: Title to print. - """ - if len(self.stash): - # without a check, this would print headers for empty stash - self.print_stash() - if self.table_counter > 0 or self.line_counter > 0: - self.out.write('\n') - self.out.write("%s:\n" % title) - self.table_counter += 1 - self.line_counter = 0 - - def produce_output(self, rows): - """ - Prints list of rows. - - There can be :py:class:`~.command.FormatterCommand` instance instead of - a row. See documentation of this class for list of allowed commands. - - :param rows: List of rows to print. - :type rows: list or generator - """ - super(TableFormatter, self).produce_output(rows) - self.print_stash() - -class CsvFormatter(ListFormatter): - """ - Renders data in a csv (Comma-separated values) format. - - This formatter supports following commands: - * :py:class:`~.command.NewHostCommand` - * :py:class:`~.command.NewTableCommand` - * :py:class:`~.command.NewTableHeaderCommand` - """ - - def render_value(self, val): - if isinstance(val, basestring): - if isinstance(val, unicode): - val.encode(self.encoding) - val = '"%s"' % val.replace('"', '""') - elif val is None: - val = '' - else: - val = str(val) - return val - - def print_text_row(self, row): - self.print_line(",".join(self.render_value(v) for v in row)) - self.line_counter += 1 - -class SingleFormatter(Formatter): - """ - Meant to render and print attributes of single object. Attributes are - rendered as a list of assignments of values to variables (attribute names). - - This formatter supports following commands: - * :py:class:`~.command.NewHostCommand` - """ - - def produce_output(self, data): - """ - Render and print attributes of single item. - - There can be a :py:class:`~.command.FormatterCommand` instance instead - of a data. See documentation of this class for list of allowed - commands. - - :param data: Is either a pair of property names with list of values or - a dictionary with property names as keys. Using the pair allows to - order the data the way it should be printing. In the latter case - the properties will be sorted by the property names. - :type data: tuple or dict - """ - if isinstance(data, fcmd.NewHostCommand): - self.print_host(data.hostname) - return - - if not isinstance(data, (tuple, dict)): - raise ValueError("data must be tuple or dict") - - if isinstance(data, tuple): - if not len(data) == 2: - raise ValueError( - "data must contain: (list of columns, list of rows)") - dataiter = itertools.izip(data[0], data[1]) - else: - dataiter = itertools.imap( - lambda k: (k, self.render_value(data[k])), - sorted(data.keys())) - for name, value in dataiter: - self.print_line("{0}={1}", name, value) - self.line_counter += 1 - -class ShellFormatter(SingleFormatter): - """ - Specialization of :py:class:`SingleFormatter` having its output executable - as a shell script. - - This formatter supports following commands: - * :py:class:`~.command.NewHostCommand` - """ - - def render_value(self, val): - if isinstance(val, basestring): - if isinstance(val, unicode): - val.encode(self.encoding) - val = "'%s'" % val.replace("'", "\\'") - elif val is None: - val = '' - else: - val = str(val) - return val - -class ErrorFormatter(ListFormatter): - """ - Render error strings for particular host. Supported commands: - * :py:class:`~.command.NewHostCommand` - """ - def __init__(self, stream, padding=4): - super(ErrorFormatter, self).__init__(stream, padding) - - def print_row(self, data): - if isinstance(data, Exception): - if isinstance(data, pywbem.CIMError): - self.print_text_row("%s: %s" % (data.args[1], data.message)) - elif not isinstance(data, errors.LmiFailed): - self.print_text_row("(%s) %s" % (data.__class__.__name__, - str(data))) - else: - self.print_text_row(data) - else: - self.print_text_row(data) - - def print_host(self, hostname): - self.out.write("host %s\n" % hostname) - self.host_counter += 1 - self.table_counter = 0 - self.line_counter = 0 - - def produce_output(self, rows): - for row in rows: - if isinstance(row, fcmd.NewHostCommand): - self.print_host(row.hostname) - elif isinstance(row, fcmd.NewTableCommand): - self.print_table_title(row.title) - else: - self.print_row(row) - diff --git a/lmi/scripts/common/formatter/command.py b/lmi/scripts/common/formatter/command.py deleted file mode 100644 index 78830c6..0000000 --- a/lmi/scripts/common/formatter/command.py +++ /dev/null @@ -1,71 +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 used to control formatters from inside of command -execution functions. -""" - -class FormatterCommand(object): - """ - Base class for formatter commands. - """ - pass - -class NewHostCommand(FormatterCommand): - """ - Command for formatter to finish current table (if any), print header for - new host and (if there are any data) print table header. - - :param string hostname: Name of host appearing at the front of new table. - """ - def __init__(self, hostname): - self.hostname = hostname - -class NewTableCommand(FormatterCommand): - """ - Command for formatter to finish current table (if any), print the **title** - and (if there are any data) print table header. - - :param string title: Optional title for new table. - """ - def __init__(self, title=None): - self.title = title - -class NewTableHeaderCommand(FormatterCommand): - """ - Command for formatter to finish current table (if any), store new table - header and (if there are any data) print the table header. - The table header will be printed in all subsequent tables, until - new instance of this class arrives. - - :param tuple columns: Array of column names. - """ - def __init__(self, columns=None): - self.columns = columns diff --git a/lmi/scripts/common/lmi_logging.py b/lmi/scripts/common/lmi_logging.py deleted file mode 100644 index a6ff0e1..0000000 --- a/lmi/scripts/common/lmi_logging.py +++ /dev/null @@ -1,256 +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> -# -""" -Utilities for logging framework. -""" -import logging -import sys - -from lmi.scripts.common import configuration -from lmi.scripts.common import errors - -TERM_COLOR_NAMES = ( - 'black', - 'red', - 'green', - 'yellow', - 'blue', - 'magenta', - 'cyan', - 'white', - ) - -# Following are terminal color codes of normal mode. -CN_BLACK = 0 -CN_RED = 1 -CN_GREEN = 2 -CN_YELLOW = 3 -CN_BLUE = 4 -CN_MAGENTA = 5 -CN_CYAN = 6 -CN_WHITE = 7 -# Following are terminal color codes of bright mode. -CB_BLACK = 8 -CB_RED = 9 -CB_GREEN = 10 -CB_YELLOW = 11 -CB_BLUE = 12 -CB_MAGENTA = 13 -CB_CYAN = 14 -CB_WHITE = 15 - -WARNING_COLOR = CB_YELLOW -ERROR_COLOR = CB_RED -CRITICAL_COLOR = CB_MAGENTA - -#: Dictionary assigning color code to logging level. -LOG_LEVEL_2_COLOR = { - logging.WARNING : WARNING_COLOR, - logging.ERROR : ERROR_COLOR, - logging.CRITICAL: CRITICAL_COLOR -} - -class LogRecord(logging.LogRecord): - """ - Overrides :py:class:`logging.LogRecord`. It adds new attributes: - - * `levelname_` - Name of level in lowercase. - * `cseq` - Escape sequence for terminal used to set color - assigned to particular log level. - * `creset` - Escape sequence for terminal used to reset foreground - color. - - These can be used in format strings initializing logging formatters. - - Accepts the same arguments as base class. - """ - - def __init__(self, name, level, *args, **kwargs): - use_colors = kwargs.pop('use_colors', True) - logging.LogRecord.__init__(self, name, level, *args, **kwargs) - self.levelname_ = self.levelname.lower() - if use_colors and level >= logging.WARNING: - if level >= logging.CRITICAL: - color = LOG_LEVEL_2_COLOR[logging.CRITICAL] - elif level >= logging.ERROR: - color = LOG_LEVEL_2_COLOR[logging.ERROR] - else: - color = LOG_LEVEL_2_COLOR[logging.WARNING] - self.cseq = get_color_sequence(color) - self.creset = "\x1b[39m" - else: - self.cseq = self.creset = '' - -class ScriptsLogger(logging.getLoggerClass()): - - #: Boolean saying whether the color sequences for log messages shall be - #: generated. - USE_COLORS = True - - def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, - extra=None): - """ - Overriden method that just changes the *LogRecord* class to our - predefined and ensures that exception's traceback is printed once at - most. - """ - if exc_info: - err = exc_info[1] - if getattr(err, '_traceback_logged', False): - # do not print traceback second time - exc_info = None - elif self.isEnabledFor(level): - try: - err._traceback_logged = True - except AttributeError: - pass - rv = LogRecord(name, level, fn, lno, msg, args, exc_info, func, - use_colors=self.USE_COLORS) - if extra is not None: - for key in extra: - if (key in ["message", "asctime"]) or (key in rv.__dict__): - raise KeyError("Attempt to overwrite %r in LogRecord" % key) - rv.__dict__[key] = extra[key] - return rv - - def exception(self, msg, *args, **kwargs): - lmi_config = configuration.Configuration.get_instance() - exc_info = sys.exc_info() - err = exc_info[1] - if ( lmi_config.trace - and ( not isinstance(err, errors.LmiError) - or ( not getattr(err, '_traceback_logged', False) - and lmi_config.verbosity >= lmi_config.OUTPUT_DEBUG))): - kwargs['exc_info'] = exc_info - else: - kwargs.pop('exc_info', None) - self.error(msg, *args, **kwargs) - -class LevelDispatchingFormatter(object): - """ - Formatter class for logging module. It allows to predefine different format - string used for some level ranges. - - :param dict formatters: Mapping of module names to *format*. - It is a dictionary of following format: :: - - { max_level1 : format1 - , max_level2 : format2 - , ... } - - *format* in parameters description can be either ``string`` or another - formatter object. - - For example if you want to have format3 used for *ERROR* and *CRITICAL* - levels, *format2* for *INFO* and *format1* for anything else, your - dictionary will look like this: :: - - { logging.INFO - 1 : format1 - , logging.INFO : format2 } - - And the *default* value should have *format3* assigned. - - :param default: Default *format* to use. This format is used for all levels - higher than the maximum of *formatters*' keys. - """ - def __init__(self, formatters, default=configuration.DEFAULT_FORMAT_STRING, - datefmt=None): - for k, formatter in formatters.items(): - if isinstance(formatter, basestring): - formatters[k] = logging.Formatter(formatter, datefmt=datefmt) - #: This a a tuple of pairs sorted by the first item in descending - #: order (highest priority ordered first). - self._formatters = tuple(sorted(formatters.items(), - key=lambda t: -t[0])) - if isinstance(default, basestring): - default = logging.Formatter(default, datefmt=datefmt) - self._default_formatter = default - - def format(self, record): - """ - Interface for logging module. - """ - record.levelname_ = record.levelname.lower() - formatter = self._default_formatter - for level, fmt in self._formatters: - if level < record.levelno: - break - formatter = fmt - try: - return formatter.format(record) - except KeyError: - # in some modules or libraries it may happen that logger is - # initialized before our logger class is set as default - record.cseq = record.creset = '' - return formatter.format(record) - -def get_logger(module_name): - """ - Convenience function for getting callable returning logger for particular - module name. It's supposed to be used at module's level to assign its - result to global variable like this: :: - - from lmi.scripts import common - - LOG = common.get_logger(__name__) - - This can be used in module's functions and classes like this: :: - - def module_function(param): - LOG().debug("This is debug statement logging param: %s", param) - - Thanks to ``LOG`` being a callable, it always returns valid logger object - with current configuration, which may change overtime. - - :param string module_name: Absolute dotted module path. - :rtype: :py:class:`logging.Logger` - """ - def _logger(): - """ Callable used to obtain current logger object. """ - return logging.getLogger(module_name) - return _logger - -def get_color_sequence(color_code): - """ - Computer color sequence for particular color code. - - :returns: Escape sequence for terminal used to set foreground color. - :rtype: str - """ - if color_code <= 7: - return "\x1b[%dm" % (30 + color_code) - return "\x1b[38;5;%dm" % color_code - -def setup_logger(use_colors=True): - """ - This needs to be called before any logging takes place. - """ - ScriptsLogger.USE_COLORS = use_colors - logging.setLoggerClass(ScriptsLogger) diff --git a/lmi/scripts/common/session.py b/lmi/scripts/common/session.py deleted file mode 100644 index b063d0a..0000000 --- a/lmi/scripts/common/session.py +++ /dev/null @@ -1,190 +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 for session object representing all connection to remote hosts. -""" - -from collections import defaultdict - -from lmi.scripts.common import errors -from lmi.scripts.common import get_logger -from lmi.scripts.common.util import FilteredDict -from lmi.shell.LMIConnection import connect - -LOG = get_logger(__name__) - -class Session(object): - """ - Session object keeps connection objects to remote hosts. Their are - associated with particular hostnames. It also caches credentials for them. - Connections are made as they are needed. When credentials are missing - for connection to be made, the user is asked to supply them from - standard input. - - :param app: Instance of main application. - :param list hosts: List of hostname strings. - :param dictionary credentials: Mapping assigning a pair - ``(user, password)`` to each hostname. - :param boolean same_credentials: Use the same credentials for all - hosts in session. The first credentials given will be used. - """ - - def __init__(self, app, hosts, credentials=None, same_credentials=False): - self._app = app - self._connections = {h: None for h in hosts} - # { hostname : (username, password, verified), ... } - # where verified is a flag saying, whether these credentials - # were successfuly used for logging in - self._credentials = defaultdict(lambda: ('', '', False)) - self._same_credentials = same_credentials - if credentials is not None: - if not isinstance(credentials, dict): - raise TypeError("credentials must be a dictionary") - for hostname, creds in credentials.items(): - credentials[hostname] = (creds[0], creds[1], False) - self._credentials.update(credentials) - - def __getitem__(self, hostname): - """ - :rtype: (``LMIConnection``) Connection object to remote host. - ``None`` if connection can not be made. - """ - if self._connections[hostname] is None: - try: - self._connections[hostname] = self._connect( - hostname, interactive=True) - except Exception as exc: - LOG().error('Failed to make a connection to "%s": %s', - hostname, exc) - return self._connections[hostname] - - def __len__(self): - """ Get the number of hostnames in session. """ - return len(self._connections) - - def __contains__(self, uri): - return uri in self._connections - - def __iter__(self): - """ Yields connection objects. """ - successful_connections = 0 - for hostname in self._connections: - connection = self[hostname] - if connection is not None: - yield connection - successful_connections += 1 - if successful_connections == 0: - raise errors.LmiNoConnections('No successful connection made.') - - def _connect(self, hostname, interactive=False): - """ - Makes the connection to host. - - :param string hostname: Name of host. - :param boolean interactive: Whether we can interact with user - and expect a reply from him. - :returns: Connection to remote host or ``None``. - :rtype: :py:class:`lmi.shell.LMIConnection` or ``None`` - """ - username, password = self.get_credentials(hostname) - kwargs = { - 'verify_server_cert' : self._app.config.verify_server_cert, - 'interactive' : interactive - } - if len(self._connections) > 1: - kwargs['prompt_prefix'] = '[%s] ' % hostname - connection = connect(hostname, username, password, **kwargs) - if connection is not None: - tp = connection._client._cliconn.creds - if tp is None: - tp = ('', '') - self._credentials[hostname] = (tp[0], tp[1], True) - else: - LOG().error('Failed to connect to host "%s".', hostname) - return connection - - @property - def hostnames(self): - """ - List of hostnames in session. - - :rtype: list - """ - return self._connections.keys() - - def get_credentials(self, hostname): - """ - :param string hostname: Name of host to get credentials for. - :returns: Pair of ``(username, password)`` for given hostname. If no - credentials were given for this host, ``('', '')`` is returned. - :rtype: tuple - """ - username, password, verified = self._credentials[hostname] - if ( not verified - and (not username or not password) - and self._same_credentials): - for tp in self._credentials.values(): - if tp[2]: - username, password = tp[0], tp[1] - break - return username, password - - def get_unconnected(self): - """ - :returns: List of hostnames, which do not have associated connection - yet. - :rtype: list - """ - return [h for h, c in self._connections.items() if c is None] - -class SessionProxy(Session): - """ - Behaves like a session. But it just encapsulates other session object and - provides access to a subset of its items. - - :param session: Session object or even another session proxy. - :param list uris: Subset of uris in encapsulated session object. - """ - - def __init__(self, session, uris): - uris = set(uris) - if not all(isinstance(uri, basestring) for uri in uris): - raise ValueError("uris must be iterable of uris") - for uri in uris: - if not uri in session: - raise ValueError('uri "%s" needs to belong to given session' - % uri) - Session.__init__(self, session._app, uris, session._credentials, - session._same_credentials) - self._origin = session - self._connections = FilteredDict(uris, session._connections) - # let the credentials propagage to original session - self._credentials = session._credentials - diff --git a/lmi/scripts/common/util.py b/lmi/scripts/common/util.py deleted file mode 100644 index 4ee2161..0000000 --- a/lmi/scripts/common/util.py +++ /dev/null @@ -1,146 +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> -# -""" -Various utilities for LMI Scripts. -""" - -class FilteredDict(dict): - """ - Dictionary-like collection that wraps some other dictionary and provides - limited access to its keys and values. It permits to get, delete and set - items specified in advance. - - .. note:: - Please use only the methods overriden. This class does not guarantee - 100% API compliance. Not overriden methods won't work properly. - - :param list key_filter: Set of keys that can be get, set or deleted. - For other keys, :py:class:`KeyError` will be raised. - :param dictionary original: Original dictionary containing not only - keys in *key_filter* but others as well. All modifying operations - operate also on this dictionary. But only those keys in *key_filter* - can be affected by them. - """ - - def __init__(self, key_filter, original=None): - dict.__init__(self) - if original is not None and not isinstance(original, dict): - raise TypeError("original needs to be a dictionary") - if original is None: - original = dict() - self._original = original - self._keys = frozenset(key_filter) - - def __contains__(self, key): - return key in self._keys and key in self._original - - def __delitem__(self, key): - if not key in self._keys: - raise KeyError(repr(key)) - del self._original[key] - - def clear(self): - for key in self._keys: - self._original.pop(key, None) - - def copy(self): - return FilteredDict(self._keys, self._original) - - def iterkeys(self): - for key in self._keys: - yield key - - def __getitem__(self, key): - if not key in self._keys: - raise KeyError(repr(key)) - return self._original[key] - - def __iter__(self): - for key in self._keys: - if key in self._original: - yield key - - def __eq__(self, other): - return ( isinstance(other, FilteredDict) - and self._original == other._original - and self._keys == other._keys) - - def __lt__(self, other): - if isinstance(other, dict): - return { k: v for k, v in self._original.items() - if k in self._keys} < other - if not isinstance(other, FilteredDict): - raise TypeError("Can not compare FilteredDict to objects" - " of other types!") - return self._original <= other._original and self._keys <= other._keys - - def __len__(self): - return len(self.keys()) - - def __setitem__(self, key, value): - if not key in self._keys: - raise KeyError(repr(key)) - self._original[key] = value - - def keys(self): - return [k for k in self._keys if k in self._original] - - def values(self): - return [self._original[k] for k in self.keys() if k in self._original] - - def items(self): - return [(k, self._original[k]) for k in self.keys()] - - def iteritems(self): - return iter(self.items()) - - def pop(self, key, *args): - ret = self[key] - del self[key] - return ret - - def popitem(self): - for key in self._keys: - if key in self._original: - return self.pop(key) - raise KeyError("FilterDict is empty!") - - def update(self, *args, **kwargs): - if len(args) > 1: - raise TypeError('Expected just one positional argument!') - if args and callable(getattr(args[0], 'keys', None)): - for key in args[0].keys(): - self[key] = args[0][key] - elif args: - for key, value in args[0]: - self[key] = value - for key, value in kwargs.items(): - self[key] = value - diff --git a/lmi/scripts/common/versioncheck/__init__.py b/lmi/scripts/common/versioncheck/__init__.py deleted file mode 100644 index c7e595f..0000000 --- a/lmi/scripts/common/versioncheck/__init__.py +++ /dev/null @@ -1,146 +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> -# -""" -Package with utilities for checking availability of profiles or CIM classes. -Version requirements can also be specified. -""" - -import functools -from pyparsing import ParseException - -from lmi.scripts.common import Configuration -from lmi.scripts.common import errors -from lmi.scripts.common.versioncheck import parser - -def cmp_profiles(fst, snd): - """ - Compare two profiles by their version. - - :returns: - * -1 if the *fst* profile has lower version than *snd* - * 0 if their versions are equal - * 1 otherwise - :rtype: int - """ - fstver = fst.RegisteredVersion - sndver = snd.RegisteredVersion - if fstver == sndver: - return 0 - return -1 if parser.cmp_version(fstver, sndver) else 1 - -def get_profile_version(conn, name, cache=None): - """ - Get version of registered profile on particular broker. Queries - ``CIM_RegisteredProfile`` and ``CIM_RegisteredSubProfile``. The latter - comes in question only when ``CIM_RegisteredProfile`` does not yield any - matching result. - - :param conn: Connection object. - :param string name: Name of the profile which must match value of *RegisteredName* - property. - :param dictionary cache: Optional cache where the result will be stored for - later use. This greatly speeds up evaluation of several expressions refering - to same profiles or classes. - :returns: Version of matching profile found. If there were more of them, - the highest version will be returned. ``None`` will be returned when no matching - profile or subprofile is found. - :rtype: string - """ - if cache and name in cache: - return cache[(conn.uri, name)] - insts = conn.root.interop.wql('SELECT * FROM CIM_RegisteredProfile' - ' WHERE RegisteredName=\"%s\"' % name) - regular = set(i for i in insts if i.classname.endswith('RegisteredProfile')) - if regular: # select instances of PG_RegisteredProfile if available - insts = regular - else: # otherwise fallback to PG_RegisteredSubProfile instances - insts = set(i for i in insts if i not in regular) - if not insts: - ret = None - else: - ret = sorted(insts, cmp=cmp_profiles)[-1].RegisteredVersion - if cache is not None: - cache[(conn.uri, name)] = ret - return ret - -def get_class_version(conn, name, namespace=None, cache=None): - """ - Query broker for version of particular CIM class. Version is stored in - ``Version`` qualifier of particular CIM class. - - :param conn: Connection object. - :param string name: Name of class to query. - :param string namespace: Optional CIM namespace. Defaults to configured namespace. - :param dictionary cache: Optional cache used to speed up expression prrocessing. - :returns: Version of CIM matching class. Empty string if class is registered but - is missing ``Version`` qualifier and ``None`` if it is not registered. - :rtype: string - """ - if namespace is None: - namespace = Configuration.get_instance().namespace - if cache and (namespace, name) in cache: - return cache[(conn.uri, namespace, name)] - ns = conn.get_namespace(namespace) - cls = getattr(ns, name, None) - if not cls: - ret = None - else: - quals = cls.wrapped_object.qualifiers - if 'Version' not in quals: - ret = '' - else: - ret = quals['Version'].value - if cache is not None: - cache[(conn.uri, namespace, name)] = ret - return ret - -def eval_respl(expr, conn, namespace=None, cache=None): - """ - Evaluate LMIReSpL expression on particular broker. - - :param string expr: Expression to evaluate. - :param conn: Connection object. - :param string namespace: Optional CIM namespace where CIM classes will be - searched. - :param dictionary cache: Optional cache speeding up evaluation. - :returns: ``True`` if requirements in expression are satisfied. - :rtype: boolean - """ - if namespace is None: - namespace = Configuration.get_instance().namespace - stack = [] - pvget = functools.partial(get_profile_version, conn, cache=cache) - cvget = functools.partial(get_class_version, conn, - namespace=namespace, cache=cache) - pr = parser.bnf_parser(stack, pvget, cvget) - pr.parseString(expr, parseAll=True) - # Now evaluate starting non-terminal created on stack. - return stack[0]() - diff --git a/lmi/scripts/common/versioncheck/parser.py b/lmi/scripts/common/versioncheck/parser.py deleted file mode 100644 index b5f9116..0000000 --- a/lmi/scripts/common/versioncheck/parser.py +++ /dev/null @@ -1,521 +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> -# -""" -Parser for mini-language specifying profile and class requirements. We call -the language LMIReSpL (openLMI Requirement Specification Language). - -The only thing designed for use outside this module is :py:func:`bnf_parser`. - -Language is generated by BNF grammer which served as a model for parser. - -Formal representation of BNF grammer is following: :: - - expr ::= term [ op expr ]* - term ::= '!'? req - req ::= profile_cond | clsreq_cond | '(' expr ')' - profile_cond ::= 'profile'? [ profile | profile_quot ] cond? - clsreq_cond ::= 'class' [ clsname | clsname_quot] cond? - profile_quot ::= '"' /\w+[ +.a-zA-Z0-9_-]*/ '"' - profile ::= /\w+[+.a-zA-Z_-]*/ - clsname_quot ::= '"' clsname '"' - clsname ::= /[a-zA-Z]+_[a-zA-Z][a-zA-Z0-9_]*/ - cond ::= cmpop version - cmpop ::= /(<|=|>|!)=|<|>/ - version ::= /[0-9]+(\.[0-9]+)*/ - op ::= '&' | '|' - -String surrounded by quotes is a literal. String enclosed with slashes is a -regular expression. Square brackets encloses a group of words and limit -the scope of some operation (like iteration). -""" -import abc -import operator -from pyparsing import Literal, Combine, Optional, ZeroOrMore, \ - Forward, Regex, Keyword, FollowedBy, LineEnd, ParseException - -#: Dictionary mapping supported comparison operators to a pair. First item is a -#: function making the comparison and the second can be of two values (``all`` -#: or ``any``). Former sayes that each part of first version string must be in -#: relation to corresponding part of second version string in order to satisfy -#: the condition. The latter causes the comparison to end on first satisfied -#: part. -OP_MAP = { - '==' : (operator.eq, all), - '<=' : (operator.le, all), - '>=' : (operator.ge, all), - '!=' : (operator.ne, any), - '>' : (operator.gt, any), - '<' : (operator.lt, any) -} - -def cmp_version(fst, snd, opsign='<'): - """ - Compare two version specifications. Each version string shall contain - digits delimited with dots. Empty string is also valid version. It will be - replaced with -1. - - :param str fst: First version string. - :param str snd: Second version string. - :param str opsign: Sign denoting operation to be used. Supported signs - are present in :py:attr:`OP_MAP`. - :returns: ``True`` if the relation denoted by particular operation exists - between two operands. - :rtype: boolean - """ - def splitver(ver): - """ Converts version string to a tuple of integers. """ - return tuple(int(p) if p else -1 for p in ver.split('.')) - aparts = splitver(fst) - bparts = splitver(snd) - op, which = OP_MAP[opsign] - if which is all: - for ap, bp in zip(aparts, bparts): - if not op(ap, bp): - return False - else: - for ap, bp in zip(aparts, bparts): - if op(ap, bp): - return True - if ap != bp: - return False - return op(len(aparts), len(bparts)) - -class SemanticGroup(object): - """ - Base class for non-terminals. Just a minimal set of non-terminals is - represented by objects the rest is represented by strings. - - All subclasses need to define their own :py:meth:`evaluate` method. The - parser builds a tree of these non-terminals with single non-terminal being - a root node. This node's *evaluate* method returns a boolean saying whether - the condition is satisfied. Root node is always an object of - :py:class:`Expr`. - """ - - __metaclass__ = abc.ABCMeta - - def __call__(self): - return self.evaluate() - - @abc.abstractmethod - def evaluate(self): - """ - :returns: ``True`` if the sub-condition represented by this non-terminal - is satisfied. - :rtype: boolean - """ - pass - -class Expr(SemanticGroup): - """ - Initial non-terminal. Object of this class (or one of its subclasses) is a - result of parsing. - - :param term: An object of :py:class:`Term` non-terminal. - """ - - def __init__(self, term): - assert isinstance(term, Term) - self.fst = term - - def evaluate(self): - return self.fst() - - def __str__(self): - return str(self.fst) - -class And(Expr): - """ - Represents logical *AND* of two expressions. Short-circuit evaluation is - being exploited here. - - :param fst: An object of :py:class:`Term` non-terminal. - :param snd: An object of :py:class:`Term` non-terminal. - """ - - def __init__(self, fst, snd): - assert isinstance(snd, (Term, Expr)) - Expr.__init__(self, fst) - self.snd = snd - - def evaluate(self): - if self.fst(): - return self.snd() - return False - - def __str__(self): - return "%s & %s" % (self.fst, self.snd) - -class Or(Expr): - """ - Represents logical *OR* of two expressions. Short-circuit evaluation is being - exploited here. - - :param fst: An object of :py:class:`Term` non-terminal. - :param snd: An object of :py:class:`Term` non-terminal. - """ - - def __init__(self, fst, snd): - assert isinstance(snd, (Term, Expr)) - Expr.__init__(self, fst) - self.snd = snd - - def evaluate(self): - if self.fst(): - return True - return self.snd() - - def __str__(self): - return "%s | %s" % (self.fst, self.snd) - -class Term(SemanticGroup): - """ - Represents possible negation of expression. - - :param req: An object of :py:class:`Req`. - :param boolean negate: Whether the result of children shall be negated. - """ - - def __init__(self, req, negate): - assert isinstance(req, Req) - self.req = req - self.negate = negate - - def evaluate(self): - res = self.req() - return not res if self.negate else res - - def __str__(self): - if self.negate: - return '!' + str(self.req) - return str(self.req) - -class Req(SemanticGroup): - """ - Represents one of following subexpressions: - - * single requirement on particular profile - * single requirement on particular class - * a subexpression - """ - pass - -class ReqCond(Req): - """ - Represents single requirement on particular class or profile. - - :param str kind: Name identifying kind of thing this belongs. For example - ``'class'`` or ``'profile'``. - :param callable version_getter: Is a function called to get version of - either profile or CIM class. It must return corresponding version string - if the profile or class is registered and ``None`` otherwise. - Version string is read from ``RegisteredVersion`` property of - ``CIM_RegisteredProfile``. If a class is being queried, version - shall be taken from ``Version`` qualifier of given class. - :param str name: Name of profile or CIM class to check for. In case - of a profile, it is compared to ``RegisteredName`` property of - ``CIM_RegisteredProfile``. If any instance of this class has matching - name, it's version will be checked. If no matching instance is found, - instances of ``CIM_RegisteredSubProfile`` are queried the same way. - Failing to find it results in ``False``. - :param str cond: Is a version requirement. Check the grammer above for - ``cond`` non-terminal. - """ - - def __init__(self, kind, version_getter, name, cond=None): - assert isinstance(kind, basestring) - assert callable(version_getter) - assert isinstance(name, basestring) - assert cond is None or (isinstance(cond, tuple) and len(cond) == 2) - self.kind = kind - self.version_getter = version_getter - self.name = name - self.cond = cond - - def evaluate(self): - version = self.version_getter(self.name) - return version and (not self.cond or self._check_version(version)) - - def _check_version(self, version): - """ - Checks whether the version of profile or class satisfies the - requirement. Version strings are first split into a list of integers - (that were delimited with a dot) and then they are compared in - descending order from the most signigicant down. - - :param str version: Version of profile or class to check. - """ - opsign, cmpver = self.cond - return cmp_version(version, cmpver, opsign) - - def __str__(self): - return '{%s "%s"%s}' % ( - self.kind, self.name, ' %s %s' % self.cond if self.cond else '') - -class Subexpr(Req): - """ - Represents a subexpression originally enclosed in brackets. - """ - - def __init__(self, expr): - assert isinstance(expr, Expr) - self.expr = expr - - def evaluate(self): - return self.expr() - - def __str__(self): - return "(%s)" % self.expr - -class TreeBuilder(object): - """ - A stack interface for parser. It defines methods modifying the stack with - additional checks. - """ - - def __init__(self, stack, profile_version_getter, class_version_getter): - if not isinstance(stack, list): - raise TypeError("stack needs to be empty!") - if stack: - stack[:] = [] - self.stack = stack - self.profile_version_getter = profile_version_getter - self.class_version_getter = class_version_getter - - def expr(self, strg, loc, toks): - """ - Operates upon a stack. It takes either one or two *terms* there - and makes an expression object out of them. Terms need to be delimited - with logical operator. - """ - assert len(self.stack) > 0 - if not isinstance(self.stack[-1], (Term, Expr)): - raise ParseException("Invalid expression (stopped at char %d)." - % loc) - if len(self.stack) >= 3 and self.stack[-2] in ('&', '|'): - assert isinstance(self.stack[-3], Term) - if self.stack[-2] == '&': - expr = And(self.stack[-3], self.stack[-1]) - else: - expr = Or(self.stack[-3], self.stack[-1]) - self.stack.pop() - self.stack.pop() - elif not isinstance(self.stack[-1], Expr): - expr = Expr(self.stack[-1]) - else: - expr = self.stack[-1] - self.stack[-1] = expr - - def term(self, strg, loc, toks): - """ - Creates a ``term`` out of requirement (``req`` non-terminal). - """ - assert len(self.stack) > 0 - assert isinstance(self.stack[-1], Req) - self.stack[-1] = Term(self.stack[-1], toks[0] == '!') - - def subexpr(self, strg, loc, toks): - """ - Operates upon a stack. It creates an instance of :py:class:`Subexpr` - out of :py:class:`Expr` which is enclosed in brackets. - """ - assert len(self.stack) > 1 - assert self.stack[-2] == '(' - assert isinstance(self.stack[-1], Expr) - assert len(toks) > 0 and toks[-1] == ')' - self.stack[-2] = Subexpr(self.stack[-1]) - self.stack.pop() - - def push_class(self, strg, loc, toks): - """ - Handles ``clsreq_cond`` non-terminal in one go. It extracts - corresponding tokens and pushes an object of :py:class:`ReqCond` to a - stack. - """ - assert toks[0] == 'class' - assert len(toks) >= 2 - name = toks[1] - condition = None - if len(toks) > 2 and toks[2] in OP_MAP: - assert len(toks) >= 4 - condition = toks[2], toks[3] - self.stack.append(ReqCond('class', self.class_version_getter, - name, condition)) - - def push_profile(self, strg, loc, toks): - """ - Handles ``profile_cond`` non-terminal in one go. It behaves in the same - way as :py:meth:`push_profile`. - """ - index = 0 - if toks[0] == 'profile': - index = 1 - assert len(toks) > index - name = toks[index] - index += 1 - condition = None - if len(toks) > index and toks[index] in OP_MAP: - assert len(toks) >= index + 2 - condition = toks[index], toks[index + 1] - self.stack.append(ReqCond('profile', self.profile_version_getter, - name, condition)) - - def push_literal(self, strg, loc, toks): - """ - Pushes operators to a stack. - """ - assert toks[0] in ('&', '|', '(') - if toks[0] == '(': - assert not self.stack or self.stack[-1] in ('&', '|') - else: - assert len(self.stack) > 0 - assert isinstance(self.stack[-1], Term) - self.stack.append(toks[0]) - -def bnf_parser(stack, profile_version_getter, class_version_getter): - """ - Builds a parser operating on provided stack. - - :param list stack: Stack to operate on. It will contain the resulting - :py:class:`Expr` object when the parsing is successfully over - - it will be the only item in the list. It needs to be initially empty. - :param callable profile_version_getter: Function returning version - of registered profile or ``None`` if not present. - :param callable class_version_getter: Fucntion returning version - of registered class or ``None`` if not present. - :returns: Parser object. - :rtype: :py:class:`pyparsing,ParserElement` - """ - if not isinstance(stack, list): - raise TypeError("stack must be a list!") - builder = TreeBuilder(stack, profile_version_getter, class_version_getter) - - ntop = ((Literal('&') | Literal('|')) + FollowedBy(Regex('["a-zA-Z\(!]'))) \ - .setName('op').setParseAction(builder.push_literal) - ntversion = Regex(r'[0-9]+(\.[0-9]+)*').setName('version') - ntcmpop = Regex(r'(<|=|>|!)=|<|>(?=\s*\d)').setName('cmpop') - ntcond = (ntcmpop + ntversion).setName('cond') - ntclsname = Regex(r'[a-zA-Z]+_[a-zA-Z][a-zA-Z0-9_]*').setName('clsname') - ntclsname_quot = Combine( - Literal('"').suppress() - + ntclsname - + Literal('"').suppress()).setName('clsname_quot') - ntprofile_quot = Combine( - Literal('"').suppress() - + Regex(r'\w+[ +.a-zA-Z0-9_-]*') - + Literal('"').suppress()).setName('profile_quot') - ntprofile = Regex(r'\w+[+.a-zA-Z0-9_-]*').setName('profile') - ntclsreq_cond = ( - Keyword('class') - + (ntclsname_quot | ntclsname) - + Optional(ntcond)).setName('clsreq_cond').setParseAction( - builder.push_class) - ntprofile_cond = ( - Optional(Keyword('profile')) - + (ntprofile_quot | ntprofile) - + Optional(ntcond)).setName('profile_cond').setParseAction( - builder.push_profile) - ntexpr = Forward().setName('expr') - bracedexpr = ( - Literal('(').setParseAction(builder.push_literal) - + ntexpr - + Literal(')')).setParseAction(builder.subexpr) - ntreq = (bracedexpr | ntclsreq_cond | ntprofile_cond).setName('req') - ntterm = (Optional(Literal("!")) + ntreq + FollowedBy(Regex('[\)&\|]') | LineEnd()))\ - .setParseAction(builder.term) - ntexpr << ntterm + ZeroOrMore(ntop + ntexpr).setParseAction(builder.expr) - - return ntexpr - -if __name__ == '__main__': - def get_class_version(class_name): - try: - version = { 'lmi_logicalfile' : '0.1.2' - , 'lmi_softwareidentity' : '3.2.1' - , 'pg_computersystem' : '1.1.1' - }[class_name.lower()] - except KeyError: - version = None - return version - - def get_profile_version(profile_name): - try: - version = { 'openlmi software' : '0.1.2' - , 'openlmi-software' : '1.3.4' - , 'openlmi hardware' : '1.1.1' - , 'openlmi-hardware' : '0.2.3' - }[profile_name.lower()] - except KeyError: - version = None - return version - - def test(s, expected): - stack = [] - parser = bnf_parser(stack, get_profile_version, get_class_version) - results = parser.parseString(s, parseAll=True) - if len(stack) == 1: - evalresult = stack[0]() - if expected == evalresult: - print "%s\t=>\tOK" % s - else: - print "%s\t=>\tFAILED" % s - else: - print "%s\t=>\tFAILED" % s - print " stack: [%s]" % ', '.join(str(i) for i in stack) - - test( 'class LMI_SoftwareIdentity == 0.2.0', False) - test( '"OpenLMI-Software" == 0.1.2 & "OpenLMI-Hardware" < 0.1.3', False) - test( 'OpenLMI-Software<1|OpenLMI-Hardware!=1.2.4', True) - test( '"OpenLMI Software" & profile "OpenLMI Hardware"' - ' | ! class LMI_LogicalFile', True) - test( 'profile OpenLMI-Software > 0.1.2 & !(class "PG_ComputerSystem"' - ' == 2.3.4 | "OpenLMI Hardware")', False) - test( 'OpenLMI-Software > 1.3 & OpenLMI-Software >= 1.3.4' - ' & OpenLMI-Software < 1.3.4.1 & OpenLMI-Software <= 1.3.4' - ' & OpenLMI-Software == 1.3.4', True) - test( 'OpenLMI-Software < 1.3.4 | OpenLMI-Software > 1.3.4' - ' | OpenLMI-Software != 1.3.4', False) - test( '(! OpenLMI-Software == 1.3.4 | OpenLMI-Software <= 1.3.4.1)' - ' & !(openlmi-software > 1.3.4 | Openlmi-software != 1.3.4)', True) - for badexpr in ( - 'OpenLMI-Software > & OpenLMI-Hardware', - 'classs LMI_SoftwareIdentity', - 'OpenLMI-Software > 1.2.3 | == 5.4.3', - '', - '"OpenLMI-Software', - 'OpenLMI-Software < > OpenLMI-Hardware', - 'OpenlmiSoftware & (openLMI-Hardware ', - 'OpenLMISoftware & ) OpenLMI-Hardare (', - 'OpenLMISoftware | OpenlmiSoftware > "1.2.3"' - ): - try: - test(badexpr, None) - except ParseException: - print "%s\t=>\tOK" % badexpr |