diff options
Diffstat (limited to 'lava_dashboard_tool/commands.py')
-rw-r--r-- | lava_dashboard_tool/commands.py | 239 |
1 files changed, 149 insertions, 90 deletions
diff --git a/lava_dashboard_tool/commands.py b/lava_dashboard_tool/commands.py index 1159681..1d6a25e 100644 --- a/lava_dashboard_tool/commands.py +++ b/lava_dashboard_tool/commands.py @@ -37,11 +37,11 @@ import simplejson from json_schema_validator.extensions import datetime_extension from lava_tool.authtoken import AuthenticatingServerProxy, KeyringAuthBackend -from lava.tool.commands import ExperimentalCommandMixIn from lava.tool.command import Command, CommandGroup class dashboard(CommandGroup): + """ Commands for interacting with LAVA Dashboard """ @@ -50,16 +50,19 @@ class dashboard(CommandGroup): class InsufficientServerVersion(Exception): + """ Exception raised when server version that a command interacts with is too old to support required features. """ + def __init__(self, server_version, required_version): self.server_version = server_version self.required_version = required_version class DataSetRenderer(object): + """ Support class for rendering a table out of list of dictionaries. @@ -77,8 +80,10 @@ class DataSetRenderer(object): Each dictionary must have the same keys. In particular the first row is used to determine columns. """ + def __init__(self, column_map=None, row_formatter=None, empty=None, - order=None, caption=None, separator=" ", header_separator=None): + order=None, caption=None, separator=" ", + header_separator=None): if column_map is None: column_map = {} if row_formatter is None: @@ -179,11 +184,11 @@ class DataSetRenderer(object): if column in self.row_formatter: row[column] = self.row_formatter[column](row[column]) maxlen = dict( - [(column, max( - len(self.column_map.get(column, column)), - max([ - len(str(row[column])) for row in dataset_out]))) - for column in columns]) + [(column, max( + len(self.column_map.get(column, column)), + max([ + len(str(row[column])) for row in dataset_out]))) + for column in columns]) return dataset_out, columns, maxlen def _render_header(self, dataset, columns, maxlen): @@ -244,7 +249,7 @@ class DataSetRenderer(object): # Now print the coulum names print self.separator.join([ "{0:^{1}}".format(self.column_map.get(column, column), - maxlen[column]) for column in columns]) + maxlen[column]) for column in columns]) # Finally print the header separator if self.header_separator: print "-" * total_len @@ -265,7 +270,7 @@ class DataSetRenderer(object): >>> maxlen = {'a': 13, 'bee': 3} Now a plain table. Note! To really understand this test - you should check out the length of the strings below. There + you should check out the whitespace in the strings below. There are two more spaces after 'b' in the second row >>> DataSetRenderer()._render_rows(dataset, columns, maxlen) shorter @@ -306,6 +311,7 @@ class DataSetRenderer(object): class XMLRPCCommand(Command): + """ Abstract base class for commands that interact with dashboard server over XML-RPC. @@ -344,7 +350,7 @@ class XMLRPCCommand(Command): '0.4.0b2' """ try: - major, minor, micro, releaselevel, serial = version.split(".") + major, minor, micro, releaselevel, serial = version.split(".") except ValueError: raise ValueError( ("version %r does not follow pattern " @@ -377,7 +383,7 @@ class XMLRPCCommand(Command): def __init__(self, parser, args): super(XMLRPCCommand, self).__init__(parser, args) - xml_rpc_url = self._construct_xml_rpc_url(self.args.dashboard_url) + xml_rpc_url = self._construct_xml_rpc_url(self.args.dashboard_url) self.server = AuthenticatingServerProxy( xml_rpc_url, verbose=args.verbose_xml_rpc, @@ -393,19 +399,20 @@ class XMLRPCCommand(Command): @classmethod def register_arguments(cls, parser): - dashboard_group = parser.add_argument_group("dashboard specific arguments") + dashboard_group = parser.add_argument_group( + "dashboard specific arguments") default_dashboard_url = os.getenv("DASHBOARD_URL") if default_dashboard_url: dashboard_group.add_argument("--dashboard-url", - metavar="URL", help="URL of your validation dashboard (currently %(default)s)", - default=default_dashboard_url) + metavar="URL", help="URL of your validation dashboard (currently %(default)s)", + default=default_dashboard_url) else: dashboard_group.add_argument("--dashboard-url", required=True, - metavar="URL", help="URL of your validation dashboard") + metavar="URL", help="URL of your validation dashboard") debug_group = parser.add_argument_group("debugging arguments") debug_group.add_argument("--verbose-xml-rpc", - action="store_true", default=False, - help="Show XML-RPC data") + action="store_true", default=False, + help="Show XML-RPC data") return dashboard_group @contextlib.contextmanager @@ -414,7 +421,7 @@ class XMLRPCCommand(Command): yield except socket.error as ex: print >> sys.stderr, "Unable to connect to server at %s" % ( - self.args.dashboard_url,) + self.args.dashboard_url,) # It seems that some errors are reported as -errno # while others as +errno. ex.errno = abs(ex.errno) @@ -429,12 +436,13 @@ class XMLRPCCommand(Command): print >> sys.stderr, "Socket %d: %s" % (ex.errno, ex.strerror) except xmlrpclib.ProtocolError as ex: print >> sys.stderr, "Unable to exchange XML-RPC message with dashboard server" - print >> sys.stderr, "HTTP error code: %d/%s" % (ex.errcode, ex.errmsg) + print >> sys.stderr, "HTTP error code: %d/%s" % ( + ex.errcode, ex.errmsg) except xmlrpclib.Fault as ex: self.handle_xmlrpc_fault(ex.faultCode, ex.faultString) except InsufficientServerVersion as ex: print >> sys.stderr, ("This command requires at least server version " - "%s, actual server version is %s" % + "%s, actual server version is %s" % (ex.required_version, ex.server_version)) def invoke(self): @@ -447,13 +455,15 @@ class XMLRPCCommand(Command): print >> sys.stderr, "Dashboard server has experienced internal error" print >> sys.stderr, faultString else: - print >> sys.stderr, "XML-RPC error %d: %s" % (faultCode, faultString) + print >> sys.stderr, "XML-RPC error %d: %s" % ( + faultCode, faultString) def invoke_remote(self): raise NotImplementedError() class server_version(XMLRPCCommand): + """ Display dashboard server version """ @@ -463,6 +473,7 @@ class server_version(XMLRPCCommand): class put(XMLRPCCommand): + """ Upload a bundle on the server """ @@ -471,11 +482,11 @@ class put(XMLRPCCommand): def register_arguments(cls, parser): super(put, cls).register_arguments(parser) parser.add_argument("LOCAL", - type=argparse.FileType("rb"), - help="pathname on the local file system") + type=argparse.FileType("rb"), + help="pathname on the local file system") parser.add_argument("REMOTE", - default="/anonymous/", nargs='?', - help="pathname on the server") + default="/anonymous/", nargs='?', + help="pathname on the server") def invoke_remote(self): content = self.args.LOCAL.read() @@ -487,7 +498,7 @@ class put(XMLRPCCommand): def handle_xmlrpc_fault(self, faultCode, faultString): if faultCode == 404: print >> sys.stderr, "Bundle stream %s does not exist" % ( - self.args.REMOTE) + self.args.REMOTE) elif faultCode == 409: print >> sys.stderr, "You have already uploaded this bundle to the dashboard" else: @@ -495,6 +506,7 @@ class put(XMLRPCCommand): class get(XMLRPCCommand): + """ Download a bundle from the server """ @@ -503,15 +515,15 @@ class get(XMLRPCCommand): def register_arguments(cls, parser): super(get, cls).register_arguments(parser) parser.add_argument("SHA1", - type=str, - help="SHA1 of the bundle to download") + type=str, + help="SHA1 of the bundle to download") parser.add_argument("--overwrite", - action="store_true", - help="Overwrite files on the local disk") + action="store_true", + help="Overwrite files on the local disk") parser.add_argument("--output", "-o", - type=argparse.FileType("wb"), - default=None, - help="Alternate name of the output file") + type=argparse.FileType("wb"), + default=None, + help="Alternate name of the output file") def invoke_remote(self): response = self.server.get(self.args.SHA1) @@ -519,7 +531,7 @@ class get(XMLRPCCommand): filename = self.args.SHA1 if os.path.exists(filename) and not self.args.overwrite: print >> sys.stderr, "File {filename!r} already exists".format( - filename=filename) + filename=filename) print >> sys.stderr, "You may pass --overwrite to write over it" return -1 stream = open(filename, "wb") @@ -528,17 +540,18 @@ class get(XMLRPCCommand): filename = self.args.output.name stream.write(response['content']) print "Downloaded bundle {0} to file {1!r}".format( - self.args.SHA1, filename) + self.args.SHA1, filename) def handle_xmlrpc_fault(self, faultCode, faultString): if faultCode == 404: print >> sys.stderr, "Bundle {sha1} does not exist".format( - sha1=self.args.SHA1) + sha1=self.args.SHA1) else: super(get, self).handle_xmlrpc_fault(faultCode, faultString) class deserialize(XMLRPCCommand): + """ Deserialize a bundle on the server """ @@ -547,8 +560,8 @@ class deserialize(XMLRPCCommand): def register_arguments(cls, parser): super(deserialize, cls).register_arguments(parser) parser.add_argument("SHA1", - type=str, - help="SHA1 of the bundle to deserialize") + type=str, + help="SHA1 of the bundle to deserialize") def invoke_remote(self): response = self.server.deserialize(self.args.SHA1) @@ -558,13 +571,17 @@ class deserialize(XMLRPCCommand): def handle_xmlrpc_fault(self, faultCode, faultString): if faultCode == 404: print >> sys.stderr, "Bundle {sha1} does not exist".format( - sha1=self.args.SHA1) + sha1=self.args.SHA1) elif faultCode == 409: print >> sys.stderr, "Unable to deserialize bundle {sha1}".format( sha1=self.args.SHA1) print >> sys.stderr, faultString else: - super(deserialize, self).handle_xmlrpc_fault(faultCode, faultString) + super( + deserialize, + self).handle_xmlrpc_fault( + faultCode, + faultString) def _get_pretty_renderer(**kwargs): @@ -576,6 +593,7 @@ def _get_pretty_renderer(**kwargs): class streams(XMLRPCCommand): + """ Show streams you have access to """ @@ -596,33 +614,34 @@ class streams(XMLRPCCommand): class bundles(XMLRPCCommand): + """ Show bundles in the specified stream """ renderer = _get_pretty_renderer( - column_map={ - 'uploaded_by': 'Uploader', - 'uploaded_on': 'Upload date', - 'content_filename': 'File name', - 'content_sha1': 'SHA1', - 'is_deserialized': "Deserialized?"}, - row_formatter={ - 'is_deserialized': lambda x: "yes" if x else "no", - 'uploaded_by': lambda x: x or "(anonymous)", - 'uploaded_on': lambda x: x}, - order=('content_sha1', 'content_filename', 'uploaded_by', - 'uploaded_on', 'is_deserialized'), - empty="There are no bundles in this stream", - caption="Bundles", - separator=" | ") + column_map={ + 'uploaded_by': 'Uploader', + 'uploaded_on': 'Upload date', + 'content_filename': 'File name', + 'content_sha1': 'SHA1', + 'is_deserialized': "Deserialized?"}, + row_formatter={ + 'is_deserialized': lambda x: "yes" if x else "no", + 'uploaded_by': lambda x: x or "(anonymous)", + 'uploaded_on': lambda x: x}, + order=('content_sha1', 'content_filename', 'uploaded_by', + 'uploaded_on', 'is_deserialized'), + empty="There are no bundles in this stream", + caption="Bundles", + separator=" | ") @classmethod def register_arguments(cls, parser): super(bundles, cls).register_arguments(parser) parser.add_argument("PATHNAME", - default="/anonymous/", nargs='?', - help="pathname on the server (defaults to %(default)s)") + default="/anonymous/", nargs='?', + help="pathname on the server (defaults to %(default)s)") def invoke_remote(self): self.renderer.render(self.server.bundles(self.args.PATHNAME)) @@ -630,12 +649,13 @@ class bundles(XMLRPCCommand): def handle_xmlrpc_fault(self, faultCode, faultString): if faultCode == 404: print >> sys.stderr, "Bundle stream %s does not exist" % ( - self.args.PATHNAME) + self.args.PATHNAME) else: super(bundles, self).handle_xmlrpc_fault(faultCode, faultString) class make_stream(XMLRPCCommand): + """ Create a bundle stream on the server """ @@ -660,9 +680,9 @@ class make_stream(XMLRPCCommand): class backup(XMLRPCCommand): + """ Backup data uploaded to a dashboard instance. - Not all data is preserved. The following data is lost: identity of the user that uploaded each bundle, time of uploading and deserialization on the server, name of the bundle stream that contained the data @@ -679,7 +699,9 @@ class backup(XMLRPCCommand): os.mkdir(self.args.BACKUP_DIR) for bundle_stream in self.server.streams(): print "Processing stream %s" % bundle_stream["pathname"] - bundle_stream_dir = os.path.join(self.args.BACKUP_DIR, urllib.quote_plus(bundle_stream["pathname"])) + bundle_stream_dir = os.path.join( + self.args.BACKUP_DIR, + urllib.quote_plus(bundle_stream["pathname"])) if not os.path.exists(bundle_stream_dir): os.mkdir(bundle_stream_dir) with open(os.path.join(bundle_stream_dir, "metadata.json"), "wt") as stream: @@ -692,14 +714,18 @@ class backup(XMLRPCCommand): for bundle in self.server.bundles(bundle_stream["pathname"]): print " * Backing up bundle %s" % bundle["content_sha1"] data = self.server.get(bundle["content_sha1"]) - bundle_pathname = os.path.join(bundle_stream_dir, bundle["content_sha1"]) - # Note: we write bundles as binary data to preserve anything the user might have dumped on us + bundle_pathname = os.path.join( + bundle_stream_dir, + bundle["content_sha1"]) + # Note: we write bundles as binary data to preserve anything + # the user might have dumped on us with open(bundle_pathname + ".json", "wb") as stream: stream.write(data["content"]) with open(bundle_pathname + ".metadata.json", "wt") as stream: simplejson.dump({ "uploaded_by": bundle["uploaded_by"], - "uploaded_on": datetime_extension.to_json(bundle["uploaded_on"]), + "uploaded_on": + datetime_extension.to_json(bundle["uploaded_on"]), "content_filename": bundle["content_filename"], "content_sha1": bundle["content_sha1"], "content_size": bundle["content_size"], @@ -707,6 +733,7 @@ class backup(XMLRPCCommand): class restore(XMLRPCCommand): + """ Restore a dashboard instance from backup """ @@ -720,7 +747,9 @@ class restore(XMLRPCCommand): def invoke_remote(self): self._check_server_version(self.server, "0.3") for stream_pathname_quoted in os.listdir(self.args.BACKUP_DIR): - filesystem_stream_pathname = os.path.join(self.args.BACKUP_DIR, stream_pathname_quoted) + filesystem_stream_pathname = os.path.join( + self.args.BACKUP_DIR, + stream_pathname_quoted) if not os.path.isdir(filesystem_stream_pathname): continue stream_pathname = urllib.unquote(stream_pathname_quoted) @@ -731,12 +760,18 @@ class restore(XMLRPCCommand): stream_metadata = {} print "Processing stream %s" % stream_pathname try: - self.server.make_stream(stream_pathname, stream_metadata.get("name", "Restored from backup")) + self.server.make_stream( + stream_pathname, + stream_metadata.get( + "name", + "Restored from backup")) except xmlrpclib.Fault as ex: if ex.faultCode != 409: raise for content_sha1 in [item[:-len(".json")] for item in os.listdir(filesystem_stream_pathname) if item.endswith(".json") and not item.endswith(".metadata.json") and item != "metadata.json"]: - filesystem_content_filename = os.path.join(filesystem_stream_pathname, content_sha1 + ".json") + filesystem_content_filename = os.path.join( + filesystem_stream_pathname, + content_sha1 + ".json") if not os.path.isfile(filesystem_content_filename): continue with open(os.path.join(filesystem_stream_pathname, content_sha1) + ".metadata.json", "rt") as stream: @@ -745,16 +780,18 @@ class restore(XMLRPCCommand): content = stream.read() print " * Restoring bundle %s" % content_sha1 try: - self.server.put(content, bundle_metadata["content_filename"], stream_pathname) + self.server.put( + content, + bundle_metadata["content_filename"], + stream_pathname) except xmlrpclib.Fault as ex: if ex.faultCode != 409: raise - -class pull(ExperimentalCommandMixIn, XMLRPCCommand): + +class pull(XMLRPCCommand): """ Copy bundles and bundle streams from one dashboard to another. - This command checks for two environment varialbes: The value of DASHBOARD_URL is used as a replacement for --dashbard-url. The value of REMOTE_DASHBOARD_URL as a replacement for FROM. @@ -785,7 +822,10 @@ class pull(ExperimentalCommandMixIn, XMLRPCCommand): group.add_argument( "FROM", help="URL of the remote validation dashboard)") - group.add_argument("STREAM", nargs="*", help="Streams to pull from (all by default)") + group.add_argument( + "STREAM", + nargs="*", + help="Streams to pull from (all by default)") @staticmethod def _filesizeformat(num_bytes): @@ -810,7 +850,7 @@ class pull(ExperimentalCommandMixIn, XMLRPCCommand): def invoke_remote(self): self._check_server_version(self.server, "0.3") - + print "Checking local and remote streams" remote = self.remote_server.streams() if self.args.STREAM: @@ -819,28 +859,37 @@ class pull(ExperimentalCommandMixIn, XMLRPCCommand): remote_set = frozenset((stream["pathname"] for stream in remote)) unavailable_set = requested_set - remote_set if unavailable_set: - print >> sys.stderr, "Remote stream not found: %s" % ", ".join(unavailable_set) + print >> sys.stderr, "Remote stream not found: %s" % ", ".join( + unavailable_set) return -1 # Limit to requested streams if necessary - remote = [stream for stream in remote if stream["pathname"] in requested_set] + remote = [ + stream for stream in remote if stream[ + "pathname"] in requested_set] local = self.server.streams() - missing_pathnames = set([stream["pathname"] for stream in remote]) - set([stream["pathname"] for stream in local]) + missing_pathnames = set([stream["pathname"] + for stream in remote]) - set([stream["pathname"] for stream in local]) for stream in remote: if stream["pathname"] in missing_pathnames: self.server.make_stream(stream["pathname"], stream["name"]) local_bundles = [] else: - local_bundles = [bundle for bundle in self.server.bundles(stream["pathname"])] - remote_bundles = [bundle for bundle in self.remote_server.bundles(stream["pathname"])] - missing_bundles = set((bundle["content_sha1"] for bundle in remote_bundles)) - missing_bundles -= set((bundle["content_sha1"] for bundle in local_bundles)) + local_bundles = [ + bundle for bundle in self.server.bundles(stream["pathname"])] + remote_bundles = [ + bundle for bundle in self.remote_server.bundles(stream["pathname"])] + missing_bundles = set( + (bundle["content_sha1"] for bundle in remote_bundles)) + missing_bundles -= set( + (bundle["content_sha1"] for bundle in local_bundles)) try: missing_bytes = sum( (bundle["content_size"] for bundle in remote_bundles if bundle["content_sha1"] in missing_bundles)) except KeyError as ex: - # Older servers did not return content_size so this part is optional + # Older servers did not return content_size so this part is + # optional missing_bytes = None if missing_bytes: print "Stream %s needs update (%s)" % (stream["pathname"], self._filesizeformat(missing_bytes)) @@ -855,17 +904,20 @@ class pull(ExperimentalCommandMixIn, XMLRPCCommand): print "got %s, storing" % (self._filesizeformat(len(data["content"]))), sys.stdout.flush() try: - self.server.put(data["content"], data["content_filename"], stream["pathname"]) + self.server.put( + data["content"], + data["content_filename"], + stream["pathname"]) except xmlrpclib.Fault as ex: if ex.faultCode == 409: # duplicate print "already present (in another stream)" else: raise - else: + else: print "done" -class data_views(ExperimentalCommandMixIn, XMLRPCCommand): +class data_views(XMLRPCCommand): """ Show data views defined on the server """ @@ -885,7 +937,7 @@ class data_views(ExperimentalCommandMixIn, XMLRPCCommand): print "Tip: to invoke a data view try `lc-tool query-data-view`" -class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand): +class query_data_view(XMLRPCCommand): """ Invoke a specified data view """ @@ -893,7 +945,8 @@ class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand): def register_arguments(cls, parser): super(query_data_view, cls).register_arguments(parser) parser.add_argument("QUERY", metavar="QUERY", nargs="...", - help="Data view name and any optional and required arguments") + help="Data view name and any optional \ + and required arguments") def _probe_data_views(self): """ @@ -914,7 +967,7 @@ class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand): del parser._actions[-1] subparsers = parser.add_subparsers( title="Data views available on the server") - for data_view in self.data_views: + for data_view in self.data_views: data_view_parser = subparsers.add_parser( data_view["name"], help=data_view["summary"], @@ -924,14 +977,16 @@ class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand): for argument in data_view["arguments"]: if argument["default"] is None: group.add_argument( - "--{name}".format(name=argument["name"].replace("_", "-")), + "--{name}".format( + name=argument["name"].replace("_", "-")), dest=argument["name"], help=argument["help"], type=str, required=True) else: group.add_argument( - "--{name}".format(name=argument["name"].replace("_", "-")), + "--{name}".format( + name=argument["name"].replace("_", "-")), dest=argument["name"], help=argument["help"], type=str, @@ -953,9 +1008,11 @@ class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand): for argument in self.args.data_view["arguments"]: arg_name = argument["name"] if arg_name in self.args: - data_view_args[arg_name] = getattr(self.args, arg_name) + data_view_args[arg_name] = getattr(self.args, arg_name) # Invoke the data view - response = self.server.query_data_view(self.args.data_view["name"], data_view_args) + response = self.server.query_data_view( + self.args.data_view["name"], + data_view_args) # Create a pretty-printer renderer = _get_pretty_renderer( caption=self.args.data_view["summary"], @@ -971,9 +1028,11 @@ class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand): class version(Command): + """ Show dashboard client version """ + def invoke(self): import versiontools from lava_dashboard_tool import __version__ |