blob: 9a90c27fd00e117787e55916f3660dfe06717956 [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',
mblighbe630eb2008-08-01 16:41:48 +0000100 'control_file': 'Control File',
showard989f25d2008-10-01 11:38:11 +0000101 'only_if_needed': 'Use only if needed',
mblighe163b032008-10-18 14:30:27 +0000102 'protection': 'Protection',
showard21baa452008-10-21 00:08:39 +0000103 'run_verify': 'Run verify',
104 'reboot_before': 'Pre-job reboot',
105 'reboot_after': 'Post-job reboot',
mbligh140a23c2008-10-29 16:55:21 +0000106 'experimental': 'Experimental',
mbligh8fadff32009-03-09 21:19:59 +0000107 'synch_count': 'Sync Count',
showardfb64e6a2009-04-22 21:01:18 +0000108 'max_number_of_machines': 'Max. hosts to use',
showarda1e74b32009-05-12 17:32:04 +0000109 'parse_failed_repair': 'Include failed repair results',
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800110 'shard': 'Shard',
mblighbe630eb2008-08-01 16:41:48 +0000111 }
112
113# In the failure, tag that will replace the item.
114FAIL_TAG = '<XYZ>'
115
mbligh8c7b04c2009-03-25 18:01:56 +0000116# Global socket timeout: uploading kernels can take much,
117# much longer than the default
mblighbe630eb2008-08-01 16:41:48 +0000118UPLOAD_SOCKET_TIMEOUT = 60*30
119
120
121# Convertion functions to be called for printing,
122# e.g. to print True/False for booleans.
123def __convert_platform(field):
mbligh0887d402009-01-30 00:50:29 +0000124 if field is None:
mblighbe630eb2008-08-01 16:41:48 +0000125 return ""
mbligh0887d402009-01-30 00:50:29 +0000126 elif isinstance(field, int):
mblighbe630eb2008-08-01 16:41:48 +0000127 # Can be 0/1 for False/True
128 return str(bool(field))
129 else:
130 # Can be a platform name
131 return field
132
133
showard989f25d2008-10-01 11:38:11 +0000134def _int_2_bool_string(value):
135 return str(bool(value))
136
137KEYS_CONVERT = {'locked': _int_2_bool_string,
mblighbe630eb2008-08-01 16:41:48 +0000138 'invalid': lambda flag: str(bool(not flag)),
showard989f25d2008-10-01 11:38:11 +0000139 'only_if_needed': _int_2_bool_string,
mblighbe630eb2008-08-01 16:41:48 +0000140 'platform': __convert_platform,
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800141 'labels': lambda labels: ', '.join(labels),
142 'shards': lambda shard: shard.hostname if shard else ''}
mblighbe630eb2008-08-01 16:41:48 +0000143
showard088b8262009-07-01 22:12:35 +0000144
145def _get_item_key(item, key):
146 """Allow for lookups in nested dictionaries using '.'s within a key."""
147 if key in item:
148 return item[key]
149 nested_item = item
150 for subkey in key.split('.'):
151 if not subkey:
152 raise ValueError('empty subkey in %r' % key)
153 try:
154 nested_item = nested_item[subkey]
155 except KeyError, e:
156 raise KeyError('%r - looking up key %r in %r' %
157 (e, key, nested_item))
158 else:
159 return nested_item
160
161
mblighbe630eb2008-08-01 16:41:48 +0000162class CliError(Exception):
Dan Shi3963caa2014-11-26 12:51:25 -0800163 """Error raised by cli calls.
164 """
mblighbe630eb2008-08-01 16:41:48 +0000165 pass
166
167
mbligh9deeefa2009-05-01 23:11:08 +0000168class item_parse_info(object):
Dan Shi3963caa2014-11-26 12:51:25 -0800169 """Object keeping track of the parsing options.
170 """
171
mbligh9deeefa2009-05-01 23:11:08 +0000172 def __init__(self, attribute_name, inline_option='',
173 filename_option='', use_leftover=False):
174 """Object keeping track of the parsing options that will
175 make up the content of the atest attribute:
Jakob Juelich8b110ee2014-09-15 16:13:42 -0700176 attribute_name: the atest attribute name to populate (label)
mbligh9deeefa2009-05-01 23:11:08 +0000177 inline_option: the option containing the items (--label)
178 filename_option: the option containing the filename (--blist)
179 use_leftover: whether to add the leftover arguments or not."""
180 self.attribute_name = attribute_name
181 self.filename_option = filename_option
182 self.inline_option = inline_option
183 self.use_leftover = use_leftover
184
185
186 def get_values(self, options, leftover=[]):
187 """Returns the value for that attribute by accumualting all
188 the values found through the inline option, the parsing of the
189 file and the leftover"""
jamesrenc2863162010-07-12 21:20:51 +0000190
191 def __get_items(input, split_spaces=True):
192 """Splits a string of comma separated items. Escaped commas will not
193 be split. I.e. Splitting 'a, b\,c, d' will yield ['a', 'b,c', 'd'].
194 If split_spaces is set to False spaces will not be split. I.e.
195 Splitting 'a b, c\,d, e' will yield ['a b', 'c,d', 'e']"""
196
197 # Replace escaped slashes with null characters so we don't misparse
198 # proceeding commas.
199 input = input.replace(r'\\', '\0')
200
201 # Split on commas which are not preceded by a slash.
202 if not split_spaces:
203 split = re.split(r'(?<!\\),', input)
204 else:
205 split = re.split(r'(?<!\\),|\s', input)
206
207 # Convert null characters to single slashes and escaped commas to
208 # just plain commas.
209 return (item.strip().replace('\0', '\\').replace(r'\,', ',') for
210 item in split if item.strip())
mbligh9deeefa2009-05-01 23:11:08 +0000211
212 if self.use_leftover:
213 add_on = leftover
214 leftover = []
215 else:
216 add_on = []
217
218 # Start with the add_on
219 result = set()
220 for items in add_on:
221 # Don't split on space here because the add-on
222 # may have some spaces (like the job name)
jamesrenc2863162010-07-12 21:20:51 +0000223 result.update(__get_items(items, split_spaces=False))
mbligh9deeefa2009-05-01 23:11:08 +0000224
225 # Process the inline_option, if any
226 try:
227 items = getattr(options, self.inline_option)
228 result.update(__get_items(items))
229 except (AttributeError, TypeError):
230 pass
231
232 # Process the file list, if any and not empty
233 # The file can contain space and/or comma separated items
234 try:
235 flist = getattr(options, self.filename_option)
236 file_content = []
237 for line in open(flist).readlines():
238 file_content += __get_items(line)
239 if len(file_content) == 0:
240 raise CliError("Empty file %s" % flist)
241 result.update(file_content)
242 except (AttributeError, TypeError):
243 pass
244 except IOError:
245 raise CliError("Could not open file %s" % flist)
246
247 return list(result), leftover
248
249
mblighbe630eb2008-08-01 16:41:48 +0000250class atest(object):
251 """Common class for generic processing
252 Should only be instantiated by itself for usage
253 references, otherwise, the <topic> objects should
254 be used."""
Allen Li335f2162017-02-01 14:47:01 -0800255 msg_topic = ('[acl|host|job|label|shard|test|user|server|'
Dan Shi25e1fd42014-12-19 14:36:42 -0800256 'stable_version]')
257 usage_action = '[action]'
mblighbe630eb2008-08-01 16:41:48 +0000258 msg_items = ''
259
260 def invalid_arg(self, header, follow_up=''):
Dan Shi3963caa2014-11-26 12:51:25 -0800261 """Fail the command with error that command line has invalid argument.
262
263 @param header: Header of the error message.
264 @param follow_up: Extra error message, default to empty string.
265 """
mblighbe630eb2008-08-01 16:41:48 +0000266 twrap = textwrap.TextWrapper(initial_indent=' ',
267 subsequent_indent=' ')
268 rest = twrap.fill(follow_up)
269
270 if self.kill_on_failure:
271 self.invalid_syntax(header + rest)
272 else:
273 print >> sys.stderr, header + rest
274
275
276 def invalid_syntax(self, msg):
Dan Shi3963caa2014-11-26 12:51:25 -0800277 """Fail the command with error that the command line syntax is wrong.
278
279 @param msg: Error message.
280 """
mblighbe630eb2008-08-01 16:41:48 +0000281 print
282 print >> sys.stderr, msg
283 print
284 print "usage:",
285 print self._get_usage()
286 print
287 sys.exit(1)
288
289
290 def generic_error(self, msg):
Dan Shi3963caa2014-11-26 12:51:25 -0800291 """Fail the command with a generic error.
292
293 @param msg: Error message.
294 """
showardfb64e6a2009-04-22 21:01:18 +0000295 if self.debug:
296 traceback.print_exc()
mblighbe630eb2008-08-01 16:41:48 +0000297 print >> sys.stderr, msg
298 sys.exit(1)
299
300
mbligh7a3ebe32008-12-01 17:10:33 +0000301 def parse_json_exception(self, full_error):
302 """Parses the JSON exception to extract the bad
303 items and returns them
304 This is very kludgy for the moment, but we would need
305 to refactor the exceptions sent from the front end
Dan Shi3963caa2014-11-26 12:51:25 -0800306 to make this better.
307
308 @param full_error: The complete error message.
309 """
mbligh7a3ebe32008-12-01 17:10:33 +0000310 errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
311 parts = errmsg.split(':')
312 # Kludge: If there are 2 colons the last parts contains
313 # the items that failed.
314 if len(parts) != 3:
315 return []
316 return [item.strip() for item in parts[2].split(',') if item.strip()]
317
318
mblighb68405d2010-03-11 18:32:39 +0000319 def failure(self, full_error, item=None, what_failed='', fatal=False):
mblighbe630eb2008-08-01 16:41:48 +0000320 """If kill_on_failure, print this error and die,
321 otherwise, queue the error and accumulate all the items
Dan Shi3963caa2014-11-26 12:51:25 -0800322 that triggered the same error.
323
324 @param full_error: The complete error message.
325 @param item: Name of the actionable item, e.g., hostname.
326 @param what_failed: Name of the failed item.
327 @param fatal: True to exit the program with failure.
328 """
mblighbe630eb2008-08-01 16:41:48 +0000329
330 if self.debug:
331 errmsg = str(full_error)
332 else:
333 errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
334
mblighb68405d2010-03-11 18:32:39 +0000335 if self.kill_on_failure or fatal:
mblighbe630eb2008-08-01 16:41:48 +0000336 print >> sys.stderr, "%s\n %s" % (what_failed, errmsg)
337 sys.exit(1)
338
339 # Build a dictionary with the 'what_failed' as keys. The
340 # values are dictionaries with the errmsg as keys and a set
341 # of items as values.
mbligh1ef218d2009-08-03 16:57:56 +0000342 # self.failed =
mblighbe630eb2008-08-01 16:41:48 +0000343 # {'Operation delete_host_failed': {'AclAccessViolation:
344 # set('host0', 'host1')}}
345 # Try to gather all the same error messages together,
346 # even if they contain the 'item'
347 if item and item in errmsg:
348 errmsg = errmsg.replace(item, FAIL_TAG)
349 if self.failed.has_key(what_failed):
350 self.failed[what_failed].setdefault(errmsg, set()).add(item)
351 else:
352 self.failed[what_failed] = {errmsg: set([item])}
353
354
355 def show_all_failures(self):
Dan Shi3963caa2014-11-26 12:51:25 -0800356 """Print all failure information.
357 """
mblighbe630eb2008-08-01 16:41:48 +0000358 if not self.failed:
359 return 0
360 for what_failed in self.failed.keys():
361 print >> sys.stderr, what_failed + ':'
362 for (errmsg, items) in self.failed[what_failed].iteritems():
363 if len(items) == 0:
364 print >> sys.stderr, errmsg
365 elif items == set(['']):
366 print >> sys.stderr, ' ' + errmsg
367 elif len(items) == 1:
368 # Restore the only item
369 if FAIL_TAG in errmsg:
370 errmsg = errmsg.replace(FAIL_TAG, items.pop())
371 else:
372 errmsg = '%s (%s)' % (errmsg, items.pop())
373 print >> sys.stderr, ' ' + errmsg
374 else:
375 print >> sys.stderr, ' ' + errmsg + ' with <XYZ> in:'
376 twrap = textwrap.TextWrapper(initial_indent=' ',
377 subsequent_indent=' ')
378 items = list(items)
379 items.sort()
380 print >> sys.stderr, twrap.fill(', '.join(items))
381 return 1
382
383
384 def __init__(self):
385 """Setup the parser common options"""
386 # Initialized for unit tests.
387 self.afe = None
388 self.failed = {}
389 self.data = {}
390 self.debug = False
mbligh47dc4d22009-02-12 21:48:34 +0000391 self.parse_delim = '|'
mblighbe630eb2008-08-01 16:41:48 +0000392 self.kill_on_failure = False
393 self.web_server = ''
394 self.verbose = False
Dan Shi25e1fd42014-12-19 14:36:42 -0800395 self.no_confirmation = False
mbligh9deeefa2009-05-01 23:11:08 +0000396 self.topic_parse_info = item_parse_info(attribute_name='not_used')
mblighbe630eb2008-08-01 16:41:48 +0000397
398 self.parser = optparse.OptionParser(self._get_usage())
399 self.parser.add_option('-g', '--debug',
400 help='Print debugging information',
401 action='store_true', default=False)
402 self.parser.add_option('--kill-on-failure',
403 help='Stop at the first failure',
404 action='store_true', default=False)
405 self.parser.add_option('--parse',
mbligh47dc4d22009-02-12 21:48:34 +0000406 help='Print the output using | '
mblighbe630eb2008-08-01 16:41:48 +0000407 'separated key=value fields',
408 action='store_true', default=False)
mbligh47dc4d22009-02-12 21:48:34 +0000409 self.parser.add_option('--parse-delim',
410 help='Delimiter to use to separate the '
411 'key=value fields', default='|')
Dan Shi25e1fd42014-12-19 14:36:42 -0800412 self.parser.add_option('--no-confirmation',
413 help=('Skip all confirmation in when function '
414 'require_confirmation is called.'),
415 action='store_true', default=False)
mblighbe630eb2008-08-01 16:41:48 +0000416 self.parser.add_option('-v', '--verbose',
417 action='store_true', default=False)
418 self.parser.add_option('-w', '--web',
419 help='Specify the autotest server '
420 'to talk to',
421 action='store', type='string',
422 dest='web_server', default=None)
423
mblighbe630eb2008-08-01 16:41:48 +0000424
mblighbe630eb2008-08-01 16:41:48 +0000425 def _get_usage(self):
426 return "atest %s %s [options] %s" % (self.msg_topic.lower(),
427 self.usage_action,
428 self.msg_items)
429
430
mbligh5a496082009-08-03 16:44:54 +0000431 def backward_compatibility(self, action, argv):
Dan Shi3963caa2014-11-26 12:51:25 -0800432 """To be overidden by subclass if their syntax changed.
433
434 @param action: Name of the action.
435 @param argv: A list of arguments.
436 """
mbligh5a496082009-08-03 16:44:54 +0000437 return action
438
439
mbligh9deeefa2009-05-01 23:11:08 +0000440 def parse(self, parse_info=[], req_items=None):
Dan Shi3963caa2014-11-26 12:51:25 -0800441 """Parse command arguments.
mblighbe630eb2008-08-01 16:41:48 +0000442
Dan Shi3963caa2014-11-26 12:51:25 -0800443 parse_info is a list of item_parse_info objects.
mbligh9deeefa2009-05-01 23:11:08 +0000444 There should only be one use_leftover set to True in the list.
mblighbe630eb2008-08-01 16:41:48 +0000445
Dan Shi3963caa2014-11-26 12:51:25 -0800446 Also check that the req_items is not empty after parsing.
447
448 @param parse_info: A list of item_parse_info objects.
449 @param req_items: A list of required items.
450 """
mbligh9deeefa2009-05-01 23:11:08 +0000451 (options, leftover) = self.parse_global()
mblighbe630eb2008-08-01 16:41:48 +0000452
mbligh9deeefa2009-05-01 23:11:08 +0000453 all_parse_info = parse_info[:]
454 all_parse_info.append(self.topic_parse_info)
455
456 try:
457 for item_parse_info in all_parse_info:
458 values, leftover = item_parse_info.get_values(options,
459 leftover)
460 setattr(self, item_parse_info.attribute_name, values)
461 except CliError, s:
462 self.invalid_syntax(s)
mblighbe630eb2008-08-01 16:41:48 +0000463
464 if (req_items and not getattr(self, req_items, None)):
465 self.invalid_syntax('%s %s requires at least one %s' %
466 (self.msg_topic,
467 self.usage_action,
468 self.msg_topic))
469
470 return (options, leftover)
471
472
mbligh9deeefa2009-05-01 23:11:08 +0000473 def parse_global(self):
474 """Parse the global arguments.
mblighbe630eb2008-08-01 16:41:48 +0000475
476 It consumes what the common object needs to know, and
477 let the children look at all the options. We could
478 remove the options that we have used, but there is no
479 harm in leaving them, and the children may need them
480 in the future.
481
482 Must be called from its children parse()"""
483 (options, leftover) = self.parser.parse_args()
484 # Handle our own options setup in __init__()
485 self.debug = options.debug
486 self.kill_on_failure = options.kill_on_failure
487
488 if options.parse:
489 suffix = '_parse'
490 else:
491 suffix = '_std'
492 for func in ['print_fields', 'print_table',
mblighdf75f8b2008-11-18 19:07:42 +0000493 'print_by_ids', 'print_list']:
mblighbe630eb2008-08-01 16:41:48 +0000494 setattr(self, func, getattr(self, func + suffix))
495
mbligh47dc4d22009-02-12 21:48:34 +0000496 self.parse_delim = options.parse_delim
497
mblighbe630eb2008-08-01 16:41:48 +0000498 self.verbose = options.verbose
Dan Shi25e1fd42014-12-19 14:36:42 -0800499 self.no_confirmation = options.no_confirmation
mblighbe630eb2008-08-01 16:41:48 +0000500 self.web_server = options.web_server
mblighb68405d2010-03-11 18:32:39 +0000501 try:
502 self.afe = rpc.afe_comm(self.web_server)
503 except rpc.AuthError, s:
504 self.failure(str(s), fatal=True)
mblighbe630eb2008-08-01 16:41:48 +0000505
506 return (options, leftover)
507
508
509 def check_and_create_items(self, op_get, op_create,
510 items, **data_create):
Dan Shi3963caa2014-11-26 12:51:25 -0800511 """Create the items if they don't exist already.
512
513 @param op_get: Name of `get` RPC.
514 @param op_create: Name of `create` RPC.
515 @param items: Actionable items specified in CLI command, e.g., hostname,
516 to be passed to each RPC.
517 @param data_create: Data to be passed to `create` RPC.
518 """
mblighbe630eb2008-08-01 16:41:48 +0000519 for item in items:
520 ret = self.execute_rpc(op_get, name=item)
521
522 if len(ret) == 0:
523 try:
524 data_create['name'] = item
525 self.execute_rpc(op_create, **data_create)
526 except CliError:
527 continue
528
529
530 def execute_rpc(self, op, item='', **data):
Dan Shi3963caa2014-11-26 12:51:25 -0800531 """Execute RPC.
532
533 @param op: Name of the RPC.
534 @param item: Actionable item specified in CLI command.
535 @param data: Data to be passed to RPC.
536 """
mblighbe630eb2008-08-01 16:41:48 +0000537 retry = 2
538 while retry:
539 try:
540 return self.afe.run(op, **data)
541 except urllib2.URLError, err:
mbligh11efd232008-11-27 00:20:46 +0000542 if hasattr(err, 'reason'):
543 if 'timed out' not in err.reason:
544 self.invalid_syntax('Invalid server name %s: %s' %
545 (self.afe.web_server, err))
546 if hasattr(err, 'code'):
showard53d91e22010-01-15 00:18:27 +0000547 error_parts = [str(err)]
548 if self.debug:
549 error_parts.append(err.read()) # read the response body
550 self.failure('\n\n'.join(error_parts), item=item,
mbligh11efd232008-11-27 00:20:46 +0000551 what_failed=("Error received from web server"))
552 raise CliError("Error from web server")
mblighbe630eb2008-08-01 16:41:48 +0000553 if self.debug:
554 print 'retrying: %r %d' % (data, retry)
555 retry -= 1
556 if retry == 0:
557 if item:
558 myerr = '%s timed out for %s' % (op, item)
559 else:
560 myerr = '%s timed out' % op
561 self.failure(myerr, item=item,
562 what_failed=("Timed-out contacting "
563 "the Autotest server"))
564 raise CliError("Timed-out contacting the Autotest server")
mblighcd26d042010-05-03 18:58:24 +0000565 except mock.CheckPlaybackError:
566 raise
mblighbe630eb2008-08-01 16:41:48 +0000567 except Exception, full_error:
568 # There are various exceptions throwns by JSON,
569 # urllib & httplib, so catch them all.
570 self.failure(full_error, item=item,
571 what_failed='Operation %s failed' % op)
572 raise CliError(str(full_error))
573
574
575 # There is no output() method in the atest object (yet?)
576 # but here are some helper functions to be used by its
577 # children
578 def print_wrapped(self, msg, values):
Dan Shi3963caa2014-11-26 12:51:25 -0800579 """Print given message and values in wrapped lines unless
580 AUTOTEST_CLI_NO_WRAP is specified in environment variables.
581
582 @param msg: Message to print.
583 @param values: A list of values to print.
584 """
mblighbe630eb2008-08-01 16:41:48 +0000585 if len(values) == 0:
586 return
587 elif len(values) == 1:
588 print msg + ': '
589 elif len(values) > 1:
590 if msg.endswith('s'):
591 print msg + ': '
592 else:
593 print msg + 's: '
594
595 values.sort()
mbligh552d2402009-09-18 19:35:23 +0000596
597 if 'AUTOTEST_CLI_NO_WRAP' in os.environ:
598 print '\n'.join(values)
599 return
600
mblighbe630eb2008-08-01 16:41:48 +0000601 twrap = textwrap.TextWrapper(initial_indent='\t',
602 subsequent_indent='\t')
603 print twrap.fill(', '.join(values))
604
605
606 def __conv_value(self, type, value):
607 return KEYS_CONVERT.get(type, str)(value)
608
609
610 def print_fields_std(self, items, keys, title=None):
Dan Shi3963caa2014-11-26 12:51:25 -0800611 """Print the keys in each item, one on each line.
612
613 @param items: Items to print.
614 @param keys: Name of the keys to look up each item in items.
615 @param title: Title of the output, default to None.
616 """
mblighbe630eb2008-08-01 16:41:48 +0000617 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000618 return
619 if title:
620 print title
621 for item in items:
622 for key in keys:
623 print '%s: %s' % (KEYS_TO_NAMES_EN[key],
624 self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000625 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000626
627
628 def print_fields_parse(self, items, keys, title=None):
Dan Shi3963caa2014-11-26 12:51:25 -0800629 """Print the keys in each item as comma separated name=value
630
631 @param items: Items to print.
632 @param keys: Name of the keys to look up each item in items.
633 @param title: Title of the output, default to None.
634 """
mblighbe630eb2008-08-01 16:41:48 +0000635 for item in items:
636 values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
637 self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000638 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000639 for key in keys
640 if self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000641 _get_item_key(item, key)) != '']
mbligh47dc4d22009-02-12 21:48:34 +0000642 print self.parse_delim.join(values)
mblighbe630eb2008-08-01 16:41:48 +0000643
644
645 def __find_justified_fmt(self, items, keys):
Dan Shi3963caa2014-11-26 12:51:25 -0800646 """Find the max length for each field.
647
648 @param items: Items to lookup for.
649 @param keys: Name of the keys to look up each item in items.
650 """
mblighbe630eb2008-08-01 16:41:48 +0000651 lens = {}
652 # Don't justify the last field, otherwise we have blank
653 # lines when the max is overlaps but the current values
654 # are smaller
655 if not items:
656 print "No results"
657 return
658 for key in keys[:-1]:
659 lens[key] = max(len(self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000660 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000661 for item in items)
662 lens[key] = max(lens[key], len(KEYS_TO_NAMES_EN[key]))
663 lens[keys[-1]] = 0
664
665 return ' '.join(["%%-%ds" % lens[key] for key in keys])
666
667
Simran Basi0739d682015-02-25 16:22:56 -0800668 def print_dict(self, items, title=None, line_before=False):
669 """Print a dictionary.
670
671 @param items: Dictionary to print.
672 @param title: Title of the output, default to None.
673 @param line_before: True to print an empty line before the output,
674 default to False.
675 """
676 if not items:
677 return
678 if line_before:
679 print
680 print title
681 for key, value in items.items():
682 print '%s : %s' % (key, value)
683
684
mbligh838c7472009-05-13 20:56:50 +0000685 def print_table_std(self, items, keys_header, sublist_keys=()):
Dan Shi3963caa2014-11-26 12:51:25 -0800686 """Print a mix of header and lists in a user readable format.
687
688 The headers are justified, the sublist_keys are wrapped.
689
690 @param items: Items to print.
691 @param keys_header: Header of the keys, use to look up in items.
692 @param sublist_keys: Keys for sublist in each item.
693 """
mblighbe630eb2008-08-01 16:41:48 +0000694 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000695 return
696 fmt = self.__find_justified_fmt(items, keys_header)
697 header = tuple(KEYS_TO_NAMES_EN[key] for key in keys_header)
698 print fmt % header
699 for item in items:
showard088b8262009-07-01 22:12:35 +0000700 values = tuple(self.__conv_value(key,
701 _get_item_key(item, key))
mblighbe630eb2008-08-01 16:41:48 +0000702 for key in keys_header)
703 print fmt % values
mbligh838c7472009-05-13 20:56:50 +0000704 if sublist_keys:
mblighbe630eb2008-08-01 16:41:48 +0000705 for key in sublist_keys:
706 self.print_wrapped(KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000707 _get_item_key(item, key))
mblighbe630eb2008-08-01 16:41:48 +0000708 print '\n'
709
710
mbligh838c7472009-05-13 20:56:50 +0000711 def print_table_parse(self, items, keys_header, sublist_keys=()):
Dan Shi3963caa2014-11-26 12:51:25 -0800712 """Print a mix of header and lists in a user readable format.
713
714 @param items: Items to print.
715 @param keys_header: Header of the keys, use to look up in items.
716 @param sublist_keys: Keys for sublist in each item.
717 """
mblighbe630eb2008-08-01 16:41:48 +0000718 for item in items:
719 values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000720 self.__conv_value(key, _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000721 for key in keys_header
722 if self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000723 _get_item_key(item, key)) != '']
mblighbe630eb2008-08-01 16:41:48 +0000724
mbligh838c7472009-05-13 20:56:50 +0000725 if sublist_keys:
mblighbe630eb2008-08-01 16:41:48 +0000726 [values.append('%s=%s'% (KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000727 ','.join(_get_item_key(item, key))))
mblighbe630eb2008-08-01 16:41:48 +0000728 for key in sublist_keys
showard088b8262009-07-01 22:12:35 +0000729 if len(_get_item_key(item, key))]
mblighbe630eb2008-08-01 16:41:48 +0000730
mbligh47dc4d22009-02-12 21:48:34 +0000731 print self.parse_delim.join(values)
mblighbe630eb2008-08-01 16:41:48 +0000732
733
734 def print_by_ids_std(self, items, title=None, line_before=False):
Dan Shi3963caa2014-11-26 12:51:25 -0800735 """Prints ID & names of items in a user readable form.
736
737 @param items: Items to print.
738 @param title: Title of the output, default to None.
739 @param line_before: True to print an empty line before the output,
740 default to False.
741 """
mblighbe630eb2008-08-01 16:41:48 +0000742 if not items:
743 return
744 if line_before:
745 print
746 if title:
747 print title + ':'
748 self.print_table_std(items, keys_header=['id', 'name'])
749
750
751 def print_by_ids_parse(self, items, title=None, line_before=False):
Dan Shi3963caa2014-11-26 12:51:25 -0800752 """Prints ID & names of items in a parseable format.
753
754 @param items: Items to print.
755 @param title: Title of the output, default to None.
756 @param line_before: True to print an empty line before the output,
757 default to False.
758 """
mblighbe630eb2008-08-01 16:41:48 +0000759 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000760 return
Dan Shi3963caa2014-11-26 12:51:25 -0800761 if line_before:
762 print
mblighbe630eb2008-08-01 16:41:48 +0000763 if title:
764 print title + '=',
765 values = []
766 for item in items:
767 values += ['%s=%s' % (KEYS_TO_NAMES_EN[key],
768 self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000769 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000770 for key in ['id', 'name']
771 if self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000772 _get_item_key(item, key)) != '']
mbligh47dc4d22009-02-12 21:48:34 +0000773 print self.parse_delim.join(values)
mblighdf75f8b2008-11-18 19:07:42 +0000774
775
776 def print_list_std(self, items, key):
Dan Shi3963caa2014-11-26 12:51:25 -0800777 """Print a wrapped list of results
778
779 @param items: Items to to lookup for given key, could be a nested
780 dictionary.
781 @param key: Name of the key to look up for value.
782 """
mblighdf75f8b2008-11-18 19:07:42 +0000783 if not items:
mblighdf75f8b2008-11-18 19:07:42 +0000784 return
showard088b8262009-07-01 22:12:35 +0000785 print ' '.join(_get_item_key(item, key) for item in items)
mblighdf75f8b2008-11-18 19:07:42 +0000786
787
788 def print_list_parse(self, items, key):
Dan Shi3963caa2014-11-26 12:51:25 -0800789 """Print a wrapped list of results.
790
791 @param items: Items to to lookup for given key, could be a nested
792 dictionary.
793 @param key: Name of the key to look up for value.
794 """
mblighdf75f8b2008-11-18 19:07:42 +0000795 if not items:
mblighdf75f8b2008-11-18 19:07:42 +0000796 return
797 print '%s=%s' % (KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000798 ','.join(_get_item_key(item, key) for item in items))
Dan Shi25e1fd42014-12-19 14:36:42 -0800799
800
801 @staticmethod
802 def prompt_confirmation(message=None):
803 """Prompt a question for user to confirm the action before proceeding.
804
805 @param message: A detailed message to explain possible impact of the
806 action.
807
808 @return: True to proceed or False to abort.
809 """
810 if message:
811 print message
812 sys.stdout.write('Continue? [y/N] ')
813 read = raw_input().lower()
814 if read == 'y':
815 return True
816 else:
817 print 'User did not confirm. Aborting...'
818 return False
819
820
821 @staticmethod
822 def require_confirmation(message=None):
823 """Decorator to prompt a question for user to confirm action before
824 proceeding.
825
826 If user chooses not to proceed, do not call the function.
827
828 @param message: A detailed message to explain possible impact of the
829 action.
830
831 @return: A decorator wrapper for calling the actual function.
832 """
833 def deco_require_confirmation(func):
834 """Wrapper for the decorator.
835
836 @param func: Function to be called.
837
838 @return: the actual decorator to call the function.
839 """
840 def func_require_confirmation(*args, **kwargs):
841 """Decorator to prompt a question for user to confirm.
842
843 @param message: A detailed message to explain possible impact of
844 the action.
845 """
846 if (args[0].no_confirmation or
847 atest.prompt_confirmation(message)):
848 func(*args, **kwargs)
849
850 return func_require_confirmation
851 return deco_require_confirmation