diff options
author | Antonio Terceiro <antonio.terceiro@linaro.org> | 2013-04-30 16:10:15 -0300 |
---|---|---|
committer | Antonio Terceiro <antonio.terceiro@linaro.org> | 2013-04-30 16:10:15 -0300 |
commit | 97af8e8d8c9448e3f2f84392ac537f7a4adb6fe4 (patch) | |
tree | c3da2cd57b9199f5eb0e8db61c081b5b51b59501 |
Imported Upstream version 0.6
30 files changed, 1763 insertions, 0 deletions
diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..90aadab --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,17 @@ +Metadata-Version: 1.1 +Name: lava-tool +Version: 0.6 +Summary: Command line utility for Linaro validation services +Home-page: https://launchpad.net/lava-tool +Author: Zygmunt Krynicki +Author-email: zygmunt.krynicki@linaro.org +License: LGPLv3 +Description: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Topic :: Software Development :: Testing @@ -0,0 +1,21 @@ +About +===== + +This source package contains the command-line tool for interacting +with the various services built by the Linaro (www.linaro.org) +Infrastructure Team. + +Note that this package only contains the core tool; to actually +interact with a service you'll need to install a corresponding plugin. +XXX explain where to get some plugins. + +Installation +============ + +See INSTALL + +Reporting Bugs +============== + +All bugs should be reported to the launchpad project at +https://bugs.launchpad.net/lava-tool/+filebug diff --git a/lava/__init__.py b/lava/__init__.py new file mode 100644 index 0000000..d3a6eaf --- /dev/null +++ b/lava/__init__.py @@ -0,0 +1,3 @@ +__import__('pkg_resources').declare_namespace(__name__) +# DO NOT ADD ANYTHING TO THIS FILE! +# IT MUST STAY AS IS (empty apart from the two lines above) diff --git a/lava/tool/__init__.py b/lava/tool/__init__.py new file mode 100644 index 0000000..6feed16 --- /dev/null +++ b/lava/tool/__init__.py @@ -0,0 +1,27 @@ +# Copyright (C) 2010, 2011 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# Author: Michael Hudson-Doyle <michael.hudson@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +lava.tool +========= + +Generic code for command line utilities for LAVA +""" + +__version__ = (0, 6, 0, "final", 0) diff --git a/lava/tool/command.py b/lava/tool/command.py new file mode 100644 index 0000000..a12ed29 --- /dev/null +++ b/lava/tool/command.py @@ -0,0 +1,166 @@ +# Copyright (C) 2010, 2011 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +Interface for all lava-tool commands +""" + +import inspect + + +class Command(object): + """ + Base class for all command line tool sub-commands. + """ + + def __init__(self, parser, args): + """ + Prepare instance for executing commands. + + This method is called immediately after all arguments are parsed and + results are available. This gives subclasses a chance to configure + themselves. The provided parser is an instance of + argparse.ArgumentParser but it may not be the top-level parser (it will + be a parser specific for this command) + + The default implementation stores both arguments as instance + attributes. + """ + self.parser = parser + self.args = args + + def say(self, message, *args, **kwargs): + """ + Handy wrapper for print + format + """ + self.args.dispatcher.say(self, message, *args, **kwargs) + + def invoke(self): + """ + Invoke command action. + """ + raise NotImplementedError() + + def reparse_arguments(self, parser, raw_args): + """ + Re-parse raw arguments into normal arguments + + Parser is the same as in register_arguments (a sub-parser) The true, + topmost parser is in self.parser. + + This method is only needed for specific commands that need to peek at + the arguments before being able to truly redefine the parser and + re-parse the raw arguments again. + """ + raise NotImplementedError() + + @classmethod + def get_name(cls): + """ + Return the name of this command. + + The default implementation strips any leading underscores and replaces + all other underscores with dashes. + """ + return cls.__name__.lstrip("_").replace("_", "-") + + @classmethod + def get_help(cls): + """ + Return the help message of this command + """ + doc = inspect.getdoc(cls) + if doc is not None and "" in doc: + doc = doc[:doc.index("")].rstrip() + return doc + + @classmethod + def get_epilog(cls): + """ + Return the epilog of the help message + """ + doc = inspect.getdoc(cls) + if doc is not None and "" in doc: + doc = doc[doc.index("") + 1:].lstrip() + else: + doc = None + return doc + + @classmethod + def register_arguments(cls, parser): + """ + Register arguments if required. + + Subclasses can override this to add any arguments that will be + exposed to the command line interface. + """ + pass + + +class CommandGroup(Command): + """ + Base class for all command sub-command hubs. + + This class is needed when one wants to get a custom level of command + options that can be freely extended, just like the top-level lava-tool + command. + + For example, a CommandGroup 'actions' will load additional commands from a + the 'lava.actions' namespace. For the end user it will be available as:: + + $ lava-tool foo actions xxx + + Where xxx is one of the Commands that is declared to live in the namespace + provided by 'foo actions'. + """ + + namespace = None + + @classmethod + def get_namespace(cls): + """ + Return the pkg-resources entry point namespace name from which + sub-commands will be loaded. + """ + return cls.namespace + + @classmethod + def register_subcommands(cls, parser): + """ + Register sub commands. + + This method is called around the same time as register_arguments() + would be called for the plain command classes. It loads commands from + the entry point namespace returned by get_namespace() and registeres + them with a Dispatcher class. The parsers used by that dispatcher + are linked to the calling dispatcher parser so the new commands enrich + the top-level parser tree. + + In addition, the provided parser stores a dispatcher instance in its + defaults. This is useful when one wants to access it later. To a final + command instance it shall be available as self.args.dispatcher. + """ + from lava.tool.dispatcher import Dispatcher + dispatcher = Dispatcher(parser, name=cls.get_name()) + namespace = cls.get_namespace() + if namespace is not None: + dispatcher.import_commands(namespace) + parser.set_defaults(dispatcher=dispatcher) + + +SubCommand = CommandGroup diff --git a/lava/tool/commands/__init__.py b/lava/tool/commands/__init__.py new file mode 100644 index 0000000..d4928d4 --- /dev/null +++ b/lava/tool/commands/__init__.py @@ -0,0 +1,83 @@ +# Copyright (C) 2010 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +Package with command line commands +""" + +import argparse +import re + + +class ExperimentalNoticeAction(argparse.Action): + """ + Argparse action that implements the --experimental-notice + """ + + message = """ + Some lc-tool sub-commands are marked as EXPERIMENTAL. Those commands are + not guaranteed to work identically, or have identical interface between + subsequent lc-tool releases. + + We do that to make it possible to provide good user interface and + server-side API when working on new features. Once a feature is stabilized + the UI will be frozen and all subsequent changes will retain backwards + compatibility. + """ + message = message.lstrip() + message = re.sub(re.compile("[ \t]+", re.M), " ", message) + message = re.sub(re.compile("^ ", re.M), "", message) + + def __init__(self, + option_strings, dest, default=None, required=False, + help=None): + super(ExperimentalNoticeAction, self).__init__( + option_strings=option_strings, dest=dest, default=default, nargs=0, + help=help) + + def __call__(self, parser, namespace, values, option_string=None): + parser.exit(message=self.message) + + +class ExperimentalCommandMixIn(object): + """ + Experimental command. + + Prints a warning message on each call to invoke() + """ + + def invoke(self): + self.print_experimental_notice() + return super(ExperimentalCommandMixIn, self).invoke() + + @classmethod + def register_arguments(cls, parser): + retval = super(ExperimentalCommandMixIn, + cls).register_arguments(parser) + parser.register("action", "experimental_notice", + ExperimentalNoticeAction) + group = parser.add_argument_group("experimental commands") + group.add_argument("--experimental-notice", + action="experimental_notice", + default=argparse.SUPPRESS, + help="Explain the nature of experimental commands") + return retval + + def print_experimental_notice(self): + print ("EXPERIMENTAL - SUBJECT TO CHANGE" + " (See --experimental-notice for more info)") diff --git a/lava/tool/commands/help.py b/lava/tool/commands/help.py new file mode 100644 index 0000000..9de5d8e --- /dev/null +++ b/lava/tool/commands/help.py @@ -0,0 +1,35 @@ +# Copyright (C) 2010 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +lava.tool.commands.help +======================= + +Implementation of `lava help` +""" + +from lava.tool.command import Command + + +class help(Command): + """ + Show a summary of all available commands + """ + + def invoke(self): + self.parser.print_help() diff --git a/lava/tool/dispatcher.py b/lava/tool/dispatcher.py new file mode 100644 index 0000000..1256912 --- /dev/null +++ b/lava/tool/dispatcher.py @@ -0,0 +1,155 @@ +# Copyright (C) 2010, 2011, 2012 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +Module with LavaDispatcher - the command dispatcher +""" + +import argparse +import logging +import pkg_resources +import sys + +from lava.tool.errors import CommandError + + +class Dispatcher(object): + """ + Class implementing command line interface for launch control + """ + + description = None + epilog = None + + def __init__(self, parser=None, name=None): + self.parser = parser or self.construct_parser() + self.subparsers = self.parser.add_subparsers( + title="Sub-command to invoke") + self.name = name + + def __repr__(self): + return "%r(name=%r)" % (self.__class__.__name__, self.name) + + @classmethod + def construct_parser(cls): + """ + Construct a parser for this dispatcher. + + This is only used if the parser is not provided by the parent + dispatcher instance. + """ + parser_args = dict(add_help=True) + # Set description based on class description + if cls.description is not None: + parser_args['description'] = cls.description + # Set the epilog based on class epilog + if cls.epilog is not None: + parser_args['epilog'] = cls.epilog + # Return the fresh parser + return argparse.ArgumentParser(**parser_args) + + def import_commands(self, entrypoint_name): + """ + Import commands from given entry point namespace + """ + logging.debug("Loading commands in entry point %r", entrypoint_name) + for entrypoint in pkg_resources.iter_entry_points(entrypoint_name): + try: + command_cls = entrypoint.load() + except (ImportError, pkg_resources.DistributionNotFound) as exc: + logging.exception("Unable to load command: %s", entrypoint.name) + else: + self.add_command_cls(command_cls) + + def add_command_cls(self, command_cls): + """ + Add a new command class to this dispatcher. + + The command must be a subclass of Command or SubCommand. + """ + logging.debug("Loading command class %r", command_cls) + # Create a sub-parser where the command/sub-command can register + # things. + sub_parser = self.subparsers.add_parser( + command_cls.get_name(), + help=command_cls.get_help(), + epilog=command_cls.get_epilog()) + from lava.tool.command import CommandGroup + if issubclass(command_cls, CommandGroup): + # Handle CommandGroup somewhat different. Instead of calling + # register_arguments we call register_subcommands + command_cls.register_subcommands(sub_parser) + # Let's also call register arguments in case we need both + command_cls.register_arguments(sub_parser) + else: + # Handle plain commands by recording their commands in the + # dedicated sub-parser we've crated for them. + command_cls.register_arguments(sub_parser) + # In addition, since we don't want to require all sub-classes of + # Command to super-call register_arguments (everyone would forget + # this anyway) we manually register the command class for that + # sub-parser so that dispatch() can look it up later. + sub_parser.set_defaults( + command_cls=command_cls, + parser=sub_parser) + # Make sure the sub-parser knows about this dispatcher + sub_parser.set_defaults(dispatcher=self) + + def _adjust_logging_level(self, args): + """ + Adjust logging level after seeing the initial arguments + """ + + def dispatch(self, raw_args=None): + """ + Dispatch a command with the specified arguments. + + If arguments are left out they are looked up in sys.argv automatically + """ + # First parse whatever input arguments we've got + args = self.parser.parse_args(raw_args) + # Adjust logging level after seeing arguments + self._adjust_logging_level(args) + # Then look up the command class and construct it with the parser it + # belongs to and the parsed arguments. + command = args.command_cls(args.parser, args) + try: + # Give the command a chance to re-parse command line arguments + command.reparse_arguments(args.parser, raw_args) + except NotImplementedError: + pass + try: + return command.invoke() + except CommandError as ex: + print >> sys.stderr, "ERROR: %s" % (ex,) + return 1 + + @classmethod + def run(cls, args=None): + """ + Dispatch commandsd and exit + """ + raise SystemExit(cls().dispatch(args)) + + def say(self, command, message, *args, **kwargs): + """ + Handy wrapper for print + format + """ + print "{0} >>> {1}".format( + command.get_name(), + message.format(*args, **kwargs)) diff --git a/lava/tool/errors.py b/lava/tool/errors.py new file mode 100644 index 0000000..6f8f0e4 --- /dev/null +++ b/lava/tool/errors.py @@ -0,0 +1,31 @@ +# Copyright (C) 2010, 2011, 2012 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +lava.tool.errors +================ + +Error classes for LAVA Tool. +""" + +class CommandError(Exception): + """ + Raise this from a Command's invoke() method to display an error nicely. + + lava-tool will exit with a status of 1 if this is raised. + """ diff --git a/lava/tool/main.py b/lava/tool/main.py new file mode 100644 index 0000000..d151961 --- /dev/null +++ b/lava/tool/main.py @@ -0,0 +1,132 @@ +# Copyright (C) 2010, 2011 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +lava.tool.main +============== + +Implementation of the `lava` shell command. +""" + +import logging +import sys + +from lava.tool.dispatcher import Dispatcher + + +class LavaDispatcher(Dispatcher): + """ + Dispatcher implementing the `lava` shell command + + This dispatcher imports plugins from `lava.commands` pkg_resources + namespace. Additional plugins can be registered as either + :class:`lava.command.Command` or :class:`lava.command.SubCommand` + sub-classes. + """ + + def __init__(self): + # Call this early so that we don't get logging.basicConfig + # being called by accident. Otherwise we'd have to + # purge all loggers from the root logger and that sucks + self.setup_logging() + # Initialize the base dispatcher + super(LavaDispatcher, self).__init__() + # And import the non-flat namespace commands + self.import_commands('lava.commands') + + @classmethod + def construct_parser(cls): + """ + Construct a parser for this dispatcher. + + This is only used if the parser is not provided by the parent + dispatcher instance. + """ + # Construct a basic parser + parser = super(LavaDispatcher, cls).construct_parser() + # Add the --verbose flag + parser.add_argument( + "-v", "--verbose", + default=False, + action="store_true", + help="Be more verbose (displays more messages globally)") + # Add the --debug flag + parser.add_argument( + "-D", "--debug", + action="store_true", + default=False, + help="Enable debugging on all loggers") + # Add the --trace flag + parser.add_argument( + "-T", "--trace", + action="append", + default=[], + help="Enable debugging of the specified logger, can be specified multiple times") + # Return the improved parser + return parser + + def setup_logging(self): + """ + Setup logging for the root dispatcher + """ + # Enable warning/error message handler + class OnlyProblemsFilter(logging.Filterer): + def filter(self, record): + if record.levelno >= logging.WARN: + return 1 + return 0 + err_handler = logging.StreamHandler(sys.stderr) + err_handler.setLevel(logging.WARN) + err_handler.setFormatter( + logging.Formatter("%(levelname)s: %(message)s")) + err_handler.addFilter(OnlyProblemsFilter()) + logging.getLogger().addHandler(err_handler) + # Enable the debug handler + class DebugFilter(logging.Filter): + def filter(self, record): + if record.levelno == logging.DEBUG: + return 1 + return 0 + dbg_handler = logging.StreamHandler(sys.stderr) + dbg_handler.setLevel(logging.DEBUG) + dbg_handler.setFormatter( + logging.Formatter("%(levelname)s %(name)s: %(message)s")) + dbg_handler.addFilter(DebugFilter()) + logging.getLogger().addHandler(dbg_handler) + + def _adjust_logging_level(self, args): + # Enable verbose message handler + if args.verbose: + logging.getLogger().setLevel(logging.INFO) + class OnlyInfoFilter(logging.Filterer): + def filter(self, record): + if record.levelno == logging.INFO: + return 1 + return 0 + msg_handler = logging.StreamHandler(sys.stdout) + msg_handler.setLevel(logging.INFO) + msg_handler.setFormatter( + logging.Formatter("%(message)s")) + msg_handler.addFilter(OnlyInfoFilter()) + logging.getLogger().addHandler(msg_handler) + # Enable debugging + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + # Enable trace loggers + for name in args.trace: + logging.getLogger(name).setLevel(logging.DEBUG) diff --git a/lava_tool.egg-info/PKG-INFO b/lava_tool.egg-info/PKG-INFO new file mode 100644 index 0000000..90aadab --- /dev/null +++ b/lava_tool.egg-info/PKG-INFO @@ -0,0 +1,17 @@ +Metadata-Version: 1.1 +Name: lava-tool +Version: 0.6 +Summary: Command line utility for Linaro validation services +Home-page: https://launchpad.net/lava-tool +Author: Zygmunt Krynicki +Author-email: zygmunt.krynicki@linaro.org +License: LGPLv3 +Description: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Topic :: Software Development :: Testing diff --git a/lava_tool.egg-info/SOURCES.txt b/lava_tool.egg-info/SOURCES.txt new file mode 100644 index 0000000..5eccec1 --- /dev/null +++ b/lava_tool.egg-info/SOURCES.txt @@ -0,0 +1,29 @@ +README +setup.cfg +setup.py +lava/__init__.py +lava/tool/__init__.py +lava/tool/command.py +lava/tool/dispatcher.py +lava/tool/errors.py +lava/tool/main.py +lava/tool/commands/__init__.py +lava/tool/commands/help.py +lava_tool/__init__.py +lava_tool/authtoken.py +lava_tool/dispatcher.py +lava_tool/interface.py +lava_tool.egg-info/PKG-INFO +lava_tool.egg-info/SOURCES.txt +lava_tool.egg-info/dependency_links.txt +lava_tool.egg-info/entry_points.txt +lava_tool.egg-info/namespace_packages.txt +lava_tool.egg-info/requires.txt +lava_tool.egg-info/top_level.txt +lava_tool.egg-info/zip-safe +lava_tool/commands/__init__.py +lava_tool/commands/auth.py +lava_tool/tests/__init__.py +lava_tool/tests/test_auth_commands.py +lava_tool/tests/test_authtoken.py +lava_tool/tests/test_commands.py
\ No newline at end of file diff --git a/lava_tool.egg-info/dependency_links.txt b/lava_tool.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lava_tool.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/lava_tool.egg-info/entry_points.txt b/lava_tool.egg-info/entry_points.txt new file mode 100644 index 0000000..e792eb6 --- /dev/null +++ b/lava_tool.egg-info/entry_points.txt @@ -0,0 +1,10 @@ + + [console_scripts] + lava-tool = lava_tool.dispatcher:main + lava = lava.tool.main:LavaDispatcher.run + [lava.commands] + help = lava.tool.commands.help:help + [lava_tool.commands] + help = lava.tool.commands.help:help + auth-add = lava_tool.commands.auth:auth_add [auth] +
\ No newline at end of file diff --git a/lava_tool.egg-info/namespace_packages.txt b/lava_tool.egg-info/namespace_packages.txt new file mode 100644 index 0000000..8d8de6d --- /dev/null +++ b/lava_tool.egg-info/namespace_packages.txt @@ -0,0 +1 @@ +lava diff --git a/lava_tool.egg-info/requires.txt b/lava_tool.egg-info/requires.txt new file mode 100644 index 0000000..1c8f8dd --- /dev/null +++ b/lava_tool.egg-info/requires.txt @@ -0,0 +1,4 @@ +argparse >= 1.1 + +[auth] +keyring
\ No newline at end of file diff --git a/lava_tool.egg-info/top_level.txt b/lava_tool.egg-info/top_level.txt new file mode 100644 index 0000000..41444de --- /dev/null +++ b/lava_tool.egg-info/top_level.txt @@ -0,0 +1,2 @@ +lava_tool +lava diff --git a/lava_tool.egg-info/zip-safe b/lava_tool.egg-info/zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lava_tool.egg-info/zip-safe @@ -0,0 +1 @@ + diff --git a/lava_tool/__init__.py b/lava_tool/__init__.py new file mode 100644 index 0000000..7d127d4 --- /dev/null +++ b/lava_tool/__init__.py @@ -0,0 +1,24 @@ +# Copyright (C) 2010, 2011 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# Author: Michael Hudson-Doyle <michael.hudson@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +Deprecated lava_tool package +""" + +from lava.tool import __version__ diff --git a/lava_tool/authtoken.py b/lava_tool/authtoken.py new file mode 100644 index 0000000..70837cd --- /dev/null +++ b/lava_tool/authtoken.py @@ -0,0 +1,107 @@ +# Copyright (C) 2011 Linaro Limited +# +# Author: Michael Hudson-Doyle <michael.hudson@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +import base64 +import urllib +import urllib2 +import os +import xmlrpclib + +import keyring.core + +from lava_tool.interface import LavaCommandError + + +class AuthBackend(object): + + def add_token(self, username, endpoint_url, token): + raise NotImplementedError + + def get_token_for_endpoint(self, user, endpoint_url): + raise NotImplementedError + + +class KeyringAuthBackend(AuthBackend): + + def add_token(self, username, endpoint_url, token): + keyring.core.set_password( + "lava-tool-%s" % endpoint_url, username, token) + + def get_token_for_endpoint(self, username, endpoint_url): + return keyring.core.get_password( + "lava-tool-%s" % endpoint_url, username) + + +class MemoryAuthBackend(AuthBackend): + + def __init__(self, user_endpoint_token_list): + self._tokens = {} + for user, endpoint, token in user_endpoint_token_list: + self._tokens[(user, endpoint)] = token + + def add_token(self, username, endpoint_url, token): + self._tokens[(username, endpoint_url)] = token + + def get_token_for_endpoint(self, username, endpoint_url): + return self._tokens.get((username, endpoint_url)) + + +class XMLRPCTransport(xmlrpclib.Transport): + + def __init__(self, scheme, auth_backend): + xmlrpclib.Transport.__init__(self) + self._scheme = scheme + self.auth_backend = auth_backend + self._opener = urllib2.build_opener() + self.verbose = 0 + + def request(self, host, handler, request_body, verbose=0): + self.verbose = verbose + token = None + user = None + auth, host = urllib.splituser(host) + if auth: + user, token = urllib.splitpasswd(auth) + url = self._scheme + "://" + host + handler + if user is not None and token is None: + token = self.auth_backend.get_token_for_endpoint(user, url) + if token is None: + raise LavaCommandError( + "Username provided but no token found.") + request = urllib2.Request(url, request_body) + request.add_header("Content-Type", "text/xml") + if token: + auth = base64.b64encode(urllib.unquote(user + ':' + token)) + request.add_header("Authorization", "Basic " + auth) + try: + response = self._opener.open(request) + except urllib2.HTTPError as e: + raise xmlrpclib.ProtocolError( + host + handler, e.code, e.msg, e.info()) + return self.parse_response(response) + + +class AuthenticatingServerProxy(xmlrpclib.ServerProxy): + + def __init__(self, uri, transport=None, encoding=None, verbose=0, + allow_none=0, use_datetime=0, auth_backend=None): + if transport is None: + scheme = urllib.splittype(uri)[0] + transport = XMLRPCTransport(scheme, auth_backend=auth_backend) + xmlrpclib.ServerProxy.__init__( + self, uri, transport, encoding, verbose, allow_none, use_datetime) diff --git a/lava_tool/commands/__init__.py b/lava_tool/commands/__init__.py new file mode 100644 index 0000000..f8bf829 --- /dev/null +++ b/lava_tool/commands/__init__.py @@ -0,0 +1,24 @@ +# Copyright (C) 2010 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +Package with command line commands +""" + + +from lava.tool.commands import ExperimentalNoticeAction, ExperimentalCommandMixIn diff --git a/lava_tool/commands/auth.py b/lava_tool/commands/auth.py new file mode 100644 index 0000000..0e0f343 --- /dev/null +++ b/lava_tool/commands/auth.py @@ -0,0 +1,127 @@ +# Copyright (C) 2011 Linaro Limited +# +# Author: Michael Hudson-Doyle <michael.hudson@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +import getpass +import urlparse +import xmlrpclib + +from lava_tool.authtoken import ( + AuthenticatingServerProxy, + KeyringAuthBackend, + MemoryAuthBackend, + ) +from lava_tool.interface import Command, LavaCommandError + + +def normalize_xmlrpc_url(uri): + if '://' not in uri: + uri = 'http://' + uri + if not uri.endswith('/'): + uri += '/' + if not uri.endswith('/RPC2/'): + uri += 'RPC2/' + return uri + + +class auth_add(Command): + """ + Add an authentication token. + """ + + def __init__(self, parser, args, auth_backend=None): + super(auth_add, self).__init__(parser, args) + if auth_backend is None: + auth_backend = KeyringAuthBackend() + self.auth_backend = auth_backend + + @classmethod + def register_arguments(cls, parser): + super(auth_add, cls).register_arguments(parser) + parser.add_argument( + "HOST", + help=("Endpoint to add token for, in the form " + "scheme://username@host. The username will default to " + "the currently logged in user.")) + parser.add_argument( + "--token-file", default=None, + help="Read the secret from here rather than prompting for it.") + parser.add_argument( + "--no-check", action='store_true', + help=("By default, a call to the remote server is made to check " + "that the added token works before remembering it. " + "Passing this option prevents this check.")) + + def invoke(self): + uri = normalize_xmlrpc_url(self.args.HOST) + parsed_host = urlparse.urlparse(uri) + + if parsed_host.username: + username = parsed_host.username + else: + username = getpass.getuser() + + host = parsed_host.hostname + if parsed_host.port: + host += ':' + str(parsed_host.port) + + uri = '%s://%s@%s%s' % ( + parsed_host.scheme, username, host, parsed_host.path) + + if self.args.token_file: + if parsed_host.password: + raise LavaCommandError( + "Token specified in url but --token-file also passed.") + else: + try: + token_file = open(self.args.token_file) + except IOError as ex: + raise LavaCommandError( + "opening %r failed: %s" % (self.args.token_file, ex)) + token = token_file.read().strip() + else: + if parsed_host.password: + token = parsed_host.password + else: + token = getpass.getpass("Paste token for %s: " % uri) + + userless_uri = '%s://%s%s' % ( + parsed_host.scheme, host, parsed_host.path) + + if not self.args.no_check: + sp = AuthenticatingServerProxy( + uri, auth_backend=MemoryAuthBackend( + [(username, userless_uri, token)])) + try: + token_user = sp.system.whoami() + except xmlrpclib.ProtocolError as ex: + if ex.errcode == 401: + raise LavaCommandError( + "Token rejected by server for user %s." % username) + else: + raise + except xmlrpclib.Fault as ex: + raise LavaCommandError( + "Server reported error during check: %s." % ex) + if token_user != username: + raise LavaCommandError( + "whoami() returned %s rather than expected %s -- this is " + "a bug." % (token_user, username)) + + self.auth_backend.add_token(username, userless_uri, token) + + print 'Token added successfully for user %s.' % username diff --git a/lava_tool/dispatcher.py b/lava_tool/dispatcher.py new file mode 100644 index 0000000..7df6578 --- /dev/null +++ b/lava_tool/dispatcher.py @@ -0,0 +1,49 @@ +# Copyright (C) 2010, 2011 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +Module with LavaDispatcher - the command dispatcher +""" + +from lava.tool.dispatcher import Dispatcher +from lava.tool.main import LavaDispatcher as LavaNonLegacyDispatcher +from lava_tool.interface import LavaCommandError + + +class LavaDispatcher(Dispatcher): + """ + Class implementing command line interface for launch control + """ + + toolname = None + + def __init__(self): + super(LavaDispatcher, self).__init__() + prefixes = ['lava_tool'] + if self.toolname is not None: + prefixes.append(self.toolname) + for prefix in prefixes: + self.import_commands("%s.commands" % prefix) + + +def run_with_dispatcher_class(cls): + raise cls.run() + + +def main(): + LavaDispatcher.run() diff --git a/lava_tool/interface.py b/lava_tool/interface.py new file mode 100644 index 0000000..9c162e1 --- /dev/null +++ b/lava_tool/interface.py @@ -0,0 +1,24 @@ +# Copyright (C) 2010, 2011 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +Interface for all lava-tool commands +""" + +from lava.tool.errors import CommandError as LavaCommandError +from lava.tool.command import Command, CommandGroup as SubCommand diff --git a/lava_tool/tests/__init__.py b/lava_tool/tests/__init__.py new file mode 100644 index 0000000..cd2d2fd --- /dev/null +++ b/lava_tool/tests/__init__.py @@ -0,0 +1,57 @@ +# Copyright (C) 2010 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +Package with unit tests for lava_tool +""" + +import doctest +import unittest + + +def app_modules(): + return [ + 'lava_tool.commands', + 'lava_tool.commands.misc', + 'lava_tool.dispatcher', + 'lava_tool.interface', + ] + + +def test_modules(): + return [ + 'lava_tool.tests.test_authtoken', + 'lava_tool.tests.test_auth_commands', + 'lava_tool.tests.test_commands', + ] + + +def test_suite(): + """ + Build an unittest.TestSuite() object with all the tests in _modules. + Each module is harvested for both regular unittests and doctests + """ + modules = app_modules() + test_modules() + suite = unittest.TestSuite() + loader = unittest.TestLoader() + for name in modules: + unit_suite = loader.loadTestsFromName(name) + suite.addTests(unit_suite) + doc_suite = doctest.DocTestSuite(name) + suite.addTests(doc_suite) + return suite diff --git a/lava_tool/tests/test_auth_commands.py b/lava_tool/tests/test_auth_commands.py new file mode 100644 index 0000000..9dc829c --- /dev/null +++ b/lava_tool/tests/test_auth_commands.py @@ -0,0 +1,259 @@ +# Copyright (C) 2011 Linaro Limited +# +# Author: Michael Hudson-Doyle <michael.hudson@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +Unit tests for the lava_tool.commands.auth package +""" + +import StringIO +import sys +import tempfile +import xmlrpclib + +from mocker import ARGS, KWARGS, CONTAINS, MockerTestCase + +from lava_tool.authtoken import MemoryAuthBackend +from lava_tool.interface import LavaCommandError +from lava_tool.commands.auth import auth_add + + +class FakeArgs: + token_file = None + no_check = False + + +class AuthAddTests(MockerTestCase): + + def setUp(self): + MockerTestCase.setUp(self) + self.saved_stdout = sys.stdout + sys.stdout = StringIO.StringIO() + self.saved_stderr = sys.stderr + sys.stderr = StringIO.StringIO() + + def tearDown(self): + MockerTestCase.tearDown(self) + sys.stdout = self.saved_stdout + sys.stderr = self.saved_stderr + + def make_command(self, auth_backend, **kwargs): + args = FakeArgs() + args.__dict__.update(kwargs) + return auth_add(None, args, auth_backend) + + def test_token_taken_from_argument(self): + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, HOST='http://user:TOKEN@example.com/RPC2/', + no_check=True) + cmd.invoke() + self.assertEqual( + 'TOKEN', + auth_backend.get_token_for_endpoint( + 'user', 'http://example.com/RPC2/')) + + def test_RPC2_implied(self): + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, HOST='http://user:TOKEN@example.com', no_check=True) + cmd.invoke() + self.assertEqual( + 'TOKEN', + auth_backend.get_token_for_endpoint( + 'user', 'http://example.com/RPC2/')) + + def test_scheme_recorded(self): + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, HOST='https://user:TOKEN@example.com/RPC2/', + no_check=True) + cmd.invoke() + self.assertEqual( + None, + auth_backend.get_token_for_endpoint( + 'user', 'http://example.com/RPC2/')) + self.assertEqual( + 'TOKEN', + auth_backend.get_token_for_endpoint( + 'user', 'https://example.com/RPC2/')) + + def test_path_on_server_recorded(self): + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, HOST='https://user:TOKEN@example.com/path', + no_check=True) + cmd.invoke() + self.assertEqual( + 'TOKEN', + auth_backend.get_token_for_endpoint( + 'user', 'https://example.com/path/RPC2/')) + + def test_token_taken_from_getpass(self): + mocked_getpass = self.mocker.replace( + 'getpass.getpass', passthrough=False) + mocked_getpass(CONTAINS('Paste token')) + self.mocker.result("TOKEN") + self.mocker.replay() + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, HOST='http://user@example.com', no_check=True) + cmd.invoke() + self.assertEqual( + 'TOKEN', + auth_backend.get_token_for_endpoint( + 'user', 'http://example.com/RPC2/')) + + def test_token_taken_from_file(self): + auth_backend = MemoryAuthBackend([]) + token_file = tempfile.NamedTemporaryFile('w') + token_file.write("TOKEN") + token_file.flush() + cmd = self.make_command( + auth_backend, HOST='http://user@example.com', no_check=True, + token_file=token_file.name) + cmd.invoke() + self.assertEqual( + 'TOKEN', + auth_backend.get_token_for_endpoint( + 'user', 'http://example.com/RPC2/')) + + def test_token_file_and_in_url_conflict(self): + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, HOST='http://user:TOKEN@example.com', no_check=True, + token_file='some-file-name') + self.assertRaises(LavaCommandError, cmd.invoke) + + def test_non_existent_token_reported(self): + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, HOST='http://user:TOKEN@example.com', no_check=True, + token_file='does-not-exist') + self.assertRaises(LavaCommandError, cmd.invoke) + + def test_user_taken_from_getuser(self): + mocked_getuser = self.mocker.replace( + 'getpass.getuser', passthrough=False) + mocked_getuser() + self.mocker.result("user") + self.mocker.replay() + auth_backend = MemoryAuthBackend([]) + token_file = tempfile.NamedTemporaryFile('w') + token_file.write("TOKEN") + token_file.flush() + cmd = self.make_command( + auth_backend, HOST='http://example.com', no_check=True, + token_file=token_file.name) + cmd.invoke() + self.assertEqual( + 'TOKEN', + auth_backend.get_token_for_endpoint( + 'user', 'http://example.com/RPC2/')) + + def test_port_included(self): + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, + HOST='http://user:TOKEN@example.com:1234', + no_check=True) + cmd.invoke() + self.assertEqual( + 'TOKEN', + auth_backend.get_token_for_endpoint( + 'user', 'http://example.com:1234/RPC2/')) + + def test_check_made(self): + mocked_AuthenticatingServerProxy = self.mocker.replace( + 'lava_tool.authtoken.AuthenticatingServerProxy', passthrough=False) + mocked_sp = mocked_AuthenticatingServerProxy(ARGS, KWARGS) + # nospec() is required because of + # https://bugs.launchpad.net/mocker/+bug/794351 + self.mocker.nospec() + mocked_sp.system.whoami() + self.mocker.result('user') + self.mocker.replay() + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, HOST='http://user:TOKEN@example.com', no_check=False) + cmd.invoke() + self.assertEqual( + 'TOKEN', + auth_backend.get_token_for_endpoint( + 'user', 'http://example.com/RPC2/')) + + def test_check_auth_failure_reported_nicely(self): + mocked_AuthenticatingServerProxy = self.mocker.replace( + 'lava_tool.authtoken.AuthenticatingServerProxy', passthrough=False) + mocked_sp = mocked_AuthenticatingServerProxy(ARGS, KWARGS) + # nospec() is required because of + # https://bugs.launchpad.net/mocker/+bug/794351 + self.mocker.nospec() + mocked_sp.system.whoami() + self.mocker.throw(xmlrpclib.ProtocolError('', 401, '', [])) + self.mocker.replay() + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, HOST='http://user:TOKEN@example.com', no_check=False) + self.assertRaises(LavaCommandError, cmd.invoke) + + def test_check_fails_token_not_recorded(self): + mocked_AuthenticatingServerProxy = self.mocker.replace( + 'lava_tool.authtoken.AuthenticatingServerProxy', passthrough=False) + mocked_sp = mocked_AuthenticatingServerProxy(ARGS, KWARGS) + self.mocker.nospec() + mocked_sp.system.whoami() + self.mocker.throw(xmlrpclib.ProtocolError('', 401, '', [])) + self.mocker.replay() + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, HOST='http://user:TOKEN@example.com', no_check=False) + self.assertRaises(LavaCommandError, cmd.invoke) + self.assertEqual( + None, + auth_backend.get_token_for_endpoint( + 'user', 'http://example.com/RPC2/')) + + def test_check_other_http_failure_just_raised(self): + mocked_AuthenticatingServerProxy = self.mocker.replace( + 'lava_tool.authtoken.AuthenticatingServerProxy', passthrough=False) + mocked_sp = mocked_AuthenticatingServerProxy(ARGS, KWARGS) + # nospec() is required because of + # https://bugs.launchpad.net/mocker/+bug/794351 + self.mocker.nospec() + mocked_sp.system.whoami() + self.mocker.throw(xmlrpclib.ProtocolError('', 500, '', [])) + self.mocker.replay() + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, HOST='http://user:TOKEN@example.com', no_check=False) + self.assertRaises(xmlrpclib.ProtocolError, cmd.invoke) + + def test_fault_reported(self): + mocked_AuthenticatingServerProxy = self.mocker.replace( + 'lava_tool.authtoken.AuthenticatingServerProxy', passthrough=False) + mocked_sp = mocked_AuthenticatingServerProxy(ARGS, KWARGS) + # nospec() is required because of + # https://bugs.launchpad.net/mocker/+bug/794351 + self.mocker.nospec() + mocked_sp.system.whoami() + self.mocker.throw(xmlrpclib.Fault(100, 'faultString')) + self.mocker.replay() + auth_backend = MemoryAuthBackend([]) + cmd = self.make_command( + auth_backend, HOST='http://user:TOKEN@example.com', no_check=False) + self.assertRaises(LavaCommandError, cmd.invoke) diff --git a/lava_tool/tests/test_authtoken.py b/lava_tool/tests/test_authtoken.py new file mode 100644 index 0000000..12e0151 --- /dev/null +++ b/lava_tool/tests/test_authtoken.py @@ -0,0 +1,153 @@ +# Copyright (C) 2011 Linaro Limited +# +# Author: Michael Hudson-Doyle <michael.hudson@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +Unit tests for the lava_tool.authtoken package +""" + +import base64 +import StringIO +from unittest import TestCase +import urlparse +import sys +import xmlrpclib + +from mocker import ARGS, KWARGS, Mocker + +from lava_tool.authtoken import ( + AuthenticatingServerProxy, + MemoryAuthBackend, + ) +from lava_tool.interface import LavaCommandError + +if sys.version_info[:2] <= (2, 6): + TWO_SIX = True +else: + TWO_SIX = False + +class TestAuthenticatingServerProxy(TestCase): + + def auth_headers_for_method_call_on(self, url, auth_backend): + parsed = urlparse.urlparse(url) + expected_host = parsed.hostname + if parsed.port: + expected_host += ':' + str(parsed.port) + server_proxy = AuthenticatingServerProxy( + url, auth_backend=auth_backend) + mocker = Mocker() + if url.startswith('https'): + cls_name = 'httplib.HTTPS' + expected_constructor_args = (expected_host, ARGS) + else: + cls_name = 'httplib.HTTP' + expected_constructor_args = (expected_host, ARGS) + if not TWO_SIX: + cls_name += 'Connection' + mocked_HTTPConnection = mocker.replace(cls_name, passthrough=False) + mocked_connection = mocked_HTTPConnection(*expected_constructor_args) + # nospec() is required because of + # https://bugs.launchpad.net/mocker/+bug/794351 + mocker.nospec() + auth_data = [] + mocked_connection.putrequest(ARGS, KWARGS) + if TWO_SIX: + mocked_connection.send(ARGS, KWARGS) + + def match_header(header, *values): + if header.lower() == 'authorization': + if len(values) != 1: + self.fail( + 'more than one value for ' + 'putheader("Authorization", ...)') + auth_data.append(values[0]) + mocked_connection.putheader(ARGS) + mocker.call(match_header) + mocker.count(1, None) + + mocked_connection.endheaders(ARGS, KWARGS) + + if TWO_SIX: + mocked_connection.getreply(ARGS, KWARGS) + mocker.result((200, None, None)) + s = StringIO.StringIO(xmlrpclib.dumps((1,), methodresponse=True)) + mocked_connection.getfile() + mocker.result(s) + mocked_connection._conn + mocker.result(None) + else: + mocked_connection.getresponse(ARGS, KWARGS) + s = StringIO.StringIO(xmlrpclib.dumps((1,), methodresponse=True)) + s.status = 200 + mocker.result(s) + + mocked_connection.close() + mocker.count(0, 1) + + with mocker: + server_proxy.method() + + return auth_data + + def user_and_password_from_auth_data(self, auth_data): + if len(auth_data) != 1: + self.fail("expected exactly 1 header, got %r" % len(auth_data)) + [value] = auth_data + if not value.startswith("Basic "): + self.fail("non-basic auth header found in %r" % auth_data) + auth = base64.b64decode(value[len("Basic "):]) + if ':' in auth: + return tuple(auth.split(':', 1)) + else: + return (auth, None) + + def test_no_user_no_auth(self): + auth_headers = self.auth_headers_for_method_call_on( + 'http://localhost/RPC2/', MemoryAuthBackend([])) + self.assertEqual([], auth_headers) + + def test_token_used_for_auth_http(self): + auth_headers = self.auth_headers_for_method_call_on( + 'http://user@localhost/RPC2/', + MemoryAuthBackend([('user', 'http://localhost/RPC2/', 'TOKEN')])) + self.assertEqual( + ('user', 'TOKEN'), + self.user_and_password_from_auth_data(auth_headers)) + + def test_token_used_for_auth_https(self): + auth_headers = self.auth_headers_for_method_call_on( + 'https://user@localhost/RPC2/', + MemoryAuthBackend([('user', 'https://localhost/RPC2/', 'TOKEN')])) + self.assertEqual( + ('user', 'TOKEN'), + self.user_and_password_from_auth_data(auth_headers)) + + def test_port_included(self): + auth_headers = self.auth_headers_for_method_call_on( + 'http://user@localhost:1234/RPC2/', + MemoryAuthBackend( + [('user', 'http://localhost:1234/RPC2/', 'TOKEN')])) + self.assertEqual( + ('user', 'TOKEN'), + self.user_and_password_from_auth_data(auth_headers)) + + def test_error_when_user_but_no_token(self): + self.assertRaises( + LavaCommandError, + self.auth_headers_for_method_call_on, + 'http://user@localhost/RPC2/', + MemoryAuthBackend([])) diff --git a/lava_tool/tests/test_commands.py b/lava_tool/tests/test_commands.py new file mode 100644 index 0000000..0961b6a --- /dev/null +++ b/lava_tool/tests/test_commands.py @@ -0,0 +1,137 @@ +# Copyright (C) 2010 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +""" +Unit tests for the launch_control.commands package +""" + +from mocker import MockerTestCase + +from lava_tool.interface import ( + Command, + LavaCommandError, + ) +from lava_tool.dispatcher import ( + LavaDispatcher, + main, + ) + + +class CommandTestCase(MockerTestCase): + + def test_register_arguments_does_nothing(self): + parser = self.mocker.mock() + self.mocker.replay() + Command.register_arguments(parser) + + def test_not_implemented(self): + self.assertRaises(NotImplementedError, Command(None, None).invoke) + + def test_get_name_uses_class_name(self): + class Foo(Command): + pass + self.assertEqual(Foo.get_name(), "Foo") + + def test_get_name_strips_leading_underscore(self): + class _Bar(Command): + pass + self.assertEqual(_Bar.get_name(), "Bar") + + def test_get_name_converts_underscore_to_dash(self): + class froz_bot(Command): + pass + self.assertEqual(froz_bot.get_name(), "froz-bot") + + def test_get_help_uses_docstring(self): + class ASDF(Command): + """ + This command was named after the lisp package management system + """ + self.assertEqual( + ASDF.get_help(), + 'This command was named after the lisp package management system') + + def test_get_help_defaults_to_None(self): + class mysterious(Command): + pass + + self.assertEqual(mysterious.get_help(), None) + + def test_get_epilog_defaults_to_None(self): + class mysterious(Command): + pass + self.assertEqual(mysterious.get_epilog(), None) + + def test_get_epilog_returns_data_after_carriage_L(self): + # The dot after 'before' is to make pep8 happy + class help_with_epilog(Command): + """ + before + . + after + """ + self.assertEqual(help_with_epilog.get_epilog(), "after") + + def test_get_help_returns_data_before_carriage_L(self): + # The dot after 'before' is to make pep8 happy + class help_with_epilog(Command): + """ + before + . + after + """ + self.assertEqual(help_with_epilog.get_help(), "before\n.") + + +class DispatcherTestCase(MockerTestCase): + + def test_main(self): + mock_LavaDispatcher = self.mocker.replace( + 'lava_tool.dispatcher.LavaDispatcher') + mock_LavaDispatcher().dispatch() + self.mocker.replay() + self.assertRaises(SystemExit, main) + + def test_add_command_cls(self): + test_calls = [] + + class test(Command): + + def invoke(self): + test_calls.append(None) + + dispatcher = LavaDispatcher() + dispatcher.add_command_cls(test) + dispatcher.dispatch(raw_args=['test']) + self.assertEqual(1, len(test_calls)) + + def test_print_LavaCommandError_nicely(self): + stderr = self.mocker.replace('sys.stderr', passthrough=False) + stderr.write("ERROR: error message") + stderr.write("\n") + self.mocker.replay() + + class error(Command): + + def invoke(self): + raise LavaCommandError("error message") + + dispatcher = LavaDispatcher() + dispatcher.add_command_cls(error) + exit_code = dispatcher.dispatch(raw_args=['error']) + self.assertEquals(1, exit_code) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..a98a913 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,8 @@ +[upload] +sign = True + +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..a41f47e --- /dev/null +++ b/setup.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# +# Copyright (C) 2010 Linaro Limited +# +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> +# +# This file is part of lava-tool. +# +# lava-tool is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation +# +# lava-tool 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with lava-tool. If not, see <http://www.gnu.org/licenses/>. + +from setuptools import setup, find_packages + + +setup( + name='lava-tool', + version=":versiontools:lava.tool:__version__", + author="Zygmunt Krynicki", + author_email="zygmunt.krynicki@linaro.org", + namespace_packages=['lava'], + packages=find_packages(), + description="Command line utility for Linaro validation services", + url='https://launchpad.net/lava-tool', + test_suite='lava_tool.tests.test_suite', + license="LGPLv3", + entry_points=""" + [console_scripts] + lava-tool = lava_tool.dispatcher:main + lava = lava.tool.main:LavaDispatcher.run + [lava.commands] + help = lava.tool.commands.help:help + [lava_tool.commands] + help = lava.tool.commands.help:help + auth-add = lava_tool.commands.auth:auth_add [auth] + """, + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + ("License :: OSI Approved :: GNU Library or Lesser General Public" + " License (LGPL)"), + "Operating System :: OS Independent", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Topic :: Software Development :: Testing", + ], + extras_require={'auth': ['keyring']}, + install_requires=['argparse >= 1.1'], + setup_requires=['versiontools >= 1.3.1'], + tests_require=['mocker >= 1.0'], + zip_safe=True) |