1
# Copyright (C) 2010,2011 Linaro Limited
3
# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
5
# This file is part of lava-dashboard-tool.
7
# lava-dashboard-tool is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU Lesser General Public License version 3
9
# as published by the Free Software Foundation
11
# lava-dashboard-tool is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU Lesser General Public License
17
# along with lava-dashboard-tool. If not, see <http://www.gnu.org/licenses/>.
20
Module with command-line tool commands that interact with the dashboard
21
server. All commands listed here should have counterparts in the
22
launch_control.dashboard_app.xml_rpc package.
37
from json_schema_validator.extensions import datetime_extension
39
from lava_tool.authtoken import AuthenticatingServerProxy, KeyringAuthBackend
40
from lava.tool.commands import ExperimentalCommandMixIn
41
from lava.tool.command import Command, CommandGroup
44
class dashboard(CommandGroup):
46
Commands for interacting with LAVA Dashboard
49
namespace = "lava.dashboard.commands"
52
class InsufficientServerVersion(Exception):
54
Exception raised when server version that a command interacts with is too
55
old to support required features.
57
def __init__(self, server_version, required_version):
58
self.server_version = server_version
59
self.required_version = required_version
62
class DataSetRenderer(object):
64
Support class for rendering a table out of list of dictionaries.
66
It supports several features that make printing tabular data easier.
68
* Custom column headers
69
* Custom cell formatting
70
* Custom table captions
71
* Custom column ordering
72
* Custom Column separators
73
* Custom dataset notification
75
The primary method is render() which does all of the above. You
76
need to pass a dataset argument which is a list of dictionaries.
77
Each dictionary must have the same keys. In particular the first row
78
is used to determine columns.
80
def __init__(self, column_map=None, row_formatter=None, empty=None,
81
order=None, caption=None, separator=" ", header_separator=None):
82
if column_map is None:
84
if row_formatter is None:
87
empty = "There is no data to display"
88
self.column_map = column_map
89
self.row_formatter = row_formatter
92
self.separator = separator
93
self.caption = caption
94
self.header_separator = header_separator
96
def _analyze_dataset(self, dataset):
98
Determine the columns that will be displayed and the maximum
99
length of each of those columns.
101
Returns a tuple (dataset, columms, maxlen) where columns is a
102
list of column names and maxlen is a dictionary mapping from
103
column name to maximum length of any value in the row or the
104
column header and the dataset is a copy of the dataset altered
109
First the dataset, an array of dictionaries
111
... {'a': 'shorter', 'bee': ''},
112
... {'a': 'little longer', 'bee': 'b'}]
114
Note that column 'bee' is actually three characters long as the
115
column name made it wider.
116
>>> dataset_out, columns, maxlen = DataSetRenderer(
117
... )._analyze_dataset(dataset)
119
Unless you format rows with a custom function the data is not altered.
120
>>> dataset_out is dataset
123
Columns come out in sorted alphabetic order
127
Maximum length determines the width of each column. Note that
128
the header affects the column width.
132
You can constrain or reorder columns. In that case columns you
133
decided to ignore are simply left out of the output.
134
>>> dataset_out, columns, maxlen = DataSetRenderer(
135
... order=['bee'])._analyze_dataset(dataset)
141
You can format values anyway you like:
142
>>> dataset_out, columns, maxlen = DataSetRenderer(row_formatter={
143
... 'bee': lambda value: "%10s" % value}
144
... )._analyze_dataset(dataset)
146
Dataset is altered to take account of the row formatting
147
function. The original dataset argument is copied.
149
[{'a': 'shorter', 'bee': ' '}, {'a': 'little longer', 'bee': ' b'}]
150
>>> dataset_out is not dataset
153
Columns stay the same though:
157
Note how formatting altered the width of the column 'bee'
161
You can also format columns (with nice aliases).Note how
162
column 'bee' maximum width is now dominated by the long column
164
>>> dataset_out, columns, maxlen = DataSetRenderer(column_map={
165
... 'bee': "Column B"})._analyze_dataset(dataset)
172
columns = sorted(dataset[0].keys())
173
if self.row_formatter:
174
dataset_out = [dict(row) for row in dataset]
176
dataset_out = dataset
177
for row in dataset_out:
179
if column in self.row_formatter:
180
row[column] = self.row_formatter[column](row[column])
183
len(self.column_map.get(column, column)),
185
len(str(row[column])) for row in dataset_out])))
186
for column in columns])
187
return dataset_out, columns, maxlen
189
def _render_header(self, dataset, columns, maxlen):
191
Render a header, possibly with a caption string
193
Caption is controlled by the constructor.
195
... {'a': 'shorter', 'bee': ''},
196
... {'a': 'little longer', 'bee': 'b'}]
197
>>> columns = ['a', 'bee']
198
>>> maxlen = {'a': 13, 'bee': 3}
200
By default there is no caption, just column names:
201
>>> DataSetRenderer()._render_header(
202
... dataset, columns, maxlen)
205
If you enable the header separator then column names will be visually
206
separated from the first row of data.
207
>>> DataSetRenderer(header_separator=True)._render_header(
208
... dataset, columns, maxlen)
212
If you provide a caption it gets rendered as a centered
213
underlined text before the data:
214
>>> DataSetRenderer(caption="Dataset")._render_header(
215
... dataset, columns, maxlen)
220
You can use both caption and header separator
221
>>> DataSetRenderer(caption="Dataset", header_separator=True)._render_header(
222
... dataset, columns, maxlen)
228
Observe how the total length of the output horizontal line
229
depends on the separator! Also note the columns labels are
230
aligned to the center of the column
231
>>> DataSetRenderer(caption="Dataset", separator=" | ")._render_header(
232
... dataset, columns, maxlen)
237
total_len = sum(maxlen.itervalues())
239
total_len += len(self.separator) * (len(columns) - 1)
242
print "{0:^{1}}".format(self.caption, total_len)
243
print "=" * total_len
244
# Now print the coulum names
245
print self.separator.join([
246
"{0:^{1}}".format(self.column_map.get(column, column),
247
maxlen[column]) for column in columns])
248
# Finally print the header separator
249
if self.header_separator:
250
print "-" * total_len
252
def _render_rows(self, dataset, columns, maxlen):
254
Render rows of the dataset.
256
Each row is printed on one line using the maxlen argument to
257
determine correct column size. Text is aligned left.
259
First the dataset, columns and maxlen as produced by
262
... {'a': 'shorter', 'bee': ''},
263
... {'a': 'little longer', 'bee': 'b'}]
264
>>> columns = ['a', 'bee']
265
>>> maxlen = {'a': 13, 'bee': 3}
267
Now a plain table. Note! To really understand this test
268
you should check out the length of the strings below. There
269
are two more spaces after 'b' in the second row
270
>>> DataSetRenderer()._render_rows(dataset, columns, maxlen)
275
print self.separator.join([
276
"{0!s:{1}}".format(row[column], maxlen[column])
277
for column in columns])
279
def _render_dataset(self, dataset):
281
Render the header followed by the rows of data.
283
dataset, columns, maxlen = self._analyze_dataset(dataset)
284
self._render_header(dataset, columns, maxlen)
285
self._render_rows(dataset, columns, maxlen)
287
def _render_empty_dataset(self):
289
Render empty dataset.
291
By default it just prints out a fixed sentence:
292
>>> DataSetRenderer()._render_empty_dataset()
293
There is no data to display
295
This can be changed by passing an argument to the constructor
296
>>> DataSetRenderer(empty="there is no data")._render_empty_dataset()
301
def render(self, dataset):
303
self._render_dataset(dataset)
305
self._render_empty_dataset()
308
class XMLRPCCommand(Command):
310
Abstract base class for commands that interact with dashboard server
313
The only difference is that you should implement invoke_remote()
314
instead of invoke(). The provided implementation catches several
315
socket and XML-RPC errors and prints a pretty error message.
319
def _construct_xml_rpc_url(url):
321
Construct URL to the XML-RPC service out of the given URL
323
parts = urlparse.urlsplit(url)
324
if not parts.path.endswith("/RPC2/"):
325
path = parts.path.rstrip("/") + "/xml-rpc/"
328
return urlparse.urlunsplit(
329
(parts.scheme, parts.netloc, path, "", ""))
332
def _strict_server_version(version):
334
Calculate strict server version (as defined by
335
distutils.version.StrictVersion). This works by discarding .candidate
336
and .dev release-levels.
337
>>> XMLRPCCommand._strict_server_version("0.4.0.candidate.5")
339
>>> XMLRPCCommand._strict_server_version("0.4.0.dev.126")
341
>>> XMLRPCCommand._strict_server_version("0.4.0.alpha.1")
343
>>> XMLRPCCommand._strict_server_version("0.4.0.beta.2")
347
major, minor, micro, releaselevel, serial = version.split(".")
350
("version %r does not follow pattern "
351
"'major.minor.micro.releaselevel.serial'") % version)
352
if releaselevel in ["dev", "candidate", "final"]:
353
return "%s.%s.%s" % (major, minor, micro)
354
elif releaselevel == "alpha":
355
return "%s.%s.%sa%s" % (major, minor, micro, serial)
356
elif releaselevel == "beta":
357
return "%s.%s.%sb%s" % (major, minor, micro, serial)
360
("releaselevel %r is not one of 'final', 'alpha', 'beta', "
361
"'candidate' or 'final'") % releaselevel)
363
def _check_server_version(self, server_obj, required_version):
365
Check that server object has is at least required_version.
367
This method may raise InsufficientServerVersion.
369
from distutils.version import StrictVersion, LooseVersion
370
# For backwards compatibility the server reports
371
# major.minor.micro.releaselevel.serial which is not PEP-386 compliant
372
server_version = StrictVersion(
373
self._strict_server_version(server_obj.version()))
374
required_version = StrictVersion(required_version)
375
if server_version < required_version:
376
raise InsufficientServerVersion(server_version, required_version)
378
def __init__(self, parser, args):
379
super(XMLRPCCommand, self).__init__(parser, args)
380
xml_rpc_url = self._construct_xml_rpc_url(self.args.dashboard_url)
381
self.server = AuthenticatingServerProxy(
383
verbose=args.verbose_xml_rpc,
386
auth_backend=KeyringAuthBackend())
388
def use_non_legacy_api_if_possible(self, name='server'):
389
# Legacy APIs are registered in top-level object, non-legacy APIs are
390
# prefixed with extension name.
391
if "dashboard.version" in getattr(self, name).system.listMethods():
392
setattr(self, name, getattr(self, name).dashboard)
395
def register_arguments(cls, parser):
396
dashboard_group = parser.add_argument_group("dashboard specific arguments")
397
default_dashboard_url = os.getenv("DASHBOARD_URL")
398
if default_dashboard_url:
399
dashboard_group.add_argument("--dashboard-url",
400
metavar="URL", help="URL of your validation dashboard (currently %(default)s)",
401
default=default_dashboard_url)
403
dashboard_group.add_argument("--dashboard-url", required=True,
404
metavar="URL", help="URL of your validation dashboard")
405
debug_group = parser.add_argument_group("debugging arguments")
406
debug_group.add_argument("--verbose-xml-rpc",
407
action="store_true", default=False,
408
help="Show XML-RPC data")
409
return dashboard_group
411
@contextlib.contextmanager
412
def safety_net(self):
415
except socket.error as ex:
416
print >> sys.stderr, "Unable to connect to server at %s" % (
417
self.args.dashboard_url,)
418
# It seems that some errors are reported as -errno
419
# while others as +errno.
420
ex.errno = abs(ex.errno)
421
if ex.errno == errno.ECONNREFUSED:
422
print >> sys.stderr, "Connection was refused."
423
parts = urlparse.urlsplit(self.args.dashboard_url)
424
if parts.netloc == "localhost:8000":
425
print >> sys.stderr, "Perhaps the server is not running?"
426
elif ex.errno == errno.ENOENT:
427
print >> sys.stderr, "Unable to resolve address"
429
print >> sys.stderr, "Socket %d: %s" % (ex.errno, ex.strerror)
430
except xmlrpclib.ProtocolError as ex:
431
print >> sys.stderr, "Unable to exchange XML-RPC message with dashboard server"
432
print >> sys.stderr, "HTTP error code: %d/%s" % (ex.errcode, ex.errmsg)
433
except xmlrpclib.Fault as ex:
434
self.handle_xmlrpc_fault(ex.faultCode, ex.faultString)
435
except InsufficientServerVersion as ex:
436
print >> sys.stderr, ("This command requires at least server version "
437
"%s, actual server version is %s" %
438
(ex.required_version, ex.server_version))
441
with self.safety_net():
442
self.use_non_legacy_api_if_possible()
443
return self.invoke_remote()
445
def handle_xmlrpc_fault(self, faultCode, faultString):
447
print >> sys.stderr, "Dashboard server has experienced internal error"
448
print >> sys.stderr, faultString
450
print >> sys.stderr, "XML-RPC error %d: %s" % (faultCode, faultString)
452
def invoke_remote(self):
453
raise NotImplementedError()
456
class server_version(XMLRPCCommand):
458
Display dashboard server version
461
def invoke_remote(self):
462
print "Dashboard server version: %s" % (self.server.version(),)
465
class put(XMLRPCCommand):
467
Upload a bundle on the server
471
def register_arguments(cls, parser):
472
super(put, cls).register_arguments(parser)
473
parser.add_argument("LOCAL",
474
type=argparse.FileType("rb"),
475
help="pathname on the local file system")
476
parser.add_argument("REMOTE",
477
default="/anonymous/", nargs='?',
478
help="pathname on the server")
480
def invoke_remote(self):
481
content = self.args.LOCAL.read()
482
filename = self.args.LOCAL.name
483
pathname = self.args.REMOTE
484
content_sha1 = self.server.put(content, filename, pathname)
485
print "Stored as bundle {0}".format(content_sha1)
487
def handle_xmlrpc_fault(self, faultCode, faultString):
489
print >> sys.stderr, "Bundle stream %s does not exist" % (
491
elif faultCode == 409:
492
print >> sys.stderr, "You have already uploaded this bundle to the dashboard"
494
super(put, self).handle_xmlrpc_fault(faultCode, faultString)
497
class get(XMLRPCCommand):
499
Download a bundle from the server
503
def register_arguments(cls, parser):
504
super(get, cls).register_arguments(parser)
505
parser.add_argument("SHA1",
507
help="SHA1 of the bundle to download")
508
parser.add_argument("--overwrite",
510
help="Overwrite files on the local disk")
511
parser.add_argument("--output", "-o",
512
type=argparse.FileType("wb"),
514
help="Alternate name of the output file")
516
def invoke_remote(self):
517
response = self.server.get(self.args.SHA1)
518
if self.args.output is None:
519
filename = self.args.SHA1
520
if os.path.exists(filename) and not self.args.overwrite:
521
print >> sys.stderr, "File {filename!r} already exists".format(
523
print >> sys.stderr, "You may pass --overwrite to write over it"
525
stream = open(filename, "wb")
527
stream = self.args.output
528
filename = self.args.output.name
529
stream.write(response['content'])
530
print "Downloaded bundle {0} to file {1!r}".format(
531
self.args.SHA1, filename)
533
def handle_xmlrpc_fault(self, faultCode, faultString):
535
print >> sys.stderr, "Bundle {sha1} does not exist".format(
538
super(get, self).handle_xmlrpc_fault(faultCode, faultString)
541
class deserialize(XMLRPCCommand):
543
Deserialize a bundle on the server
547
def register_arguments(cls, parser):
548
super(deserialize, cls).register_arguments(parser)
549
parser.add_argument("SHA1",
551
help="SHA1 of the bundle to deserialize")
553
def invoke_remote(self):
554
response = self.server.deserialize(self.args.SHA1)
555
print "Bundle {sha1} deserialized".format(
558
def handle_xmlrpc_fault(self, faultCode, faultString):
560
print >> sys.stderr, "Bundle {sha1} does not exist".format(
562
elif faultCode == 409:
563
print >> sys.stderr, "Unable to deserialize bundle {sha1}".format(
565
print >> sys.stderr, faultString
567
super(deserialize, self).handle_xmlrpc_fault(faultCode, faultString)
570
def _get_pretty_renderer(**kwargs):
571
if "separator" not in kwargs:
572
kwargs["separator"] = " | "
573
if "header_separator" not in kwargs:
574
kwargs["header_separator"] = True
575
return DataSetRenderer(**kwargs)
578
class streams(XMLRPCCommand):
580
Show streams you have access to
583
renderer = _get_pretty_renderer(
584
order=('pathname', 'bundle_count', 'name'),
586
'pathname': 'Pathname',
587
'bundle_count': 'Number of bundles',
590
'name': lambda name: name or "(not set)"},
591
empty="There are no streams you can access on the server",
592
caption="Bundle streams")
594
def invoke_remote(self):
595
self.renderer.render(self.server.streams())
598
class bundles(XMLRPCCommand):
600
Show bundles in the specified stream
603
renderer = _get_pretty_renderer(
605
'uploaded_by': 'Uploader',
606
'uploaded_on': 'Upload date',
607
'content_filename': 'File name',
608
'content_sha1': 'SHA1',
609
'is_deserialized': "Deserialized?"},
611
'is_deserialized': lambda x: "yes" if x else "no",
612
'uploaded_by': lambda x: x or "(anonymous)",
613
'uploaded_on': lambda x: x.strftime("%Y-%m-%d %H:%M:%S")},
614
order=('content_sha1', 'content_filename', 'uploaded_by',
615
'uploaded_on', 'is_deserialized'),
616
empty="There are no bundles in this stream",
621
def register_arguments(cls, parser):
622
super(bundles, cls).register_arguments(parser)
623
parser.add_argument("PATHNAME",
624
default="/anonymous/", nargs='?',
625
help="pathname on the server (defaults to %(default)s)")
627
def invoke_remote(self):
628
self.renderer.render(self.server.bundles(self.args.PATHNAME))
630
def handle_xmlrpc_fault(self, faultCode, faultString):
632
print >> sys.stderr, "Bundle stream %s does not exist" % (
635
super(bundles, self).handle_xmlrpc_fault(faultCode, faultString)
638
class make_stream(XMLRPCCommand):
640
Create a bundle stream on the server
644
def register_arguments(cls, parser):
645
super(make_stream, cls).register_arguments(parser)
649
help="Pathname of the bundle stream to create")
654
help="Name of the bundle stream (description)")
656
def invoke_remote(self):
657
self._check_server_version(self.server, "0.3")
658
pathname = self.server.make_stream(self.args.pathname, self.args.name)
659
print "Bundle stream {pathname} created".format(pathname=pathname)
662
class backup(XMLRPCCommand):
664
Backup data uploaded to a dashboard instance.
666
Not all data is preserved. The following data is lost: identity of the user
667
that uploaded each bundle, time of uploading and deserialization on the
668
server, name of the bundle stream that contained the data
672
def register_arguments(cls, parser):
673
super(backup, cls).register_arguments(parser)
674
parser.add_argument("BACKUP_DIR", type=str,
675
help="Directory to backup to")
677
def invoke_remote(self):
678
if not os.path.exists(self.args.BACKUP_DIR):
679
os.mkdir(self.args.BACKUP_DIR)
680
for bundle_stream in self.server.streams():
681
print "Processing stream %s" % bundle_stream["pathname"]
682
bundle_stream_dir = os.path.join(self.args.BACKUP_DIR, urllib.quote_plus(bundle_stream["pathname"]))
683
if not os.path.exists(bundle_stream_dir):
684
os.mkdir(bundle_stream_dir)
685
with open(os.path.join(bundle_stream_dir, "metadata.json"), "wt") as stream:
687
"pathname": bundle_stream["pathname"],
688
"name": bundle_stream["name"],
689
"user": bundle_stream["user"],
690
"group": bundle_stream["group"],
692
for bundle in self.server.bundles(bundle_stream["pathname"]):
693
print " * Backing up bundle %s" % bundle["content_sha1"]
694
data = self.server.get(bundle["content_sha1"])
695
bundle_pathname = os.path.join(bundle_stream_dir, bundle["content_sha1"])
696
# Note: we write bundles as binary data to preserve anything the user might have dumped on us
697
with open(bundle_pathname + ".json", "wb") as stream:
698
stream.write(data["content"])
699
with open(bundle_pathname + ".metadata.json", "wt") as stream:
701
"uploaded_by": bundle["uploaded_by"],
702
"uploaded_on": datetime_extension.to_json(bundle["uploaded_on"]),
703
"content_filename": bundle["content_filename"],
704
"content_sha1": bundle["content_sha1"],
705
"content_size": bundle["content_size"],
709
class restore(XMLRPCCommand):
711
Restore a dashboard instance from backup
715
def register_arguments(cls, parser):
716
super(restore, cls).register_arguments(parser)
717
parser.add_argument("BACKUP_DIR", type=str,
718
help="Directory to backup from")
720
def invoke_remote(self):
721
self._check_server_version(self.server, "0.3")
722
for stream_pathname_quoted in os.listdir(self.args.BACKUP_DIR):
723
filesystem_stream_pathname = os.path.join(self.args.BACKUP_DIR, stream_pathname_quoted)
724
if not os.path.isdir(filesystem_stream_pathname):
726
stream_pathname = urllib.unquote(stream_pathname_quoted)
727
if os.path.exists(os.path.join(filesystem_stream_pathname, "metadata.json")):
728
with open(os.path.join(filesystem_stream_pathname, "metadata.json"), "rt") as stream:
729
stream_metadata = simplejson.load(stream)
732
print "Processing stream %s" % stream_pathname
734
self.server.make_stream(stream_pathname, stream_metadata.get("name", "Restored from backup"))
735
except xmlrpclib.Fault as ex:
736
if ex.faultCode != 409:
738
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"]:
739
filesystem_content_filename = os.path.join(filesystem_stream_pathname, content_sha1 + ".json")
740
if not os.path.isfile(filesystem_content_filename):
742
with open(os.path.join(filesystem_stream_pathname, content_sha1) + ".metadata.json", "rt") as stream:
743
bundle_metadata = simplejson.load(stream)
744
with open(filesystem_content_filename, "rb") as stream:
745
content = stream.read()
746
print " * Restoring bundle %s" % content_sha1
748
self.server.put(content, bundle_metadata["content_filename"], stream_pathname)
749
except xmlrpclib.Fault as ex:
750
if ex.faultCode != 409:
754
class pull(ExperimentalCommandMixIn, XMLRPCCommand):
756
Copy bundles and bundle streams from one dashboard to another.
758
This command checks for two environment varialbes:
759
The value of DASHBOARD_URL is used as a replacement for --dashbard-url.
760
The value of REMOTE_DASHBOARD_URL as a replacement for FROM.
761
Their presence automatically makes the corresponding argument optional.
764
def __init__(self, parser, args):
765
super(pull, self).__init__(parser, args)
766
remote_xml_rpc_url = self._construct_xml_rpc_url(self.args.FROM)
767
self.remote_server = AuthenticatingServerProxy(
769
verbose=args.verbose_xml_rpc,
772
auth_backend=KeyringAuthBackend())
773
self.use_non_legacy_api_if_possible('remote_server')
776
def register_arguments(cls, parser):
777
group = super(pull, cls).register_arguments(parser)
778
default_remote_dashboard_url = os.getenv("REMOTE_DASHBOARD_URL")
779
if default_remote_dashboard_url:
782
help="URL of the remote validation dashboard (currently %(default)s)",
783
default=default_remote_dashboard_url)
787
help="URL of the remote validation dashboard)")
788
group.add_argument("STREAM", nargs="*", help="Streams to pull from (all by default)")
791
def _filesizeformat(num_bytes):
793
Formats the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB,
797
num_bytes = float(num_bytes)
798
except (TypeError, ValueError, UnicodeDecodeError):
799
return "%(size)d byte", "%(size)d num_bytes" % {'size': 0}
801
filesize_number_format = lambda value: "%0.2f" % (round(value, 1),)
804
return "%(size)d bytes" % {'size': num_bytes}
805
if num_bytes < 1024 * 1024:
806
return "%s KB" % filesize_number_format(num_bytes / 1024)
807
if num_bytes < 1024 * 1024 * 1024:
808
return "%s MB" % filesize_number_format(num_bytes / (1024 * 1024))
809
return "%s GB" % filesize_number_format(num_bytes / (1024 * 1024 * 1024))
811
def invoke_remote(self):
812
self._check_server_version(self.server, "0.3")
814
print "Checking local and remote streams"
815
remote = self.remote_server.streams()
817
# Check that all requested streams are available remotely
818
requested_set = frozenset(self.args.STREAM)
819
remote_set = frozenset((stream["pathname"] for stream in remote))
820
unavailable_set = requested_set - remote_set
822
print >> sys.stderr, "Remote stream not found: %s" % ", ".join(unavailable_set)
824
# Limit to requested streams if necessary
825
remote = [stream for stream in remote if stream["pathname"] in requested_set]
826
local = self.server.streams()
827
missing_pathnames = set([stream["pathname"] for stream in remote]) - set([stream["pathname"] for stream in local])
828
for stream in remote:
829
if stream["pathname"] in missing_pathnames:
830
self.server.make_stream(stream["pathname"], stream["name"])
833
local_bundles = [bundle for bundle in self.server.bundles(stream["pathname"])]
834
remote_bundles = [bundle for bundle in self.remote_server.bundles(stream["pathname"])]
835
missing_bundles = set((bundle["content_sha1"] for bundle in remote_bundles))
836
missing_bundles -= set((bundle["content_sha1"] for bundle in local_bundles))
839
(bundle["content_size"]
840
for bundle in remote_bundles
841
if bundle["content_sha1"] in missing_bundles))
842
except KeyError as ex:
843
# Older servers did not return content_size so this part is optional
846
print "Stream %s needs update (%s)" % (stream["pathname"], self._filesizeformat(missing_bytes))
847
elif missing_bundles:
848
print "Stream %s needs update (no estimate available)" % (stream["pathname"],)
850
print "Stream %s is up to date" % (stream["pathname"],)
851
for content_sha1 in missing_bundles:
852
print "Getting %s" % (content_sha1,),
854
data = self.remote_server.get(content_sha1)
855
print "got %s, storing" % (self._filesizeformat(len(data["content"]))),
858
self.server.put(data["content"], data["content_filename"], stream["pathname"])
859
except xmlrpclib.Fault as ex:
860
if ex.faultCode == 409: # duplicate
861
print "already present (in another stream)"
868
class data_views(ExperimentalCommandMixIn, XMLRPCCommand):
870
Show data views defined on the server
872
renderer = _get_pretty_renderer(
875
'summary': 'Summary',
877
order=('name', 'summary'),
878
empty="There are no data views defined yet",
879
caption="Data Views")
881
def invoke_remote(self):
882
self._check_server_version(self.server, "0.4")
883
self.renderer.render(self.server.data_views())
885
print "Tip: to invoke a data view try `lc-tool query-data-view`"
888
class query_data_view(ExperimentalCommandMixIn, XMLRPCCommand):
890
Invoke a specified data view
893
def register_arguments(cls, parser):
894
super(query_data_view, cls).register_arguments(parser)
895
parser.add_argument("QUERY", metavar="QUERY", nargs="...",
896
help="Data view name and any optional and required arguments")
898
def _probe_data_views(self):
900
Probe the server for information about data views
902
with self.safety_net():
903
self.use_non_legacy_api_if_possible()
904
self._check_server_version(self.server, "0.4")
905
return self.server.data_views()
907
def reparse_arguments(self, parser, raw_args):
908
self.data_views = self._probe_data_views()
909
if self.data_views is None:
911
# Here we hack a little, the last actuin is the QUERY action added
912
# in register_arguments above. By removing it we make the output
913
# of lc-tool query-data-view NAME --help more consistent.
914
del parser._actions[-1]
915
subparsers = parser.add_subparsers(
916
title="Data views available on the server")
917
for data_view in self.data_views:
918
data_view_parser = subparsers.add_parser(
920
help=data_view["summary"],
921
epilog=data_view["documentation"])
922
data_view_parser.set_defaults(data_view=data_view)
923
group = data_view_parser.add_argument_group("Data view parameters")
924
for argument in data_view["arguments"]:
925
if argument["default"] is None:
927
"--{name}".format(name=argument["name"].replace("_", "-")),
928
dest=argument["name"],
929
help=argument["help"],
934
"--{name}".format(name=argument["name"].replace("_", "-")),
935
dest=argument["name"],
936
help=argument["help"],
938
default=argument["default"])
939
self.args = self.parser.parse_args(raw_args)
942
# Override and _not_ call 'use_non_legacy_api_if_possible' as we
943
# already did this reparse_arguments
944
with self.safety_net():
945
return self.invoke_remote()
947
def invoke_remote(self):
948
if self.data_views is None:
950
self._check_server_version(self.server, "0.4")
951
# Build a collection of arguments for data view
953
for argument in self.args.data_view["arguments"]:
954
arg_name = argument["name"]
955
if arg_name in self.args:
956
data_view_args[arg_name] = getattr(self.args, arg_name)
957
# Invoke the data view
958
response = self.server.query_data_view(self.args.data_view["name"], data_view_args)
959
# Create a pretty-printer
960
renderer = _get_pretty_renderer(
961
caption=self.args.data_view["summary"],
962
order=[item["name"] for item in response["columns"]])
963
# Post-process the data so that it fits the printer
964
data_for_renderer = [
966
[column["name"] for column in response["columns"]],
968
for row in response["rows"]]
970
renderer.render(data_for_renderer)
973
class version(Command):
975
Show dashboard client version
979
from lava_dashboard_tool import __version__
980
print "Dashboard client version: {version}".format(
981
version=versiontools.format_version(__version__))