aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntonio Terceiro <antonio.terceiro@linaro.org>2013-04-30 16:10:15 -0300
committerAntonio Terceiro <antonio.terceiro@linaro.org>2013-04-30 16:10:15 -0300
commit97af8e8d8c9448e3f2f84392ac537f7a4adb6fe4 (patch)
treec3da2cd57b9199f5eb0e8db61c081b5b51b59501
Imported Upstream version 0.6
-rw-r--r--PKG-INFO17
-rw-r--r--README21
-rw-r--r--lava/__init__.py3
-rw-r--r--lava/tool/__init__.py27
-rw-r--r--lava/tool/command.py166
-rw-r--r--lava/tool/commands/__init__.py83
-rw-r--r--lava/tool/commands/help.py35
-rw-r--r--lava/tool/dispatcher.py155
-rw-r--r--lava/tool/errors.py31
-rw-r--r--lava/tool/main.py132
-rw-r--r--lava_tool.egg-info/PKG-INFO17
-rw-r--r--lava_tool.egg-info/SOURCES.txt29
-rw-r--r--lava_tool.egg-info/dependency_links.txt1
-rw-r--r--lava_tool.egg-info/entry_points.txt10
-rw-r--r--lava_tool.egg-info/namespace_packages.txt1
-rw-r--r--lava_tool.egg-info/requires.txt4
-rw-r--r--lava_tool.egg-info/top_level.txt2
-rw-r--r--lava_tool.egg-info/zip-safe1
-rw-r--r--lava_tool/__init__.py24
-rw-r--r--lava_tool/authtoken.py107
-rw-r--r--lava_tool/commands/__init__.py24
-rw-r--r--lava_tool/commands/auth.py127
-rw-r--r--lava_tool/dispatcher.py49
-rw-r--r--lava_tool/interface.py24
-rw-r--r--lava_tool/tests/__init__.py57
-rw-r--r--lava_tool/tests/test_auth_commands.py259
-rw-r--r--lava_tool/tests/test_authtoken.py153
-rw-r--r--lava_tool/tests/test_commands.py137
-rw-r--r--setup.cfg8
-rwxr-xr-xsetup.py59
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
diff --git a/README b/README
new file mode 100644
index 0000000..00755cf
--- /dev/null
+++ b/README
@@ -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)