summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichal Minar <miminar@redhat.com>2013-07-13 08:16:06 +0200
committerMichal Minar <miminar@redhat.com>2013-07-19 14:32:58 +0200
commitfc5fd553327ca5cc0a496f3793697f8f39d4d00c (patch)
tree22106808b1f0c92aef2724f764458d51c3365362
parent29df6b80cdbd5f61911c58cf768e73ec8c088851 (diff)
downloadopenlmi-scripts-fc5fd553327ca5cc0a496f3793697f8f39d4d00c.tar.gz
openlmi-scripts-fc5fd553327ca5cc0a496f3793697f8f39d4d00c.tar.xz
openlmi-scripts-fc5fd553327ca5cc0a496f3793697f8f39d4d00c.zip
initial commit
Working lmi metacommand based on python-cliff and python-docopt. Dependency on python-cliff is only temporary - most of its functionality is already overriden. This commit does not contain any commands.
-rw-r--r--README.md80
-rw-r--r--config/lmi.conf14
-rw-r--r--lmi/__init__.py21
-rw-r--r--lmi/scripts/__init__.py24
-rw-r--r--lmi/scripts/_metacommand/__init__.py274
-rw-r--r--lmi/scripts/common/__init__.py110
-rw-r--r--lmi/scripts/common/command.py530
-rw-r--r--lmi/scripts/common/configuration.py91
-rw-r--r--lmi/scripts/common/errors.py72
-rw-r--r--lmi/scripts/common/session.py131
-rwxr-xr-xscripts/lmi32
-rw-r--r--setup.py53
12 files changed, 1431 insertions, 1 deletions
diff --git a/README.md b/README.md
index d4467b8..b67e3c3 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,82 @@
openlmi-scripts
===============
+Client-side python modules and command line utilities.
+
+It comprise of one binary called `lmi` and a common library. `lmi`
+meta-command allows to run commands on a set of OpenLMI providers. These
+commands can be installed separatelly in a modular way.
+
+`lmi` is a command line application allowing to run single command on a set
+of hosts with just a one statement from `shell` or it can run in an
+interactive way.
+
+Structure
+---------
+Following diagram depicts directory structure.
+
+ openlmi-tools
+ ├── commands # base directory for lmi subcommands
+ │   ├── service # service provider comand (service)
+ │   │   └── lmi
+ │   │      └── scripts
+ │   │      └── service
+ │   └── software # software provider command (sw)
+ │   └── lmi
+ │      └── scripts
+ │      └── software
+ ├── config # configuration files for lmi meta-command
+ └── lmi # common client-side library
+    └── scripts
+    ├── common
+    └── _metacommand # functionality of lmi meta-command
+
+Each subdirectory of `commands/` contains library for interfacing with
+particular set of OpenLMI providers. Each contains its own `setup.py` file,
+that handles its installation and registration of command. They have one
+command thing. Each such `setup.py` must pass `entry_points` dictionary to
+the `setup()` function, wich associates commands defined in command library
+with its name under `lmi` meta-command.
+
+Dependencies
+------------
+Code base is written for `pythopn 2.7`.
+There are following python dependencies:
+
+ * openlmi-tools
+ * python-cliff
+ * python-docopt
+
+Installation
+------------
+Use standard `setuptools` script for installation:
+
+ $ cd openlmi-scripts
+ $ python setup.py install --user
+
+This installs just the *lmi meta-command* and client-side library. To install
+subcommands, you need to do the same procedure for each particular command
+under `commands/` directory.
+
+Usage
+-----
+To get a help and see available commands, run:
+
+ $ lmi help
+
+To get a help for particular command, run:
+
+ $ lmi help service
+
+To issue single command on a host, run:
+
+ $ lmi --host ${hostname} service list
+
+To start the app in interactive mode:
+
+ $ lmi --host ${hostname}
+ > service list --disabled
+ ...
+ > service start svnserve.service
+ ...
+ > quit
-Client-side python modules and command line utilities
diff --git a/config/lmi.conf b/config/lmi.conf
new file mode 100644
index 0000000..4c8bd7d
--- /dev/null
+++ b/config/lmi.conf
@@ -0,0 +1,14 @@
+# Sample configuration file for OpenLMI script metacommand.
+
+[CIM]
+# To override default CIM namespace, uncomment the line below.
+#namespace = root/cimv2
+
+[Log]
+# These options modify logging configuration of the main process spawned
+# by CIMOM.
+
+# Level can be set to following values:
+# DEBUG, TRACE_INFO, TRACE_WARNING, INFO, WARNING, ERROR, CRITICAL
+# It does not have any effect, if file_config option is set.
+#level = ERROR
diff --git a/lmi/__init__.py b/lmi/__init__.py
new file mode 100644
index 0000000..aa4170a
--- /dev/null
+++ b/lmi/__init__.py
@@ -0,0 +1,21 @@
+# Software Management Providers
+#
+# Copyright (C) 2012-2013 Red Hat, Inc. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Michal Minar <miminar@redhat.com>
+#
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/lmi/scripts/__init__.py b/lmi/scripts/__init__.py
new file mode 100644
index 0000000..bf39403
--- /dev/null
+++ b/lmi/scripts/__init__.py
@@ -0,0 +1,24 @@
+# Software Management Providers
+#
+# Copyright (C) 2012-2013 Red Hat, Inc. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Michal Minar <miminar@redhat.com>
+#
+"""
+Package with client-side python modules and command line utilities.
+"""
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/lmi/scripts/_metacommand/__init__.py b/lmi/scripts/_metacommand/__init__.py
new file mode 100644
index 0000000..e58036a
--- /dev/null
+++ b/lmi/scripts/_metacommand/__init__.py
@@ -0,0 +1,274 @@
+# Copyright (C) 2012 Red Hat, Inc. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Michal Minar <miminar@redhat.com>
+# -*- coding: utf-8 -*-
+"""
+The base module containing the main functionality of ``lmi`` metacommand.
+"""
+
+import argparse
+import logging
+import pkg_resources
+import sys
+
+from cliff.app import App
+from cliff.commandmanager import CommandManager
+from cliff.help import HelpCommand
+from cliff.interactive import InteractiveApp
+import lmi.lmi_client_base
+from lmi.scripts import common
+from lmi.scripts.common import errors
+from lmi.scripts.common.command import LmiCommandMultiplexer, LmiBaseCommand
+from lmi.scripts.common.configuration import Configuration
+from lmi.scripts.common.session import Session
+
+PYTHON_EGG_NAME = "lmi-scripts"
+#RE_COMMAND = re.compiler(r'^[a-z_]+(?:-[a-z_]+)*$')
+
+LOG = common.get_logger(__name__)
+
+# ignore any message before the logging is configured
+logging.getLogger('').addHandler(logging.NullHandler())
+
+class LmiHelpCommand(HelpCommand):
+ """print detailed help for another command
+ """
+
+ def take_action(self, parsed_args):
+ if parsed_args.cmd:
+ try:
+ the_cmd = self.app.command_manager.find_command(
+ parsed_args.cmd,
+ )
+ cmd_factory, cmd_name, search_args = the_cmd
+ except ValueError:
+ # Did not find an exact match
+ cmd = parsed_args.cmd[0]
+ fuzzy_matches = [k[0] for k in self.app.command_manager
+ if k[0].startswith(cmd)
+ ]
+ if not fuzzy_matches:
+ raise
+ self.app.stdout.write('Command "%s" matches:\n' % cmd)
+ for fm in fuzzy_matches:
+ self.app.stdout.write(' %s\n' % fm)
+ return
+ cmd = cmd_factory(self.app, search_args)
+ full_name = (cmd_name
+ if self.app.interactive_mode
+ else ' '.join([self.app.NAME, cmd_name])
+ )
+ cmd_parser = cmd.get_parser(full_name)
+ self.app.stdout.write(cmd.__doc__)
+ return 0
+ else:
+ cmd_parser = self.get_parser(' '.join([self.app.NAME, 'help']))
+ cmd_parser.print_help(self.app.stdout)
+ return 0
+
+def parse_hosts_file(hosts_file):
+ res = []
+ for line in hosts_file.readlines():
+ hostname = line.strip()
+ res.append(hostname)
+ return res
+
+class MetaCommand(App):
+
+ def __init__(self):
+ lmi.lmi_client_base.LmiBaseClient._set_use_exceptions(True)
+ App.__init__(self,
+ "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.",
+ pkg_resources.get_distribution(PYTHON_EGG_NAME).version,
+ command_manager=CommandManager('lmi.scripts.cmd'))
+ self.command_manager.add_command('help', LmiHelpCommand)
+ self.session = None
+
+ def build_option_parser(self, *args, **kwargs):
+ parser = App.build_option_parser(self, *args, **kwargs)
+ parser.add_argument('--config-file', '-c', action='store',
+ default=Configuration.USER_CONFIG_FILE_PATH,
+ help="Specify the user configuration file to use. Options in"
+ " this file override any settings of global configuration"
+ " file located in \"%s\"." % Configuration.config_file_path())
+ parser.add_argument('--user', action='store', default="",
+ help="Supply a username used in each connection to target"
+ " host.")
+ parser.add_argument('--host', action='append', dest='hosts',
+ default=[],
+ help="Hostname of target system, where the command will be"
+ " applied.")
+ parser.add_argument('--hosts-file', type=file,
+ help="Supply a path to file containing target hostnames."
+ " Each hostname must be listed on single line.")
+ return parser
+
+ def run(self, argv):
+ """Equivalent to the main program for the application.
+
+ :param argv: input arguments and options
+ :paramtype argv: list of str
+ """
+ def _debug():
+ if hasattr(self, 'options'):
+ return self.options.debug
+ else:
+ return True
+ try:
+ self.options, remainder = self.parser.parse_known_args(argv)
+ self.configure_logging()
+ self.interactive_mode = not remainder
+ self.initialize_app(remainder)
+ except Exception as err:
+ if _debug():
+ LOG().exception(err)
+ raise
+ else:
+ LOG().error(err)
+ return 1
+
+ result = 1
+ if self.interactive_mode:
+ # Cmd does not provide a way to override arguments in some nice way
+ sys.argv = [sys.argv[0]] + remainder
+ result = self.interact()
+ else:
+ try:
+ result = self.run_subcommand(remainder)
+ except errors.LmiCommandError as exc:
+ getattr(LOG(), 'exception' if _debug() else 'critical')(
+ str(exc))
+ result = 1
+ return result
+
+ def prepare_to_run_command(self, cmd):
+ if not isinstance(cmd, LmiHelpCommand):
+ if not self.options.hosts and not self.options.hosts_file:
+ self.parser.error(
+ "missing one of (host | hosts-file) arguments")
+ hosts = []
+ if self.options.hosts_file:
+ hosts.extend(parse_hosts_file(self.options.hosts_file))
+ hosts.extend(self.options.hosts)
+ credentials = {h: (self.options.user, '') for h in hosts}
+ if self.session is None:
+ self.session = Session(self, hosts, credentials)
+
+ def run_subcommand(self, argv):
+ subcommand = self.command_manager.find_command(argv)
+ cmd_factory, cmd_name, sub_argv = subcommand
+ cmd = cmd_factory(self, self.options)
+ err = None
+ result = 1
+ try:
+ self.prepare_to_run_command(cmd)
+ full_name = (cmd_name
+ if self.interactive_mode
+ else ' '.join([self.NAME, cmd_name])
+ )
+ cmd_parser = cmd.get_parser(full_name)
+ if isinstance(cmd, LmiBaseCommand):
+ if cmd.is_end_point():
+ parsed_args = cmd_parser.parse_args(sub_argv)
+ result = cmd.run(self.session, parsed_args)
+ else:
+ parsed_args, unknown = cmd_parser.parse_known_args(sub_argv)
+ result = cmd.run(self.session, parsed_args, unknown)
+ else:
+ parsed_args = cmd_parser.parse_args(sub_argv)
+ result = cmd.run(parsed_args)
+
+ except Exception as err:
+ if self.options.debug:
+ LOG().exception(err)
+ else:
+ LOG().error(err)
+ try:
+ self.clean_up(cmd, result, err)
+ except Exception as err2:
+ if self.options.debug:
+ LOG().exception(err2)
+ else:
+ LOG().error('Could not clean up: %s', err2)
+ if self.options.debug:
+ raise
+ else:
+ try:
+ self.clean_up(cmd, result, None)
+ except Exception as err3:
+ if self.options.debug:
+ LOG().exception(err3)
+ else:
+ LOG().error('Could not clean up: %s', err3)
+ return result
+
+ def configure_logging(self):
+ # first instantiation of Configuration object
+ config = Configuration.get_instance(self.options.config_file)
+ config.verbosity = self.options.verbose_level
+ root_logger = logging.getLogger('')
+ # make a reference to null handlers (one should be installed)
+ null_handlers = [ h for h in root_logger.handlers
+ if isinstance(h, logging.NullHandler)]
+ try:
+ logging_level = getattr(logging, config.logging_level.upper())
+ except KeyError:
+ logging_level = logging.ERROR
+
+ # Set up logging to a file
+ log_file = self.options.log_file
+ if log_file is None:
+ log_file = config.get_safe('Log', 'OutputFile')
+ if log_file is not None:
+ file_handler = logging.FileHandler(filename=log_file)
+ formatter = logging.Formatter(
+ config.get_safe('Log', 'FileFormat',
+ fallback=self.LOG_FILE_MESSAGE_FORMAT,
+ raw=True))
+ file_handler.setFormatter(formatter)
+ root_logger.addHandler(file_handler)
+
+ # Always send higher-level messages to the console via stderr
+ console = logging.StreamHandler(self.stderr)
+ console_level_default = logging.ERROR if log_file else logging_level
+ console_level = {
+ Configuration.OUTPUT_SILENT : logging.CRITICAL,
+ Configuration.OUTPUT_WARNING : logging.WARNING,
+ Configuration.OUTPUT_INFO : logging.INFO,
+ Configuration.OUTPUT_DEBUG : logging.DEBUG,
+ }.get(config.verbosity, console_level_default)
+ console.setLevel(console_level)
+ formatter = logging.Formatter(
+ config.get_safe('Log', 'ConsoleFormat',
+ fallback=self.CONSOLE_MESSAGE_FORMAT))
+ console.setFormatter(formatter)
+ root_logger.addHandler(console)
+ root_logger.setLevel(min(logging_level, console_level))
+
+ # remove all null_handlers
+ for handler in null_handlers:
+ root_logger.removeHandler(handler)
+ return
+
+def main(argv=sys.argv[1:]):
+ mc = MetaCommand()
+ return mc.run(argv)
+
diff --git a/lmi/scripts/common/__init__.py b/lmi/scripts/common/__init__.py
new file mode 100644
index 0000000..b9e9d8d
--- /dev/null
+++ b/lmi/scripts/common/__init__.py
@@ -0,0 +1,110 @@
+# Software Management Providers
+#
+# Copyright (C) 2012-2013 Red Hat, Inc. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Michal Minar <miminar@redhat.com>
+#
+"""
+Package with client-side python modules and command line utilities.
+"""
+
+import logging
+from lmi.scripts.common.configuration import Configuration
+
+DEFAULT_LOGGING_CONFIG = {
+ "version" : 1,
+ 'disable_existing_loggers' : True,
+ "formatters": {
+ # this is a message format for logging function/method calls
+ # it's manually set up in YumWorker's init method
+ "default": {
+ "default" : "%(levelname)s:%(module)s:"
+ "%(funcName)s:%(lineno)d - %(message)s"
+ },
+ },
+ "handlers": {
+ "stderr" : {
+ "class" : "logging.StreamHandler",
+ "level" : "ERROR",
+ "formatter": "default",
+ },
+ },
+ "root": {
+ "level": "ERROR",
+ "handlers" : ["cmpi"],
+ },
+ }
+
+def setup_logging(config):
+ """
+ Set up the logging with options given by Configuration instance.
+ This should be called at process's startup before any message is sent to
+ log.
+
+ :param config: (``BaseConfiguration``) Configuration with Log section
+ containing settings for logging.
+ """
+ cp = config.config
+ logging_setup = False
+ try:
+ path = config.file_path('Log', 'FileConfig')
+ if not os.path.exists(path):
+ logging.getLogger(__name__).error('given FileConfig "%s" does'
+ ' not exist', path)
+ else:
+ logging.config.fileConfig(path)
+ logging_setup = True
+ except Exception:
+ if cp.has_option('Log', 'FileConfig'):
+ logging.getLogger(__name__).exception(
+ 'failed to setup logging from FileConfig')
+ if logging_setup is False:
+ defaults = DEFAULT_LOGGING_CONFIG.copy()
+ defaults["handlers"]["cmpi"]["cmpi_logger"] = env.get_logger()
+ if config.stderr:
+ defaults["root"]["handlers"] = ["stderr"]
+ level = config.logging_level
+ if not level in LOGGING_LEVELS:
+ logging.getLogger(__name__).error(
+ 'level name "%s" not supported', level)
+ else:
+ level = LOGGING_LEVELS[level]
+ for handler in defaults["handlers"].values():
+ handler["level"] = level
+ defaults["root"]["level"] = level
+ logging.config.dictConfig(defaults)
+
+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:
+
+ 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.
+ """
+ def _logger():
+ """ Callable used to obtain current logger object. """
+ return logging.getLogger(module_name)
+ return _logger
diff --git a/lmi/scripts/common/command.py b/lmi/scripts/common/command.py
new file mode 100644
index 0000000..e790985
--- /dev/null
+++ b/lmi/scripts/common/command.py
@@ -0,0 +1,530 @@
+# Copyright (C) 2012 Red Hat, Inc. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Michal Minar <miminar@redhat.com>
+# -*- coding: utf-8 -*-
+"""
+Module with abstractions for representing subcommand of lmi meta-command.
+"""
+
+import abc
+import argparse
+import inspect
+import re
+from cliff.command import Command
+from cliff.lister import Lister
+from docopt import docopt
+
+import lmi.lmi_client_base
+from lmi.scripts.common import Configuration
+from lmi.scripts.common import get_logger
+from lmi.scripts.common import errors
+
+RE_CALLABLE = re.compile(
+ r'^(?P<module>[a-z_]+(?:\.[a-z_]+)*):(?P<func>[a-z_]+)$',
+ re.IGNORECASE)
+RE_COMMAND_NAME = re.compile('^[a-z]+(-[a-z]+)*$')
+RE_OPT_BRACKET_ARGUMENT = re.compile('^<(?P<name>[^>]+)>$')
+RE_OPT_UPPER_ARGUMENT = re.compile('^(?P<name>[A-Z]+(?:[_-][A-Z]+)*)$')
+RE_OPT_SHORT_OPTION = re.compile('^-(?P<name>[a-z])$', re.IGNORECASE)
+RE_OPT_LONG_OPTION = re.compile('^--(?P<name>[a-z_-]+)$', re.IGNORECASE)
+
+LOG = get_logger(__name__)
+
+def _row_to_string(tup):
+ return tuple(str(a) for a in tup)
+
+def opt_name_sanitize(opt_name):
+ return re.sub(r'[^a-zA-Z]', '_', 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 options: (``dict``) Dictionary return by docopt call.
+ :rtype: (``dict``) New dictionary with keys passeable to function
+ as argument names.
+ """
+ # (new_name, value) for each pair in options dictionary
+ kwargs = {}
+ # (name
+ orig_names = {}
+ for name, value in options.items():
+ for (reg, func) in (
+ (RE_OPT_BRACKET_ARGUMENT, lambda m: m.group('name')),
+ (RE_OPT_UPPER_ARGUMENT, lambda m: m.group('name')),
+ (RE_OPT_SHORT_OPTION, lambda m: m.group(0)),
+ (RE_OPT_LONG_OPTION, lambda m: m.group(0))):
+ match = reg.match(name)
+ if match:
+ new_name = func(match)
+ break
+ else:
+ if RE_COMMAND_NAME:
+ LOG().warn('command "%s" is missing implementation' % name)
+ continue
+ raise errors.LmiError(
+ 'failed to convert argument "%s" to function option'
+ % name)
+ 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 _EndPointCommandMetaClass(abc.ABCMeta):
+
+ @classmethod
+ def _make_execute_method(mcs, bases, dcl, func):
+ if func is None:
+ for base in bases:
+ if hasattr(base, 'execute'):
+ # we already have abstract execute method defined
+ break
+ else:
+ # prevent instantiation of command without CALLABLE property
+ # specified
+ dcl['execute'] = abc.abstractmethod(lambda self: None)
+ else:
+ del dcl['CALLABLE']
+ def _execute(_self, *args, **kwargs):
+ return func(*args, **kwargs)
+ _execute.dest = func
+ dcl['execute'] = _execute
+
+ def __new__(mcs, name, bases, dcl):
+ 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.LmiCommandImportFailed(
+ dcl['__module__'], name, func)
+ except KeyError:
+ mod = dcl['__module__']
+ if not name.lower() in mod:
+ raise errors.LmiCommandMissingCallable(
+ 'Missing CALLABLE attribute for class "%s.%s".' % (
+ mod.__name__, name))
+ func = mod[name.lower()]
+ if func is not None and not callable(func):
+ raise errors.LmiCommandInvalidCallable(
+ '"%s" is not a callable object or function.' % (
+ func.__module__ + '.' + func.__name__))
+
+ mcs._make_execute_method(bases, dcl, func)
+ return super(_EndPointCommandMetaClass, mcs).__new__(
+ mcs, name, bases, dcl)
+
+class _ListerMetaClass(_EndPointCommandMetaClass):
+
+ def __new__(mcs, name, bases, dcl):
+ cols = dcl.get('COLUMNS', None)
+ if cols is not None:
+ cols = dcl['COLUMNS']
+ if not isinstance(cols, (list, tuple)):
+ raise errors.LmiCommandInvalidProperty(dcl['__module__'], name,
+ 'COLUMNS class property must be either list or tuple')
+ if 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 cols
+ del dcl['COLUMNS']
+ dcl['get_columns'] = classmethod(_new_get_columns)
+ return super(_ListerMetaClass, mcs).__new__(mcs, name, bases, dcl)
+
+class _CheckResultMetaClass(_EndPointCommandMetaClass):
+
+ def __new__(mcs, name, bases, dcl):
+ try:
+ expect = dcl['EXPECT']
+ if callable(expect):
+ def _new_expect(self, options, result):
+ if isinstance(result, lmi.lmi_client_base._RValue):
+ result = result.rval
+ passed = expect(options, result)
+ self._result = result
+ return passed
+ else:
+ def _new_expect(self, _options, result):
+ if isinstance(result, lmi.lmi_client_base._RValue):
+ result = result.rval
+ passed = expect == result
+ self._result = 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
+ #raise errors.LmiCommandError(dcl['__module__'], name,
+ #'missing EXPECT property')
+
+ return super(_CheckResultMetaClass, mcs).__new__(mcs, name, bases, dcl)
+
+class _MultiplexerMetaClass(abc.ABCMeta):
+
+ @classmethod
+ def _is_root_multiplexer(mcs, bases):
+ for base in bases:
+ if issubclass(type(base), _MultiplexerMetaClass):
+ return False
+ return True
+
+ def __new__(mcs, name, bases, dcl):
+ if not mcs._is_root_multiplexer(bases):
+ # check COMMANDS property and make it a classmethod
+ if not 'COMMANDS' in dcl:
+ raise errors.LmiCommandError('missing COMMANDS property')
+ cmds = dcl['COMMANDS']
+ if not isinstance(cmds, dict):
+ raise errors.LmiCommandInvalidProperty(dcl['__module__'], name,
+ 'COMMANDS must be a dictionary')
+ if not all(isinstance(c, basestring) for c in cmds.keys()):
+ raise errors.LmiCommandInvalidProperty(dcl['__module__'], name,
+ 'keys of COMMANDS dictionary must contain command names'
+ ' as strings')
+ for cmd_name, cmd in cmds.items():
+ if not RE_COMMAND_NAME.match(cmd_name):
+ raise errors.LmiCommandInvalidName(cmd, cmd_name)
+ if not issubclass(cmd, LmiBaseCommand):
+ raise errors.LmiCommandError(dcl['__module__'], cmd_name,
+ 'COMMANDS dictionary must be composed of'
+ ' LmiCommandBase subclasses, failed class: "%s"'
+ % cmd.__name__)
+ if issubclass(cmd, LmiCommandMultiplexer):
+ cmd.__doc__ = dcl['__doc__']
+ def _new_get_commands(_cls):
+ return cmds
+ del dcl['COMMANDS']
+ dcl['get_commands'] = classmethod(_new_get_commands)
+
+ # check documentation
+ if dcl.get('__doc__', None) is None:
+ LOG().warn('Command "%s.%s" is missing description string.' % (
+ dcl['__module__'], name))
+ return super(_MultiplexerMetaClass, mcs).__new__(mcs, name, bases, dcl)
+
+class LmiBaseCommand(object):
+
+ @classmethod
+ def is_end_point(cls):
+ return True
+
+ def __init__(self, args, kwargs):
+ self._cmd_name_args = None
+ self.parent = kwargs.pop('parent', None)
+
+ @property
+ def cmd_name(self):
+ """ Name of this subcommand without as a single word. """
+ return self._cmd_name_args[-1]
+
+ @property
+ def cmd_full_name(self):
+ """
+ Name of this subcommand with all prior commands included.
+ It's the sequence of commands as given on command line up to this
+ subcommand without any options present. In interactive mode
+ this won't contain the name of binary (``sys.argv[0]``).
+
+ :rtype: (``str``) Concatenation of all preceding commands with
+ ``cmd_name``.
+ """
+ return ' '.join(self._cmd_name_args)
+
+ @property
+ def cmd_name_args(self):
+ return self._cmd_name_args[:]
+ @cmd_name_args.setter
+ def cmd_name_args(self, args):
+ if isinstance(args, basestring):
+ args = args.split(' ')
+ else:
+ args = list(args)
+ if not isinstance(args, (list, tuple)):
+ raise TypeError("args must be a list")
+ self._cmd_name_args = args
+
+ @property
+ def docopt_cmd_name_args(self):
+ """
+ Arguments array for docopt parser.
+
+ :rtype: (``list``)
+ """
+ if self.app.interactive_mode:
+ return self._cmd_name_args[:]
+ return self._cmd_name_args[1:]
+
+class LmiCommandMultiplexer(LmiBaseCommand, Command):
+ __metaclass__ = _MultiplexerMetaClass
+
+ @classmethod
+ def is_end_point(cls):
+ return False
+
+ @classmethod
+ def get_commands(cls):
+ raise NotImplementedError("get_commands method must be overriden in"
+ " a subclass")
+
+ def __init__(self, *args, **kwargs):
+ LmiBaseCommand.__init__(self, args, kwargs)
+ Command.__init__(self, *args, **kwargs)
+
+ def get_parser(self, cmd_name_args):
+ self.cmd_name_args = cmd_name_args
+ parser = argparse.ArgumentParser(
+ description=self.get_description(),
+ prog=self.cmd_full_name,
+ add_help=False)
+ subparser = parser.add_subparsers(dest='command')
+ for cmd in self.get_commands():
+ subparser.add_parser(cmd)
+ return parser
+
+ def make_options(self, subcmd_name, unknown_args):
+ full_args = self.docopt_cmd_name_args
+ full_args.append(subcmd_name)
+ full_args.extend(unknown_args)
+ options = docopt(self.__doc__, full_args)
+ for scn in self.get_commands():
+ try:
+ del options[scn]
+ except KeyError:
+ LOG().warn('doc string of "%s" command does not contain'
+ ' registered command "%s" command',
+ subcmd_name, scn)
+ # remove also this command from options
+ if self.cmd_name in options:
+ del options[self.cmd_name]
+ return options
+
+ def take_action(self, session, args, unknown_args):
+ for cmd_name, cmd_cls in self.get_commands().items():
+ if cmd_name == args.command:
+ cmd = cmd_cls(self.app, self.app_args, parent=self)
+ subcmd_args = self.cmd_name_args + [cmd_name]
+ cmd_parser = cmd.get_parser(subcmd_args)
+ parsed_args, remainder = cmd_parser.parse_known_args(
+ unknown_args)
+ if cmd.is_end_point():
+ options = self.make_options(cmd_name, remainder)
+ return cmd.run(session, parsed_args, options)
+ else:
+ return cmd.run(session, parsed_args, remainder)
+ # this won't happen if checks are done correctly
+ LOG().critical('unexpected command "%s"', args.command)
+ raise errors.LmiCommandNotFound(args.command)
+
+ def run(self, session, parsed_args, unknown_args):
+ self.take_action(session, parsed_args, unknown_args)
+ return 0
+
+class LmiEndPointCommand(LmiBaseCommand):
+
+ def verify_options(self, _options):
+ pass
+
+ def transform_options(self, options):
+ return options
+
+ @abc.abstractmethod
+ def process_session(self, session, cmd_args, options):
+ raise NotImplementedError("process_session must be overriden"
+ " in subclass")
+
+ @abc.abstractmethod
+ def execute(self, *args, **kwargs):
+ raise NotImplementedError("execute method must be overriden"
+ " in subclass")
+
+ def get_parser(self, cmd_name_args):
+ cls = self.__class__
+ parser = None
+ self.cmd_name_args = cmd_name_args
+ while cls:
+ cmd_bases = tuple(c for c in cls.__bases__
+ if issubclass(c, Command))
+ not_end_point_bases = tuple(c for c in cmd_bases
+ if not issubclass(c, LmiEndPointCommand))
+ if len(not_end_point_bases) > 0:
+ parser = not_end_point_bases[0].get_parser(self,
+ self.cmd_full_name)
+ break
+ cls = cmd_bases[0]
+ return parser
+
+ def _make_end_point_args(self, options):
+ argspec = inspect.getargspec(self.execute.dest)
+ kwargs = options_dict2kwargs(options)
+ to_remove = []
+ if argspec.keywords is None:
+ for opt_name in kwargs:
+ if opt_name not in argspec.args[1:]:
+ 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:
+ del kwargs[opt_name]
+ args = []
+ for arg_name in argspec.args[1:]:
+ 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 run(self, session, cmd_args, options):
+ self.verify_options(options)
+ options = self.transform_options(options)
+ args, kwargs = self._make_end_point_args(options)
+ return self.process_session(session, cmd_args, args, kwargs)
+
+class LmiLister(LmiEndPointCommand, Lister):
+ __metaclass__ = _ListerMetaClass
+
+ def __init__(self, *args, **kwargs):
+ LmiEndPointCommand.__init__(self, args, kwargs)
+ Lister.__init__(self, *args, **kwargs)
+
+ @classmethod
+ def get_columns(cls):
+ return None
+
+ def take_action(self, connection, function_args, function_kwargs):
+ res = self.execute(connection, *function_args, **function_kwargs)
+ columns = self.get_columns()
+ if columns is None:
+ # let's get columns from the first row
+ columns = next(res)
+ return (_row_to_string(columns), res)
+
+ def process_session(self, session, cmd_args, function_args,
+ function_kwargs):
+ self.formatter = self.formatters[cmd_args.formatter]
+ for connection in session:
+ if len(session) > 1:
+ self.app.stdout.write("="*79 + "\n")
+ self.app.stdout.write("Host: %s\n" % connection.hostname)
+ self.app.stdout.write("="*79 + "\n")
+ column_names, data = self.take_action(
+ connection, function_args, function_kwargs)
+ self.produce_output(cmd_args, column_names, data)
+ if len(session) > 1:
+ self.app.stdout.write("\n")
+ return 0
+
+class LmiCheckResult(LmiEndPointCommand, Lister):
+ __metaclass__ = _CheckResultMetaClass
+
+ def __init__(self, *args, **kwargs):
+ LmiEndPointCommand.__init__(self, args, kwargs)
+ Lister.__init__(self, *args, **kwargs)
+
+ @abc.abstractmethod
+ def check_result(self, options, result):
+ raise NotImplementedError("check_result must be overriden in subclass")
+
+ def take_action(self, connection, function_args):
+ try:
+ res = self.execute(connection, function_args)
+ return (self.check_result(function_args, res), None)
+ except Exception as exc:
+ return (False, exc)
+
+ def process_session(self, session, cmd_args, function_args,
+ function_kwargs):
+ self.formatter = self.formatters[cmd_args.formatter]
+ # first list contain passed hosts, the second one failed ones
+ results = ([], [])
+ for connection in session:
+ passed, error = self.take_action(
+ connection, *function_args, **function_kwargs)
+ results[0 if passed else 1].append((connection.hostname, error))
+ if not passed and error:
+ LOG().warn('invocation failed on host "%s": %s',
+ connection.hostname, error)
+ if Configuration.get_instance().verbosity >= \
+ Configuration.OUTPUT_DEBUG:
+ self.app.stdout.write('invocation failed on host "%s":'
+ ' %s\n"' % (connection.hostname, str(error)))
+ if Configuration.get_instance().verbosity >= \
+ Configuration.OUTPUT_INFO:
+ self.app.stdout.write('Successful runs: %d\n' % len(results[0]))
+ failed_runs = len(results[1]) + len(session.get_unconnected())
+ if failed_runs:
+ self.app.stdout.write('There were %d unsuccessful runs on hosts:\n'
+ % failed_runs)
+ self.formatter = self.formatters['table']
+ data = []
+ for hostname in session.get_unconnected():
+ data.append((hostname, 'failed to connect'))
+ for hostname, error in results[1]:
+ if error is None:
+ error = "failed"
+ if ( Configuration.get_instance().verbosity
+ >= Configuration.OUTPUT_INFO
+ and hasattr(self.check_result, 'expected')):
+ error = error + (" (%s != %s)" % (
+ self.check_result.expected, self._result))
+ data.append((hostname, error))
+ self.produce_output(cmd_args, ('Name', 'Error'), data)
+
+def make_list_command(func,
+ name=None,
+ columns=None,
+ verify_func=None,
+ transform_func=None):
+ 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 }
+ if verify_func:
+ props['VERIFY'] = verify_func
+ if transform_func:
+ props['TRANSFORM'] = transform_func
+ return LmiLister.__metaclass__(name, (LmiLister, ), props)
+
+def register_subcommands(command_name, doc_string, command_map):
+ props = { 'COMMANDS' : command_map
+ , '__doc__' : doc_string }
+ return LmiCommandMultiplexer.__metaclass__(command_name,
+ (LmiCommandMultiplexer, ), props)
+
diff --git a/lmi/scripts/common/configuration.py b/lmi/scripts/common/configuration.py
new file mode 100644
index 0000000..8a2a456
--- /dev/null
+++ b/lmi/scripts/common/configuration.py
@@ -0,0 +1,91 @@
+# Copyright (C) 2012 Red Hat, Inc. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Michal Minar <miminar@redhat.com>
+# -*- coding: utf-8 -*-
+"""
+Module for SoftwareConfiguration class.
+
+SoftwareConfiguration
+---------------------
+
+.. autoclass:: SoftwareConfiguration
+ :members:
+
+"""
+
+import os
+from lmi.common.BaseConfiguration import BaseConfiguration
+
+class Configuration(BaseConfiguration):
+ """
+ Configuration class specific to software providers.
+ OpenLMI configuration file should reside in
+ /etc/openlmi/scripts/lmi.conf.
+ """
+
+ CONFIG_FILE_PATH_TEMPLATE = BaseConfiguration.CONFIG_DIRECTORY_TEMPLATE + \
+ "lmi.conf"
+ USER_CONFIG_FILE_PATH = "~/.lmirc"
+
+ OUTPUT_SILENT = -1
+ OUTPUT_WARNING = 0
+ OUTPUT_INFO = 1
+ OUTPUT_DEBUG = 2
+
+ def __init__(self, user_config_file_path=USER_CONFIG_FILE_PATH, **kwargs):
+ """
+ :param user_config_file_path: (``str``) Path to the user configuration
+ options.
+ """
+ self._user_config_file_path = os.path.expanduser(user_config_file_path)
+ BaseConfiguration.__init__(self, **kwargs)
+ self._verbosity = self.OUTPUT_WARNING
+
+ @classmethod
+ def provider_prefix(cls):
+ return "scripts"
+
+ @classmethod
+ def default_options(cls):
+ """ :rtype: (``dict``) Dictionary of default values. """
+ defaults = BaseConfiguration.default_options().copy()
+ defaults["Verbose"] = False
+ return defaults
+
+ @property
+ def verbosity(self):
+ """ Return integer indicating verbosity level of output to console. """
+ if self._verbosity is None:
+ return self.get_safe('Main', 'Verbose', bool, self.OUTPUT_WARNING)
+ return self._verbosity
+
+ @verbosity.setter
+ def verbosity(self, level):
+ """ Allow to set verbosity without modifying configuration values. """
+ if not isinstance(level, (long, int)):
+ raise TypeError("level must be integer")
+ if level < self.OUTPUT_SILENT:
+ level = self.OUTPUT_SILENT
+ elif level > self.OUTPUT_DEBUG:
+ level = self.OUTPUT_DEBUG
+ self._verbosity = level
+
+ def load(self):
+ """ Read additional user configuration file if it exists. """
+ BaseConfiguration.load(self)
+ self.config.read(self._user_config_file_path)
+
diff --git a/lmi/scripts/common/errors.py b/lmi/scripts/common/errors.py
new file mode 100644
index 0000000..43fe55c
--- /dev/null
+++ b/lmi/scripts/common/errors.py
@@ -0,0 +1,72 @@
+# Copyright (C) 2012 Red Hat, Inc. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Michal Minar <miminar@redhat.com>
+# -*- coding: utf-8 -*-
+
+class LmiError(Exception):
+ pass
+
+class LmiFailed(LmiError):
+ pass
+
+class LmiCommandNotFound(LmiError):
+ def __init__(self, cmd_name):
+ LmiError.__init__(self, 'failed to find command "%s"' % cmd_name)
+
+class LmiNoConnections(LmiError):
+ pass
+
+class LmiMissingCommands(LmiError):
+ pass
+
+class LmiAlreadyExists(LmiError):
+ pass
+
+class LmiInvalidName(LmiError):
+ def __init__(self, name):
+ LmiError.__init__(self, 'invalid name of egg "%s"' % name)
+
+class LmiCommandError(LmiError):
+ def __init__(self, module_name, class_name, msg):
+ LmiError.__init__(self, 'wrong declaration of command "%s.%s": %s'
+ % (module_name, class_name, msg))
+
+class LmiCommandAlreadyExists(LmiCommandError):
+ pass
+
+class LmiCommandInvalidName(LmiCommandError):
+ 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):
+ def __init__(self, module_name, class_name):
+ LmiCommandError.__init__(self, module_name, class_name,
+ 'missing CALLABLE property')
+
+class LmiCommandInvalidProperty(LmiCommandError):
+ pass
+
+class LmiCommandImportFailed(LmiCommandInvalidProperty):
+ 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):
+ def __init__(self, module_name, class_name, msg):
+ LmiCommandInvalidProperty.__init__(self, module_name, class_name, msg)
+
diff --git a/lmi/scripts/common/session.py b/lmi/scripts/common/session.py
new file mode 100644
index 0000000..5959b1a
--- /dev/null
+++ b/lmi/scripts/common/session.py
@@ -0,0 +1,131 @@
+# Copyright (C) 2012 Red Hat, Inc. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Michal Minar <miminar@redhat.com>
+# -*- coding: utf-8 -*-
+
+from collections import defaultdict
+import getpass
+import os
+import pywbem
+import readline
+
+from lmi.lmi_client_base import LmiBaseClient
+from lmi.lmi_client_shell import LmiConnection
+from lmi.scripts.common import errors
+from lmi.scripts.common import get_logger
+
+LOG = get_logger(__name__)
+
+class Session(object):
+
+ def __init__(self, app, hosts, credentials=None):
+ self._app = app
+ self._connections = {h: None for h in hosts}
+ self._credentials = defaultdict(lambda: ('', ''))
+ if credentials is not None:
+ if not isinstance(credentials, dict):
+ raise TypeError("credentials must be a dictionary")
+ self._credentials.update(credentials)
+
+ def __getitem__(self, hostname):
+ if self._connections[hostname] is None:
+ self._connections[hostname] = self._connect(
+ hostname, interactive=True)
+ return self._connections[hostname]
+
+ def __len__(self):
+ return len(self._connections)
+
+ def __iter__(self):
+ successful_connections = 0
+ for h in self._connections:
+ try:
+ connection = self[h]
+ if connection is not None:
+ yield connection
+ successful_connections += 1
+ except Exception as exc:
+ LOG().error('failed to make a connection to "%s": %s',
+ h, exc)
+ if successful_connections == 0:
+ raise errors.LmiNoConnections('no successful connection made')
+
+ def _connect(self, hostname, interactive=False):
+ username, password = self.get_credentials(hostname)
+ prompt_prefix = '[%s] '%hostname if len(self) > 1 else ''
+ if not username:
+ while True:
+ try:
+ username = raw_input(prompt_prefix + "username: ")
+ if username:
+ break
+ except EOFError, e:
+ self._app.stdout.write("\n")
+ continue
+ except KeyboardInterrupt, e:
+ self._app.stdout.write("\n")
+ return None
+ if self._app.interactive_mode:
+ readline.remove_history_item(
+ readline.get_current_history_length() - 1)
+ if not password:
+ try:
+ password = getpass.getpass(prompt_prefix + 'password: ')
+ except EOFError, e:
+ password = ""
+ LOG().warn('End of File when reading password for "%s"',
+ hostname)
+ except KeyboardInterrupt, e:
+ LOG().warn('failed to get password for host "%s"', hostname)
+ return None
+ if self._app.interactive_mode:
+ readline.remove_history_item(
+ readline.get_current_history_length() - 1)
+ # Try to get some non-existing class as a login check
+ connection = LmiConnection(hostname, username,
+ password, interactive)
+ use_exceptions = LmiBaseClient._get_use_exceptions()
+ try:
+ LmiBaseClient._set_use_exceptions(True)
+ connection.root.cimv2.NonExistingClass
+ except pywbem.cim_operations.CIMError, e:
+ if e.args[0] == pywbem.cim_constants.CIM_ERR_NOT_FOUND:
+ return connection
+ LOG().error('failed to connect to host "%s"', hostname)
+ if use_exceptions:
+ raise
+ return None
+ except pywbem.cim_http.AuthError, e:
+ LOG().error('failed to authenticate against host "%s"',
+ hostname)
+ return None
+ finally:
+ LmiBaseClient._set_use_exceptions(use_exceptions)
+ LOG().debug('connection to host "%s" successfully created',
+ hostname)
+ return connection
+
+ @property
+ def hostnames(self):
+ return self._connections.keys()
+
+ def get_credentials(self, hostname):
+ return self._credentials[hostname]
+
+ def get_unconnected(self):
+ return [h for h, c in self._connections.items() if c is None]
+
diff --git a/scripts/lmi b/scripts/lmi
new file mode 100755
index 0000000..fb6a014
--- /dev/null
+++ b/scripts/lmi
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+# Software Management Providers
+#
+# Copyright (C) 2012-2013 Red Hat, Inc. All rights reserved.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Authors: Michal Minar <miminar@redhat.com>
+#
+"""
+Client-side command line utility for system management based on OpenLMI.
+"""
+
+import sys
+
+from lmi.scripts._metacommand import MetaCommand
+
+if __name__ == '__main__':
+ sys.exit(MetaCommand().run())
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..0eab0c3
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+
+PROJECT = 'lmi-scripts'
+VERSION = '0.1'
+
+# Bootstrap installation of Distribute
+from setuptools import setup, find_packages
+
+try:
+ long_description = open('README.md', 'rt').read()
+except IOError:
+ long_description = ''
+
+setup(
+ name=PROJECT,
+ version=VERSION,
+ description='Client-side library and command-line client',
+ long_description=long_description,
+ author='Michal Minar',
+ author_email='miminar@redhat.com',
+ url='https://github.com/openlmi/openlmi-scripts',
+ download_url='https://github.com/openlmi/openlmi-scripts/tarball/master',
+ platforms=['Any'],
+ license="LGPLv2+",
+ classifiers=[
+ 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)',
+ 'Operating System :: POSIX :: Linux',
+ 'Topic :: System :: Systems Administration',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.2',
+ 'Intended Audience :: Developers',
+ 'Environment :: Console',
+ ],
+
+ install_requires=['distribute', 'cliff'],
+
+ namespace_packages=['lmi', 'lmi.scripts'],
+ packages=[
+ 'lmi.scripts.common',
+ 'lmi.scripts._metacommand'],
+ include_package_data=True,
+ #data_files=[('/etc/openlmi/scripts', ['config/lmi.conf'])],
+ zip_safe=False,
+ entry_points={
+ 'console_scripts': [
+ 'lmi = lmi.scripts._metacommand:main'
+ ],
+ 'lmi.scripts.cmd': [],
+ },
+ )