blob: 13d4c07f5e79f67443fa915819d75f2a91a29c8c [file] [log] [blame]
mblighbe630eb2008-08-01 16:41:48 +00001#
2# Copyright 2008 Google Inc. All Rights Reserved.
3#
4"""
5This module contains the generic CLI object
6
7High Level Design:
8
9The atest class contains attributes & method generic to all the CLI
10operations.
11
12The class inheritance is shown here using the command
13'atest host create ...' as an example:
14
15atest <-- host <-- host_create <-- site_host_create
16
17Note: The site_<topic>.py and its classes are only needed if you need
18to override the common <topic>.py methods with your site specific ones.
19
20
21High Level Algorithm:
22
231. atest figures out the topic and action from the 2 first arguments
24 on the command line and imports the <topic> (or site_<topic>)
25 module.
26
271. Init
28 The main atest module creates a <topic>_<action> object. The
29 __init__() function is used to setup the parser options, if this
30 <action> has some specific options to add to its <topic>.
31
32 If it exists, the child __init__() method must call its parent
33 class __init__() before adding its own parser arguments.
34
352. Parsing
36 If the child wants to validate the parsing (e.g. make sure that
37 there are hosts in the arguments), or if it wants to check the
38 options it added in its __init__(), it should implement a parse()
39 method.
40
41 The child parser must call its parent parser and gets back the
42 options dictionary and the rest of the command line arguments
43 (leftover). Each level gets to see all the options, but the
44 leftovers can be deleted as they can be consumed by only one
45 object.
46
473. Execution
48 This execute() method is specific to the child and should use the
49 self.execute_rpc() to send commands to the Autotest Front-End. It
50 should return results.
51
524. Output
53 The child output() method is called with the execute() resutls as a
54 parameter. This is child-specific, but should leverage the
55 atest.print_*() methods.
56"""
57
Dan Shi3963caa2014-11-26 12:51:25 -080058import optparse
59import os
60import re
61import sys
62import textwrap
63import traceback
64import urllib2
65
mblighbe630eb2008-08-01 16:41:48 +000066from autotest_lib.cli import rpc
mblighcd26d042010-05-03 18:58:24 +000067from autotest_lib.client.common_lib.test_utils import mock
mblighbe630eb2008-08-01 16:41:48 +000068
69
70# Maps the AFE keys to printable names.
71KEYS_TO_NAMES_EN = {'hostname': 'Host',
72 'platform': 'Platform',
73 'status': 'Status',
74 'locked': 'Locked',
75 'locked_by': 'Locked by',
mblighe163b032008-10-18 14:30:27 +000076 'lock_time': 'Locked time',
Matthew Sartori68186332015-04-27 17:19:53 -070077 'lock_reason': 'Lock Reason',
mblighbe630eb2008-08-01 16:41:48 +000078 'labels': 'Labels',
79 'description': 'Description',
80 'hosts': 'Hosts',
81 'users': 'Users',
82 'id': 'Id',
83 'name': 'Name',
84 'invalid': 'Valid',
85 'login': 'Login',
86 'access_level': 'Access Level',
87 'job_id': 'Job Id',
88 'job_owner': 'Job Owner',
89 'job_name': 'Job Name',
90 'test_type': 'Test Type',
91 'test_class': 'Test Class',
92 'path': 'Path',
93 'owner': 'Owner',
94 'status_counts': 'Status Counts',
95 'hosts_status': 'Host Status',
mblighfca5ed12009-11-06 02:59:56 +000096 'hosts_selected_status': 'Hosts filtered by Status',
mblighbe630eb2008-08-01 16:41:48 +000097 'priority': 'Priority',
98 'control_type': 'Control Type',
99 'created_on': 'Created On',
100 'synch_type': 'Synch Type',
101 'control_file': 'Control File',
showard989f25d2008-10-01 11:38:11 +0000102 'only_if_needed': 'Use only if needed',
mblighe163b032008-10-18 14:30:27 +0000103 'protection': 'Protection',
showard21baa452008-10-21 00:08:39 +0000104 'run_verify': 'Run verify',
105 'reboot_before': 'Pre-job reboot',
106 'reboot_after': 'Post-job reboot',
mbligh140a23c2008-10-29 16:55:21 +0000107 'experimental': 'Experimental',
mbligh8fadff32009-03-09 21:19:59 +0000108 'synch_count': 'Sync Count',
showardfb64e6a2009-04-22 21:01:18 +0000109 'max_number_of_machines': 'Max. hosts to use',
showarda1e74b32009-05-12 17:32:04 +0000110 'parse_failed_repair': 'Include failed repair results',
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800111 'shard': 'Shard',
mblighbe630eb2008-08-01 16:41:48 +0000112 }
113
114# In the failure, tag that will replace the item.
115FAIL_TAG = '<XYZ>'
116
mbligh8c7b04c2009-03-25 18:01:56 +0000117# Global socket timeout: uploading kernels can take much,
118# much longer than the default
mblighbe630eb2008-08-01 16:41:48 +0000119UPLOAD_SOCKET_TIMEOUT = 60*30
120
121
122# Convertion functions to be called for printing,
123# e.g. to print True/False for booleans.
124def __convert_platform(field):
mbligh0887d402009-01-30 00:50:29 +0000125 if field is None:
mblighbe630eb2008-08-01 16:41:48 +0000126 return ""
mbligh0887d402009-01-30 00:50:29 +0000127 elif isinstance(field, int):
mblighbe630eb2008-08-01 16:41:48 +0000128 # Can be 0/1 for False/True
129 return str(bool(field))
130 else:
131 # Can be a platform name
132 return field
133
134
showard989f25d2008-10-01 11:38:11 +0000135def _int_2_bool_string(value):
136 return str(bool(value))
137
138KEYS_CONVERT = {'locked': _int_2_bool_string,
mblighbe630eb2008-08-01 16:41:48 +0000139 'invalid': lambda flag: str(bool(not flag)),
showard989f25d2008-10-01 11:38:11 +0000140 'only_if_needed': _int_2_bool_string,
mblighbe630eb2008-08-01 16:41:48 +0000141 'platform': __convert_platform,
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800142 'labels': lambda labels: ', '.join(labels),
143 'shards': lambda shard: shard.hostname if shard else ''}
mblighbe630eb2008-08-01 16:41:48 +0000144
showard088b8262009-07-01 22:12:35 +0000145
146def _get_item_key(item, key):
147 """Allow for lookups in nested dictionaries using '.'s within a key."""
148 if key in item:
149 return item[key]
150 nested_item = item
151 for subkey in key.split('.'):
152 if not subkey:
153 raise ValueError('empty subkey in %r' % key)
154 try:
155 nested_item = nested_item[subkey]
156 except KeyError, e:
157 raise KeyError('%r - looking up key %r in %r' %
158 (e, key, nested_item))
159 else:
160 return nested_item
161
162
mblighbe630eb2008-08-01 16:41:48 +0000163class CliError(Exception):
Dan Shi3963caa2014-11-26 12:51:25 -0800164 """Error raised by cli calls.
165 """
mblighbe630eb2008-08-01 16:41:48 +0000166 pass
167
168
mbligh9deeefa2009-05-01 23:11:08 +0000169class item_parse_info(object):
Dan Shi3963caa2014-11-26 12:51:25 -0800170 """Object keeping track of the parsing options.
171 """
172
mbligh9deeefa2009-05-01 23:11:08 +0000173 def __init__(self, attribute_name, inline_option='',
174 filename_option='', use_leftover=False):
175 """Object keeping track of the parsing options that will
176 make up the content of the atest attribute:
Jakob Juelich8b110ee2014-09-15 16:13:42 -0700177 attribute_name: the atest attribute name to populate (label)
mbligh9deeefa2009-05-01 23:11:08 +0000178 inline_option: the option containing the items (--label)
179 filename_option: the option containing the filename (--blist)
180 use_leftover: whether to add the leftover arguments or not."""
181 self.attribute_name = attribute_name
182 self.filename_option = filename_option
183 self.inline_option = inline_option
184 self.use_leftover = use_leftover
185
186
187 def get_values(self, options, leftover=[]):
188 """Returns the value for that attribute by accumualting all
189 the values found through the inline option, the parsing of the
190 file and the leftover"""
jamesrenc2863162010-07-12 21:20:51 +0000191
192 def __get_items(input, split_spaces=True):
193 """Splits a string of comma separated items. Escaped commas will not
194 be split. I.e. Splitting 'a, b\,c, d' will yield ['a', 'b,c', 'd'].
195 If split_spaces is set to False spaces will not be split. I.e.
196 Splitting 'a b, c\,d, e' will yield ['a b', 'c,d', 'e']"""
197
198 # Replace escaped slashes with null characters so we don't misparse
199 # proceeding commas.
200 input = input.replace(r'\\', '\0')
201
202 # Split on commas which are not preceded by a slash.
203 if not split_spaces:
204 split = re.split(r'(?<!\\),', input)
205 else:
206 split = re.split(r'(?<!\\),|\s', input)
207
208 # Convert null characters to single slashes and escaped commas to
209 # just plain commas.
210 return (item.strip().replace('\0', '\\').replace(r'\,', ',') for
211 item in split if item.strip())
mbligh9deeefa2009-05-01 23:11:08 +0000212
213 if self.use_leftover:
214 add_on = leftover
215 leftover = []
216 else:
217 add_on = []
218
219 # Start with the add_on
220 result = set()
221 for items in add_on:
222 # Don't split on space here because the add-on
223 # may have some spaces (like the job name)
jamesrenc2863162010-07-12 21:20:51 +0000224 result.update(__get_items(items, split_spaces=False))
mbligh9deeefa2009-05-01 23:11:08 +0000225
226 # Process the inline_option, if any
227 try:
228 items = getattr(options, self.inline_option)
229 result.update(__get_items(items))
230 except (AttributeError, TypeError):
231 pass
232
233 # Process the file list, if any and not empty
234 # The file can contain space and/or comma separated items
235 try:
236 flist = getattr(options, self.filename_option)
237 file_content = []
238 for line in open(flist).readlines():
239 file_content += __get_items(line)
240 if len(file_content) == 0:
241 raise CliError("Empty file %s" % flist)
242 result.update(file_content)
243 except (AttributeError, TypeError):
244 pass
245 except IOError:
246 raise CliError("Could not open file %s" % flist)
247
248 return list(result), leftover
249
250
mblighbe630eb2008-08-01 16:41:48 +0000251class atest(object):
252 """Common class for generic processing
253 Should only be instantiated by itself for usage
254 references, otherwise, the <topic> objects should
255 be used."""
Allen Li335f2162017-02-01 14:47:01 -0800256 msg_topic = ('[acl|host|job|label|shard|test|user|server|'
Dan Shi25e1fd42014-12-19 14:36:42 -0800257 'stable_version]')
258 usage_action = '[action]'
mblighbe630eb2008-08-01 16:41:48 +0000259 msg_items = ''
260
261 def invalid_arg(self, header, follow_up=''):
Dan Shi3963caa2014-11-26 12:51:25 -0800262 """Fail the command with error that command line has invalid argument.
263
264 @param header: Header of the error message.
265 @param follow_up: Extra error message, default to empty string.
266 """
mblighbe630eb2008-08-01 16:41:48 +0000267 twrap = textwrap.TextWrapper(initial_indent=' ',
268 subsequent_indent=' ')
269 rest = twrap.fill(follow_up)
270
271 if self.kill_on_failure:
272 self.invalid_syntax(header + rest)
273 else:
274 print >> sys.stderr, header + rest
275
276
277 def invalid_syntax(self, msg):
Dan Shi3963caa2014-11-26 12:51:25 -0800278 """Fail the command with error that the command line syntax is wrong.
279
280 @param msg: Error message.
281 """
mblighbe630eb2008-08-01 16:41:48 +0000282 print
283 print >> sys.stderr, msg
284 print
285 print "usage:",
286 print self._get_usage()
287 print
288 sys.exit(1)
289
290
291 def generic_error(self, msg):
Dan Shi3963caa2014-11-26 12:51:25 -0800292 """Fail the command with a generic error.
293
294 @param msg: Error message.
295 """
showardfb64e6a2009-04-22 21:01:18 +0000296 if self.debug:
297 traceback.print_exc()
mblighbe630eb2008-08-01 16:41:48 +0000298 print >> sys.stderr, msg
299 sys.exit(1)
300
301
mbligh7a3ebe32008-12-01 17:10:33 +0000302 def parse_json_exception(self, full_error):
303 """Parses the JSON exception to extract the bad
304 items and returns them
305 This is very kludgy for the moment, but we would need
306 to refactor the exceptions sent from the front end
Dan Shi3963caa2014-11-26 12:51:25 -0800307 to make this better.
308
309 @param full_error: The complete error message.
310 """
mbligh7a3ebe32008-12-01 17:10:33 +0000311 errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
312 parts = errmsg.split(':')
313 # Kludge: If there are 2 colons the last parts contains
314 # the items that failed.
315 if len(parts) != 3:
316 return []
317 return [item.strip() for item in parts[2].split(',') if item.strip()]
318
319
mblighb68405d2010-03-11 18:32:39 +0000320 def failure(self, full_error, item=None, what_failed='', fatal=False):
mblighbe630eb2008-08-01 16:41:48 +0000321 """If kill_on_failure, print this error and die,
322 otherwise, queue the error and accumulate all the items
Dan Shi3963caa2014-11-26 12:51:25 -0800323 that triggered the same error.
324
325 @param full_error: The complete error message.
326 @param item: Name of the actionable item, e.g., hostname.
327 @param what_failed: Name of the failed item.
328 @param fatal: True to exit the program with failure.
329 """
mblighbe630eb2008-08-01 16:41:48 +0000330
331 if self.debug:
332 errmsg = str(full_error)
333 else:
334 errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
335
mblighb68405d2010-03-11 18:32:39 +0000336 if self.kill_on_failure or fatal:
mblighbe630eb2008-08-01 16:41:48 +0000337 print >> sys.stderr, "%s\n %s" % (what_failed, errmsg)
338 sys.exit(1)
339
340 # Build a dictionary with the 'what_failed' as keys. The
341 # values are dictionaries with the errmsg as keys and a set
342 # of items as values.
mbligh1ef218d2009-08-03 16:57:56 +0000343 # self.failed =
mblighbe630eb2008-08-01 16:41:48 +0000344 # {'Operation delete_host_failed': {'AclAccessViolation:
345 # set('host0', 'host1')}}
346 # Try to gather all the same error messages together,
347 # even if they contain the 'item'
348 if item and item in errmsg:
349 errmsg = errmsg.replace(item, FAIL_TAG)
350 if self.failed.has_key(what_failed):
351 self.failed[what_failed].setdefault(errmsg, set()).add(item)
352 else:
353 self.failed[what_failed] = {errmsg: set([item])}
354
355
356 def show_all_failures(self):
Dan Shi3963caa2014-11-26 12:51:25 -0800357 """Print all failure information.
358 """
mblighbe630eb2008-08-01 16:41:48 +0000359 if not self.failed:
360 return 0
361 for what_failed in self.failed.keys():
362 print >> sys.stderr, what_failed + ':'
363 for (errmsg, items) in self.failed[what_failed].iteritems():
364 if len(items) == 0:
365 print >> sys.stderr, errmsg
366 elif items == set(['']):
367 print >> sys.stderr, ' ' + errmsg
368 elif len(items) == 1:
369 # Restore the only item
370 if FAIL_TAG in errmsg:
371 errmsg = errmsg.replace(FAIL_TAG, items.pop())
372 else:
373 errmsg = '%s (%s)' % (errmsg, items.pop())
374 print >> sys.stderr, ' ' + errmsg
375 else:
376 print >> sys.stderr, ' ' + errmsg + ' with <XYZ> in:'
377 twrap = textwrap.TextWrapper(initial_indent=' ',
378 subsequent_indent=' ')
379 items = list(items)
380 items.sort()
381 print >> sys.stderr, twrap.fill(', '.join(items))
382 return 1
383
384
385 def __init__(self):
386 """Setup the parser common options"""
387 # Initialized for unit tests.
388 self.afe = None
389 self.failed = {}
390 self.data = {}
391 self.debug = False
mbligh47dc4d22009-02-12 21:48:34 +0000392 self.parse_delim = '|'
mblighbe630eb2008-08-01 16:41:48 +0000393 self.kill_on_failure = False
394 self.web_server = ''
395 self.verbose = False
Dan Shi25e1fd42014-12-19 14:36:42 -0800396 self.no_confirmation = False
mbligh9deeefa2009-05-01 23:11:08 +0000397 self.topic_parse_info = item_parse_info(attribute_name='not_used')
mblighbe630eb2008-08-01 16:41:48 +0000398
399 self.parser = optparse.OptionParser(self._get_usage())
400 self.parser.add_option('-g', '--debug',
401 help='Print debugging information',
402 action='store_true', default=False)
403 self.parser.add_option('--kill-on-failure',
404 help='Stop at the first failure',
405 action='store_true', default=False)
406 self.parser.add_option('--parse',
mbligh47dc4d22009-02-12 21:48:34 +0000407 help='Print the output using | '
mblighbe630eb2008-08-01 16:41:48 +0000408 'separated key=value fields',
409 action='store_true', default=False)
mbligh47dc4d22009-02-12 21:48:34 +0000410 self.parser.add_option('--parse-delim',
411 help='Delimiter to use to separate the '
412 'key=value fields', default='|')
Dan Shi25e1fd42014-12-19 14:36:42 -0800413 self.parser.add_option('--no-confirmation',
414 help=('Skip all confirmation in when function '
415 'require_confirmation is called.'),
416 action='store_true', default=False)
mblighbe630eb2008-08-01 16:41:48 +0000417 self.parser.add_option('-v', '--verbose',
418 action='store_true', default=False)
419 self.parser.add_option('-w', '--web',
420 help='Specify the autotest server '
421 'to talk to',
422 action='store', type='string',
423 dest='web_server', default=None)
424
mblighbe630eb2008-08-01 16:41:48 +0000425
mblighbe630eb2008-08-01 16:41:48 +0000426 def _get_usage(self):
427 return "atest %s %s [options] %s" % (self.msg_topic.lower(),
428 self.usage_action,
429 self.msg_items)
430
431
mbligh5a496082009-08-03 16:44:54 +0000432 def backward_compatibility(self, action, argv):
Dan Shi3963caa2014-11-26 12:51:25 -0800433 """To be overidden by subclass if their syntax changed.
434
435 @param action: Name of the action.
436 @param argv: A list of arguments.
437 """
mbligh5a496082009-08-03 16:44:54 +0000438 return action
439
440
mbligh9deeefa2009-05-01 23:11:08 +0000441 def parse(self, parse_info=[], req_items=None):
Dan Shi3963caa2014-11-26 12:51:25 -0800442 """Parse command arguments.
mblighbe630eb2008-08-01 16:41:48 +0000443
Dan Shi3963caa2014-11-26 12:51:25 -0800444 parse_info is a list of item_parse_info objects.
mbligh9deeefa2009-05-01 23:11:08 +0000445 There should only be one use_leftover set to True in the list.
mblighbe630eb2008-08-01 16:41:48 +0000446
Dan Shi3963caa2014-11-26 12:51:25 -0800447 Also check that the req_items is not empty after parsing.
448
449 @param parse_info: A list of item_parse_info objects.
450 @param req_items: A list of required items.
451 """
mbligh9deeefa2009-05-01 23:11:08 +0000452 (options, leftover) = self.parse_global()
mblighbe630eb2008-08-01 16:41:48 +0000453
mbligh9deeefa2009-05-01 23:11:08 +0000454 all_parse_info = parse_info[:]
455 all_parse_info.append(self.topic_parse_info)
456
457 try:
458 for item_parse_info in all_parse_info:
459 values, leftover = item_parse_info.get_values(options,
460 leftover)
461 setattr(self, item_parse_info.attribute_name, values)
462 except CliError, s:
463 self.invalid_syntax(s)
mblighbe630eb2008-08-01 16:41:48 +0000464
465 if (req_items and not getattr(self, req_items, None)):
466 self.invalid_syntax('%s %s requires at least one %s' %
467 (self.msg_topic,
468 self.usage_action,
469 self.msg_topic))
470
471 return (options, leftover)
472
473
mbligh9deeefa2009-05-01 23:11:08 +0000474 def parse_global(self):
475 """Parse the global arguments.
mblighbe630eb2008-08-01 16:41:48 +0000476
477 It consumes what the common object needs to know, and
478 let the children look at all the options. We could
479 remove the options that we have used, but there is no
480 harm in leaving them, and the children may need them
481 in the future.
482
483 Must be called from its children parse()"""
484 (options, leftover) = self.parser.parse_args()
485 # Handle our own options setup in __init__()
486 self.debug = options.debug
487 self.kill_on_failure = options.kill_on_failure
488
489 if options.parse:
490 suffix = '_parse'
491 else:
492 suffix = '_std'
493 for func in ['print_fields', 'print_table',
mblighdf75f8b2008-11-18 19:07:42 +0000494 'print_by_ids', 'print_list']:
mblighbe630eb2008-08-01 16:41:48 +0000495 setattr(self, func, getattr(self, func + suffix))
496
mbligh47dc4d22009-02-12 21:48:34 +0000497 self.parse_delim = options.parse_delim
498
mblighbe630eb2008-08-01 16:41:48 +0000499 self.verbose = options.verbose
Dan Shi25e1fd42014-12-19 14:36:42 -0800500 self.no_confirmation = options.no_confirmation
mblighbe630eb2008-08-01 16:41:48 +0000501 self.web_server = options.web_server
mblighb68405d2010-03-11 18:32:39 +0000502 try:
503 self.afe = rpc.afe_comm(self.web_server)
504 except rpc.AuthError, s:
505 self.failure(str(s), fatal=True)
mblighbe630eb2008-08-01 16:41:48 +0000506
507 return (options, leftover)
508
509
510 def check_and_create_items(self, op_get, op_create,
511 items, **data_create):
Dan Shi3963caa2014-11-26 12:51:25 -0800512 """Create the items if they don't exist already.
513
514 @param op_get: Name of `get` RPC.
515 @param op_create: Name of `create` RPC.
516 @param items: Actionable items specified in CLI command, e.g., hostname,
517 to be passed to each RPC.
518 @param data_create: Data to be passed to `create` RPC.
519 """
mblighbe630eb2008-08-01 16:41:48 +0000520 for item in items:
521 ret = self.execute_rpc(op_get, name=item)
522
523 if len(ret) == 0:
524 try:
525 data_create['name'] = item
526 self.execute_rpc(op_create, **data_create)
527 except CliError:
528 continue
529
530
531 def execute_rpc(self, op, item='', **data):
Dan Shi3963caa2014-11-26 12:51:25 -0800532 """Execute RPC.
533
534 @param op: Name of the RPC.
535 @param item: Actionable item specified in CLI command.
536 @param data: Data to be passed to RPC.
537 """
mblighbe630eb2008-08-01 16:41:48 +0000538 retry = 2
539 while retry:
540 try:
541 return self.afe.run(op, **data)
542 except urllib2.URLError, err:
mbligh11efd232008-11-27 00:20:46 +0000543 if hasattr(err, 'reason'):
544 if 'timed out' not in err.reason:
545 self.invalid_syntax('Invalid server name %s: %s' %
546 (self.afe.web_server, err))
547 if hasattr(err, 'code'):
showard53d91e22010-01-15 00:18:27 +0000548 error_parts = [str(err)]
549 if self.debug:
550 error_parts.append(err.read()) # read the response body
551 self.failure('\n\n'.join(error_parts), item=item,
mbligh11efd232008-11-27 00:20:46 +0000552 what_failed=("Error received from web server"))
553 raise CliError("Error from web server")
mblighbe630eb2008-08-01 16:41:48 +0000554 if self.debug:
555 print 'retrying: %r %d' % (data, retry)
556 retry -= 1
557 if retry == 0:
558 if item:
559 myerr = '%s timed out for %s' % (op, item)
560 else:
561 myerr = '%s timed out' % op
562 self.failure(myerr, item=item,
563 what_failed=("Timed-out contacting "
564 "the Autotest server"))
565 raise CliError("Timed-out contacting the Autotest server")
mblighcd26d042010-05-03 18:58:24 +0000566 except mock.CheckPlaybackError:
567 raise
mblighbe630eb2008-08-01 16:41:48 +0000568 except Exception, full_error:
569 # There are various exceptions throwns by JSON,
570 # urllib & httplib, so catch them all.
571 self.failure(full_error, item=item,
572 what_failed='Operation %s failed' % op)
573 raise CliError(str(full_error))
574
575
576 # There is no output() method in the atest object (yet?)
577 # but here are some helper functions to be used by its
578 # children
579 def print_wrapped(self, msg, values):
Dan Shi3963caa2014-11-26 12:51:25 -0800580 """Print given message and values in wrapped lines unless
581 AUTOTEST_CLI_NO_WRAP is specified in environment variables.
582
583 @param msg: Message to print.
584 @param values: A list of values to print.
585 """
mblighbe630eb2008-08-01 16:41:48 +0000586 if len(values) == 0:
587 return
588 elif len(values) == 1:
589 print msg + ': '
590 elif len(values) > 1:
591 if msg.endswith('s'):
592 print msg + ': '
593 else:
594 print msg + 's: '
595
596 values.sort()
mbligh552d2402009-09-18 19:35:23 +0000597
598 if 'AUTOTEST_CLI_NO_WRAP' in os.environ:
599 print '\n'.join(values)
600 return
601
mblighbe630eb2008-08-01 16:41:48 +0000602 twrap = textwrap.TextWrapper(initial_indent='\t',
603 subsequent_indent='\t')
604 print twrap.fill(', '.join(values))
605
606
607 def __conv_value(self, type, value):
608 return KEYS_CONVERT.get(type, str)(value)
609
610
611 def print_fields_std(self, items, keys, title=None):
Dan Shi3963caa2014-11-26 12:51:25 -0800612 """Print the keys in each item, one on each line.
613
614 @param items: Items to print.
615 @param keys: Name of the keys to look up each item in items.
616 @param title: Title of the output, default to None.
617 """
mblighbe630eb2008-08-01 16:41:48 +0000618 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000619 return
620 if title:
621 print title
622 for item in items:
623 for key in keys:
624 print '%s: %s' % (KEYS_TO_NAMES_EN[key],
625 self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000626 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000627
628
629 def print_fields_parse(self, items, keys, title=None):
Dan Shi3963caa2014-11-26 12:51:25 -0800630 """Print the keys in each item as comma separated name=value
631
632 @param items: Items to print.
633 @param keys: Name of the keys to look up each item in items.
634 @param title: Title of the output, default to None.
635 """
mblighbe630eb2008-08-01 16:41:48 +0000636 for item in items:
637 values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
638 self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000639 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000640 for key in keys
641 if self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000642 _get_item_key(item, key)) != '']
mbligh47dc4d22009-02-12 21:48:34 +0000643 print self.parse_delim.join(values)
mblighbe630eb2008-08-01 16:41:48 +0000644
645
646 def __find_justified_fmt(self, items, keys):
Dan Shi3963caa2014-11-26 12:51:25 -0800647 """Find the max length for each field.
648
649 @param items: Items to lookup for.
650 @param keys: Name of the keys to look up each item in items.
651 """
mblighbe630eb2008-08-01 16:41:48 +0000652 lens = {}
653 # Don't justify the last field, otherwise we have blank
654 # lines when the max is overlaps but the current values
655 # are smaller
656 if not items:
657 print "No results"
658 return
659 for key in keys[:-1]:
660 lens[key] = max(len(self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000661 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000662 for item in items)
663 lens[key] = max(lens[key], len(KEYS_TO_NAMES_EN[key]))
664 lens[keys[-1]] = 0
665
666 return ' '.join(["%%-%ds" % lens[key] for key in keys])
667
668
Simran Basi0739d682015-02-25 16:22:56 -0800669 def print_dict(self, items, title=None, line_before=False):
670 """Print a dictionary.
671
672 @param items: Dictionary to print.
673 @param title: Title of the output, default to None.
674 @param line_before: True to print an empty line before the output,
675 default to False.
676 """
677 if not items:
678 return
679 if line_before:
680 print
681 print title
682 for key, value in items.items():
683 print '%s : %s' % (key, value)
684
685
mbligh838c7472009-05-13 20:56:50 +0000686 def print_table_std(self, items, keys_header, sublist_keys=()):
Dan Shi3963caa2014-11-26 12:51:25 -0800687 """Print a mix of header and lists in a user readable format.
688
689 The headers are justified, the sublist_keys are wrapped.
690
691 @param items: Items to print.
692 @param keys_header: Header of the keys, use to look up in items.
693 @param sublist_keys: Keys for sublist in each item.
694 """
mblighbe630eb2008-08-01 16:41:48 +0000695 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000696 return
697 fmt = self.__find_justified_fmt(items, keys_header)
698 header = tuple(KEYS_TO_NAMES_EN[key] for key in keys_header)
699 print fmt % header
700 for item in items:
showard088b8262009-07-01 22:12:35 +0000701 values = tuple(self.__conv_value(key,
702 _get_item_key(item, key))
mblighbe630eb2008-08-01 16:41:48 +0000703 for key in keys_header)
704 print fmt % values
mbligh838c7472009-05-13 20:56:50 +0000705 if sublist_keys:
mblighbe630eb2008-08-01 16:41:48 +0000706 for key in sublist_keys:
707 self.print_wrapped(KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000708 _get_item_key(item, key))
mblighbe630eb2008-08-01 16:41:48 +0000709 print '\n'
710
711
mbligh838c7472009-05-13 20:56:50 +0000712 def print_table_parse(self, items, keys_header, sublist_keys=()):
Dan Shi3963caa2014-11-26 12:51:25 -0800713 """Print a mix of header and lists in a user readable format.
714
715 @param items: Items to print.
716 @param keys_header: Header of the keys, use to look up in items.
717 @param sublist_keys: Keys for sublist in each item.
718 """
mblighbe630eb2008-08-01 16:41:48 +0000719 for item in items:
720 values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000721 self.__conv_value(key, _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000722 for key in keys_header
723 if self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000724 _get_item_key(item, key)) != '']
mblighbe630eb2008-08-01 16:41:48 +0000725
mbligh838c7472009-05-13 20:56:50 +0000726 if sublist_keys:
mblighbe630eb2008-08-01 16:41:48 +0000727 [values.append('%s=%s'% (KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000728 ','.join(_get_item_key(item, key))))
mblighbe630eb2008-08-01 16:41:48 +0000729 for key in sublist_keys
showard088b8262009-07-01 22:12:35 +0000730 if len(_get_item_key(item, key))]
mblighbe630eb2008-08-01 16:41:48 +0000731
mbligh47dc4d22009-02-12 21:48:34 +0000732 print self.parse_delim.join(values)
mblighbe630eb2008-08-01 16:41:48 +0000733
734
735 def print_by_ids_std(self, items, title=None, line_before=False):
Dan Shi3963caa2014-11-26 12:51:25 -0800736 """Prints ID & names of items in a user readable form.
737
738 @param items: Items to print.
739 @param title: Title of the output, default to None.
740 @param line_before: True to print an empty line before the output,
741 default to False.
742 """
mblighbe630eb2008-08-01 16:41:48 +0000743 if not items:
744 return
745 if line_before:
746 print
747 if title:
748 print title + ':'
749 self.print_table_std(items, keys_header=['id', 'name'])
750
751
752 def print_by_ids_parse(self, items, title=None, line_before=False):
Dan Shi3963caa2014-11-26 12:51:25 -0800753 """Prints ID & names of items in a parseable format.
754
755 @param items: Items to print.
756 @param title: Title of the output, default to None.
757 @param line_before: True to print an empty line before the output,
758 default to False.
759 """
mblighbe630eb2008-08-01 16:41:48 +0000760 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000761 return
Dan Shi3963caa2014-11-26 12:51:25 -0800762 if line_before:
763 print
mblighbe630eb2008-08-01 16:41:48 +0000764 if title:
765 print title + '=',
766 values = []
767 for item in items:
768 values += ['%s=%s' % (KEYS_TO_NAMES_EN[key],
769 self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000770 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000771 for key in ['id', 'name']
772 if self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000773 _get_item_key(item, key)) != '']
mbligh47dc4d22009-02-12 21:48:34 +0000774 print self.parse_delim.join(values)
mblighdf75f8b2008-11-18 19:07:42 +0000775
776
777 def print_list_std(self, items, key):
Dan Shi3963caa2014-11-26 12:51:25 -0800778 """Print a wrapped list of results
779
780 @param items: Items to to lookup for given key, could be a nested
781 dictionary.
782 @param key: Name of the key to look up for value.
783 """
mblighdf75f8b2008-11-18 19:07:42 +0000784 if not items:
mblighdf75f8b2008-11-18 19:07:42 +0000785 return
showard088b8262009-07-01 22:12:35 +0000786 print ' '.join(_get_item_key(item, key) for item in items)
mblighdf75f8b2008-11-18 19:07:42 +0000787
788
789 def print_list_parse(self, items, key):
Dan Shi3963caa2014-11-26 12:51:25 -0800790 """Print a wrapped list of results.
791
792 @param items: Items to to lookup for given key, could be a nested
793 dictionary.
794 @param key: Name of the key to look up for value.
795 """
mblighdf75f8b2008-11-18 19:07:42 +0000796 if not items:
mblighdf75f8b2008-11-18 19:07:42 +0000797 return
798 print '%s=%s' % (KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000799 ','.join(_get_item_key(item, key) for item in items))
Dan Shi25e1fd42014-12-19 14:36:42 -0800800
801
802 @staticmethod
803 def prompt_confirmation(message=None):
804 """Prompt a question for user to confirm the action before proceeding.
805
806 @param message: A detailed message to explain possible impact of the
807 action.
808
809 @return: True to proceed or False to abort.
810 """
811 if message:
812 print message
813 sys.stdout.write('Continue? [y/N] ')
814 read = raw_input().lower()
815 if read == 'y':
816 return True
817 else:
818 print 'User did not confirm. Aborting...'
819 return False
820
821
822 @staticmethod
823 def require_confirmation(message=None):
824 """Decorator to prompt a question for user to confirm action before
825 proceeding.
826
827 If user chooses not to proceed, do not call the function.
828
829 @param message: A detailed message to explain possible impact of the
830 action.
831
832 @return: A decorator wrapper for calling the actual function.
833 """
834 def deco_require_confirmation(func):
835 """Wrapper for the decorator.
836
837 @param func: Function to be called.
838
839 @return: the actual decorator to call the function.
840 """
841 def func_require_confirmation(*args, **kwargs):
842 """Decorator to prompt a question for user to confirm.
843
844 @param message: A detailed message to explain possible impact of
845 the action.
846 """
847 if (args[0].no_confirmation or
848 atest.prompt_confirmation(message)):
849 func(*args, **kwargs)
850
851 return func_require_confirmation
852 return deco_require_confirmation