blob: c85a2e3e3cb0b44fe07574673db68e05bfa6b6c1 [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
Xixuan Wu84fe67b2020-12-30 13:31:09 -080013'atest server list ...' as an example:
mblighbe630eb2008-08-01 16:41:48 +000014
Xixuan Wu84fe67b2020-12-30 13:31:09 -080015atest <-- server <-- server_list
mblighbe630eb2008-08-01 16:41:48 +000016
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
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -070058from __future__ import print_function
59
Ningning Xia5aaca4a2018-05-16 12:18:31 -070060import logging
Dan Shi3963caa2014-11-26 12:51:25 -080061import optparse
62import os
63import re
64import sys
65import textwrap
66import traceback
67import urllib2
68
Ningning Xia84190b82018-04-16 15:01:40 -070069import common
70
mblighbe630eb2008-08-01 16:41:48 +000071from autotest_lib.cli import rpc
Ningning Xia9c188b92018-04-27 15:34:23 -070072from autotest_lib.cli import skylab_utils
mblighcd26d042010-05-03 18:58:24 +000073from autotest_lib.client.common_lib.test_utils import mock
Ningning Xia84190b82018-04-16 15:01:40 -070074from autotest_lib.client.common_lib import autotemp
Ningning Xia9c188b92018-04-27 15:34:23 -070075
Ningning Xiaa7ba0c22018-04-30 11:02:30 -070076skylab_inventory_imported = False
Ningning Xia9c188b92018-04-27 15:34:23 -070077try:
78 from skylab_inventory import translation_utils
Ningning Xiaa7ba0c22018-04-30 11:02:30 -070079 skylab_inventory_imported = True
Ningning Xia9c188b92018-04-27 15:34:23 -070080except ImportError:
Ningning Xiaa7ba0c22018-04-30 11:02:30 -070081 pass
mblighbe630eb2008-08-01 16:41:48 +000082
83
84# Maps the AFE keys to printable names.
85KEYS_TO_NAMES_EN = {'hostname': 'Host',
86 'platform': 'Platform',
87 'status': 'Status',
88 'locked': 'Locked',
89 'locked_by': 'Locked by',
mblighe163b032008-10-18 14:30:27 +000090 'lock_time': 'Locked time',
Matthew Sartori68186332015-04-27 17:19:53 -070091 'lock_reason': 'Lock Reason',
mblighbe630eb2008-08-01 16:41:48 +000092 'labels': 'Labels',
93 'description': 'Description',
94 'hosts': 'Hosts',
95 'users': 'Users',
96 'id': 'Id',
97 'name': 'Name',
98 'invalid': 'Valid',
99 'login': 'Login',
100 'access_level': 'Access Level',
101 'job_id': 'Job Id',
102 'job_owner': 'Job Owner',
103 'job_name': 'Job Name',
104 'test_type': 'Test Type',
105 'test_class': 'Test Class',
106 'path': 'Path',
107 'owner': 'Owner',
108 'status_counts': 'Status Counts',
109 'hosts_status': 'Host Status',
mblighfca5ed12009-11-06 02:59:56 +0000110 'hosts_selected_status': 'Hosts filtered by Status',
mblighbe630eb2008-08-01 16:41:48 +0000111 'priority': 'Priority',
112 'control_type': 'Control Type',
113 'created_on': 'Created On',
mblighbe630eb2008-08-01 16:41:48 +0000114 'control_file': 'Control File',
showard989f25d2008-10-01 11:38:11 +0000115 'only_if_needed': 'Use only if needed',
mblighe163b032008-10-18 14:30:27 +0000116 'protection': 'Protection',
showard21baa452008-10-21 00:08:39 +0000117 'run_verify': 'Run verify',
118 'reboot_before': 'Pre-job reboot',
119 'reboot_after': 'Post-job reboot',
mbligh140a23c2008-10-29 16:55:21 +0000120 'experimental': 'Experimental',
mbligh8fadff32009-03-09 21:19:59 +0000121 'synch_count': 'Sync Count',
showardfb64e6a2009-04-22 21:01:18 +0000122 'max_number_of_machines': 'Max. hosts to use',
showarda1e74b32009-05-12 17:32:04 +0000123 'parse_failed_repair': 'Include failed repair results',
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800124 'shard': 'Shard',
mblighbe630eb2008-08-01 16:41:48 +0000125 }
126
127# In the failure, tag that will replace the item.
128FAIL_TAG = '<XYZ>'
129
mbligh8c7b04c2009-03-25 18:01:56 +0000130# Global socket timeout: uploading kernels can take much,
131# much longer than the default
mblighbe630eb2008-08-01 16:41:48 +0000132UPLOAD_SOCKET_TIMEOUT = 60*30
133
Ningning Xia5aaca4a2018-05-16 12:18:31 -0700134LOGGING_LEVEL_MAP = {
135 'CRITICAL': logging.CRITICAL,
136 'ERROR': logging.ERROR,
137 'WARNING': logging.WARNING,
138 'INFO': logging.INFO,
139 'DEBUG': logging.DEBUG,
140}
141
mblighbe630eb2008-08-01 16:41:48 +0000142
143# Convertion functions to be called for printing,
144# e.g. to print True/False for booleans.
145def __convert_platform(field):
mbligh0887d402009-01-30 00:50:29 +0000146 if field is None:
mblighbe630eb2008-08-01 16:41:48 +0000147 return ""
mbligh0887d402009-01-30 00:50:29 +0000148 elif isinstance(field, int):
mblighbe630eb2008-08-01 16:41:48 +0000149 # Can be 0/1 for False/True
150 return str(bool(field))
151 else:
152 # Can be a platform name
153 return field
154
155
showard989f25d2008-10-01 11:38:11 +0000156def _int_2_bool_string(value):
157 return str(bool(value))
158
159KEYS_CONVERT = {'locked': _int_2_bool_string,
mblighbe630eb2008-08-01 16:41:48 +0000160 'invalid': lambda flag: str(bool(not flag)),
showard989f25d2008-10-01 11:38:11 +0000161 'only_if_needed': _int_2_bool_string,
mblighbe630eb2008-08-01 16:41:48 +0000162 'platform': __convert_platform,
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800163 'labels': lambda labels: ', '.join(labels),
164 'shards': lambda shard: shard.hostname if shard else ''}
mblighbe630eb2008-08-01 16:41:48 +0000165
showard088b8262009-07-01 22:12:35 +0000166
167def _get_item_key(item, key):
168 """Allow for lookups in nested dictionaries using '.'s within a key."""
169 if key in item:
170 return item[key]
171 nested_item = item
172 for subkey in key.split('.'):
173 if not subkey:
174 raise ValueError('empty subkey in %r' % key)
175 try:
176 nested_item = nested_item[subkey]
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700177 except KeyError as e:
showard088b8262009-07-01 22:12:35 +0000178 raise KeyError('%r - looking up key %r in %r' %
179 (e, key, nested_item))
180 else:
181 return nested_item
182
183
mblighbe630eb2008-08-01 16:41:48 +0000184class CliError(Exception):
Dan Shi3963caa2014-11-26 12:51:25 -0800185 """Error raised by cli calls.
186 """
mblighbe630eb2008-08-01 16:41:48 +0000187 pass
188
189
mbligh9deeefa2009-05-01 23:11:08 +0000190class item_parse_info(object):
Dan Shi3963caa2014-11-26 12:51:25 -0800191 """Object keeping track of the parsing options.
192 """
193
mbligh9deeefa2009-05-01 23:11:08 +0000194 def __init__(self, attribute_name, inline_option='',
195 filename_option='', use_leftover=False):
196 """Object keeping track of the parsing options that will
197 make up the content of the atest attribute:
Jakob Juelich8b110ee2014-09-15 16:13:42 -0700198 attribute_name: the atest attribute name to populate (label)
mbligh9deeefa2009-05-01 23:11:08 +0000199 inline_option: the option containing the items (--label)
200 filename_option: the option containing the filename (--blist)
201 use_leftover: whether to add the leftover arguments or not."""
202 self.attribute_name = attribute_name
203 self.filename_option = filename_option
204 self.inline_option = inline_option
205 self.use_leftover = use_leftover
206
207
208 def get_values(self, options, leftover=[]):
209 """Returns the value for that attribute by accumualting all
210 the values found through the inline option, the parsing of the
211 file and the leftover"""
jamesrenc2863162010-07-12 21:20:51 +0000212
213 def __get_items(input, split_spaces=True):
214 """Splits a string of comma separated items. Escaped commas will not
215 be split. I.e. Splitting 'a, b\,c, d' will yield ['a', 'b,c', 'd'].
216 If split_spaces is set to False spaces will not be split. I.e.
217 Splitting 'a b, c\,d, e' will yield ['a b', 'c,d', 'e']"""
218
219 # Replace escaped slashes with null characters so we don't misparse
220 # proceeding commas.
221 input = input.replace(r'\\', '\0')
222
223 # Split on commas which are not preceded by a slash.
224 if not split_spaces:
225 split = re.split(r'(?<!\\),', input)
226 else:
227 split = re.split(r'(?<!\\),|\s', input)
228
229 # Convert null characters to single slashes and escaped commas to
230 # just plain commas.
231 return (item.strip().replace('\0', '\\').replace(r'\,', ',') for
232 item in split if item.strip())
mbligh9deeefa2009-05-01 23:11:08 +0000233
234 if self.use_leftover:
235 add_on = leftover
236 leftover = []
237 else:
238 add_on = []
239
240 # Start with the add_on
241 result = set()
242 for items in add_on:
243 # Don't split on space here because the add-on
244 # may have some spaces (like the job name)
jamesrenc2863162010-07-12 21:20:51 +0000245 result.update(__get_items(items, split_spaces=False))
mbligh9deeefa2009-05-01 23:11:08 +0000246
247 # Process the inline_option, if any
248 try:
249 items = getattr(options, self.inline_option)
250 result.update(__get_items(items))
251 except (AttributeError, TypeError):
252 pass
253
254 # Process the file list, if any and not empty
255 # The file can contain space and/or comma separated items
256 try:
257 flist = getattr(options, self.filename_option)
258 file_content = []
259 for line in open(flist).readlines():
260 file_content += __get_items(line)
261 if len(file_content) == 0:
262 raise CliError("Empty file %s" % flist)
263 result.update(file_content)
264 except (AttributeError, TypeError):
265 pass
266 except IOError:
267 raise CliError("Could not open file %s" % flist)
268
269 return list(result), leftover
270
271
mblighbe630eb2008-08-01 16:41:48 +0000272class atest(object):
273 """Common class for generic processing
274 Should only be instantiated by itself for usage
275 references, otherwise, the <topic> objects should
276 be used."""
Xixuan Wu84fe67b2020-12-30 13:31:09 -0800277 msg_topic = '[acl|job|label|shard|test|user|server]'
Dan Shi25e1fd42014-12-19 14:36:42 -0800278 usage_action = '[action]'
mblighbe630eb2008-08-01 16:41:48 +0000279 msg_items = ''
280
281 def invalid_arg(self, header, follow_up=''):
Dan Shi3963caa2014-11-26 12:51:25 -0800282 """Fail the command with error that command line has invalid argument.
283
284 @param header: Header of the error message.
285 @param follow_up: Extra error message, default to empty string.
286 """
mblighbe630eb2008-08-01 16:41:48 +0000287 twrap = textwrap.TextWrapper(initial_indent=' ',
288 subsequent_indent=' ')
289 rest = twrap.fill(follow_up)
290
291 if self.kill_on_failure:
292 self.invalid_syntax(header + rest)
293 else:
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700294 print(header + rest, file=sys.stderr)
mblighbe630eb2008-08-01 16:41:48 +0000295
296
297 def invalid_syntax(self, msg):
Dan Shi3963caa2014-11-26 12:51:25 -0800298 """Fail the command with error that the command line syntax is wrong.
299
300 @param msg: Error message.
301 """
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700302 print()
303 print(msg, file=sys.stderr)
304 print()
305 print("usage:")
306 print(self._get_usage())
307 print()
mblighbe630eb2008-08-01 16:41:48 +0000308 sys.exit(1)
309
310
311 def generic_error(self, msg):
Dan Shi3963caa2014-11-26 12:51:25 -0800312 """Fail the command with a generic error.
313
314 @param msg: Error message.
315 """
showardfb64e6a2009-04-22 21:01:18 +0000316 if self.debug:
317 traceback.print_exc()
mblighbe630eb2008-08-01 16:41:48 +0000318 print >> sys.stderr, msg
319 sys.exit(1)
320
321
mbligh7a3ebe32008-12-01 17:10:33 +0000322 def parse_json_exception(self, full_error):
323 """Parses the JSON exception to extract the bad
324 items and returns them
325 This is very kludgy for the moment, but we would need
326 to refactor the exceptions sent from the front end
Dan Shi3963caa2014-11-26 12:51:25 -0800327 to make this better.
328
329 @param full_error: The complete error message.
330 """
mbligh7a3ebe32008-12-01 17:10:33 +0000331 errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
332 parts = errmsg.split(':')
333 # Kludge: If there are 2 colons the last parts contains
334 # the items that failed.
335 if len(parts) != 3:
336 return []
337 return [item.strip() for item in parts[2].split(',') if item.strip()]
338
339
mblighb68405d2010-03-11 18:32:39 +0000340 def failure(self, full_error, item=None, what_failed='', fatal=False):
mblighbe630eb2008-08-01 16:41:48 +0000341 """If kill_on_failure, print this error and die,
342 otherwise, queue the error and accumulate all the items
Dan Shi3963caa2014-11-26 12:51:25 -0800343 that triggered the same error.
344
345 @param full_error: The complete error message.
346 @param item: Name of the actionable item, e.g., hostname.
347 @param what_failed: Name of the failed item.
348 @param fatal: True to exit the program with failure.
349 """
mblighbe630eb2008-08-01 16:41:48 +0000350
351 if self.debug:
352 errmsg = str(full_error)
353 else:
354 errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
355
mblighb68405d2010-03-11 18:32:39 +0000356 if self.kill_on_failure or fatal:
mblighbe630eb2008-08-01 16:41:48 +0000357 print >> sys.stderr, "%s\n %s" % (what_failed, errmsg)
358 sys.exit(1)
359
360 # Build a dictionary with the 'what_failed' as keys. The
361 # values are dictionaries with the errmsg as keys and a set
362 # of items as values.
mbligh1ef218d2009-08-03 16:57:56 +0000363 # self.failed =
mblighbe630eb2008-08-01 16:41:48 +0000364 # {'Operation delete_host_failed': {'AclAccessViolation:
365 # set('host0', 'host1')}}
366 # Try to gather all the same error messages together,
367 # even if they contain the 'item'
368 if item and item in errmsg:
369 errmsg = errmsg.replace(item, FAIL_TAG)
370 if self.failed.has_key(what_failed):
371 self.failed[what_failed].setdefault(errmsg, set()).add(item)
372 else:
373 self.failed[what_failed] = {errmsg: set([item])}
374
375
376 def show_all_failures(self):
Dan Shi3963caa2014-11-26 12:51:25 -0800377 """Print all failure information.
378 """
mblighbe630eb2008-08-01 16:41:48 +0000379 if not self.failed:
380 return 0
381 for what_failed in self.failed.keys():
382 print >> sys.stderr, what_failed + ':'
383 for (errmsg, items) in self.failed[what_failed].iteritems():
384 if len(items) == 0:
385 print >> sys.stderr, errmsg
386 elif items == set(['']):
387 print >> sys.stderr, ' ' + errmsg
388 elif len(items) == 1:
389 # Restore the only item
390 if FAIL_TAG in errmsg:
391 errmsg = errmsg.replace(FAIL_TAG, items.pop())
392 else:
393 errmsg = '%s (%s)' % (errmsg, items.pop())
394 print >> sys.stderr, ' ' + errmsg
395 else:
396 print >> sys.stderr, ' ' + errmsg + ' with <XYZ> in:'
397 twrap = textwrap.TextWrapper(initial_indent=' ',
398 subsequent_indent=' ')
399 items = list(items)
400 items.sort()
401 print >> sys.stderr, twrap.fill(', '.join(items))
402 return 1
403
404
405 def __init__(self):
406 """Setup the parser common options"""
407 # Initialized for unit tests.
408 self.afe = None
409 self.failed = {}
410 self.data = {}
411 self.debug = False
mbligh47dc4d22009-02-12 21:48:34 +0000412 self.parse_delim = '|'
mblighbe630eb2008-08-01 16:41:48 +0000413 self.kill_on_failure = False
414 self.web_server = ''
415 self.verbose = False
Anh Le004b0ab2020-11-04 21:20:45 +0000416 self.no_confirmation = False
Ningning Xia84190b82018-04-16 15:01:40 -0700417 # Whether the topic or command supports skylab inventory repo.
418 self.allow_skylab = False
Ningning Xia9df3d152018-05-23 17:15:14 -0700419 self.enforce_skylab = False
mbligh9deeefa2009-05-01 23:11:08 +0000420 self.topic_parse_info = item_parse_info(attribute_name='not_used')
mblighbe630eb2008-08-01 16:41:48 +0000421
422 self.parser = optparse.OptionParser(self._get_usage())
423 self.parser.add_option('-g', '--debug',
424 help='Print debugging information',
425 action='store_true', default=False)
426 self.parser.add_option('--kill-on-failure',
427 help='Stop at the first failure',
428 action='store_true', default=False)
429 self.parser.add_option('--parse',
mbligh47dc4d22009-02-12 21:48:34 +0000430 help='Print the output using | '
mblighbe630eb2008-08-01 16:41:48 +0000431 'separated key=value fields',
432 action='store_true', default=False)
mbligh47dc4d22009-02-12 21:48:34 +0000433 self.parser.add_option('--parse-delim',
434 help='Delimiter to use to separate the '
435 'key=value fields', default='|')
Dan Shi25e1fd42014-12-19 14:36:42 -0800436 self.parser.add_option('--no-confirmation',
Anh Le004b0ab2020-11-04 21:20:45 +0000437 help=('Skip all confirmation in when function '
438 'require_confirmation is called.'),
439 action='store_true', default=False)
mblighbe630eb2008-08-01 16:41:48 +0000440 self.parser.add_option('-v', '--verbose',
441 action='store_true', default=False)
442 self.parser.add_option('-w', '--web',
443 help='Specify the autotest server '
444 'to talk to',
445 action='store', type='string',
446 dest='web_server', default=None)
Ningning Xia5aaca4a2018-05-16 12:18:31 -0700447 self.parser.add_option('--log-level',
448 help=('Set the logging level. Must be one of %s.'
449 ' Default to ERROR' %
450 LOGGING_LEVEL_MAP.keys()),
451 choices=LOGGING_LEVEL_MAP.keys(),
452 default='ERROR',
453 dest='log_level')
mblighbe630eb2008-08-01 16:41:48 +0000454
mblighbe630eb2008-08-01 16:41:48 +0000455
Allen Li8a5458b2020-03-30 18:05:49 -0700456 def add_skylab_options(self, enforce_skylab=True):
457 """Add options for reading and writing skylab inventory repository.
458
459 The enforce_skylab parameter does nothing and is kept for compatibility.
460 """
Ningning Xia84190b82018-04-16 15:01:40 -0700461 self.allow_skylab = True
Allen Li8a5458b2020-03-30 18:05:49 -0700462 self.enforce_skylab = True
Ningning Xia9df3d152018-05-23 17:15:14 -0700463
Allen Li75e3d342018-08-06 19:04:31 +0000464 self.parser.add_option('--skylab',
Allen Li8a5458b2020-03-30 18:05:49 -0700465 help='Deprecated',
466 action='store_const', dest='skylab',
467 const=True)
Ningning Xia84190b82018-04-16 15:01:40 -0700468 self.parser.add_option('--env',
Allen Li75e3d342018-08-06 19:04:31 +0000469 help=('Environment ("prod" or "staging") of the '
470 'machine. Default to "prod". %s' %
Ningning Xia3748b5f2018-05-21 16:33:08 -0700471 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
Ningning Xia84190b82018-04-16 15:01:40 -0700472 dest='environment',
Ningning Xiae8714052018-04-30 18:58:54 -0700473 default='prod')
Ningning Xia84190b82018-04-16 15:01:40 -0700474 self.parser.add_option('--inventory-repo-dir',
Ningning Xiaee3e3a92018-05-22 17:38:51 -0700475 help=('The path of directory to clone skylab '
476 'inventory repo into. It can be an empty '
477 'folder or an existing clean checkout of '
Allen Li75e3d342018-08-06 19:04:31 +0000478 'infra_internal/skylab_inventory. '
Ningning Xia84190b82018-04-16 15:01:40 -0700479 'If not provided, a temporary dir will be '
Ningning Xia3748b5f2018-05-21 16:33:08 -0700480 'created and used as the repo dir. %s' %
481 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
Ningning Xia84190b82018-04-16 15:01:40 -0700482 dest='inventory_repo_dir')
483 self.parser.add_option('--keep-repo-dir',
Allen Li75e3d342018-08-06 19:04:31 +0000484 help=('Keep the inventory-repo-dir after the '
485 'action completes, otherwise the dir will '
486 'be cleaned up. %s' %
Ningning Xia3748b5f2018-05-21 16:33:08 -0700487 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
Ningning Xia84190b82018-04-16 15:01:40 -0700488 action='store_true',
489 dest='keep_repo_dir')
490 self.parser.add_option('--draft',
Allen Li75e3d342018-08-06 19:04:31 +0000491 help=('Upload a change CL as a draft. %s' %
Ningning Xia3748b5f2018-05-21 16:33:08 -0700492 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
Ningning Xia84190b82018-04-16 15:01:40 -0700493 action='store_true',
494 dest='draft',
495 default=False)
496 self.parser.add_option('--dryrun',
Ningning Xia3748b5f2018-05-21 16:33:08 -0700497 help=('Execute the action as a dryrun. %s' %
498 skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
Ningning Xia84190b82018-04-16 15:01:40 -0700499 action='store_true',
500 dest='dryrun',
501 default=False)
Ningning Xiaa043aad2018-04-23 15:07:09 -0700502 self.parser.add_option('--submit',
Allen Li75e3d342018-08-06 19:04:31 +0000503 help=('Submit a change CL directly without '
504 'reviewing and submitting it in Gerrit. %s'
505 % skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
Ningning Xiaa043aad2018-04-23 15:07:09 -0700506 action='store_true',
507 dest='submit',
508 default=False)
Ningning Xia84190b82018-04-16 15:01:40 -0700509
510
mblighbe630eb2008-08-01 16:41:48 +0000511 def _get_usage(self):
512 return "atest %s %s [options] %s" % (self.msg_topic.lower(),
513 self.usage_action,
514 self.msg_items)
515
516
mbligh5a496082009-08-03 16:44:54 +0000517 def backward_compatibility(self, action, argv):
Dan Shi3963caa2014-11-26 12:51:25 -0800518 """To be overidden by subclass if their syntax changed.
519
520 @param action: Name of the action.
521 @param argv: A list of arguments.
522 """
mbligh5a496082009-08-03 16:44:54 +0000523 return action
524
525
Ningning Xia84190b82018-04-16 15:01:40 -0700526 def parse_skylab_options(self, options):
527 """Parse skylab related options.
528
529 @param: options: Option values parsed by the parser.
530 """
Allen Li8a5458b2020-03-30 18:05:49 -0700531 self.skylab = True
Ningning Xia84190b82018-04-16 15:01:40 -0700532
Ningning Xia9c188b92018-04-27 15:34:23 -0700533 # TODO(nxia): crbug.com/837831 Add skylab_inventory to
534 # autotest-server-deps ebuilds to remove the ImportError check.
Ningning Xiaa7ba0c22018-04-30 11:02:30 -0700535 if not skylab_inventory_imported:
Ningning Xia9c188b92018-04-27 15:34:23 -0700536 raise skylab_utils.SkylabInventoryNotImported(
537 "Please try to run utils/build_externals.py.")
538
Ningning Xiaa043aad2018-04-23 15:07:09 -0700539 self.draft = options.draft
Ningning Xia84190b82018-04-16 15:01:40 -0700540
Ningning Xiaa043aad2018-04-23 15:07:09 -0700541 self.dryrun = options.dryrun
542 if self.dryrun:
543 print('This is a dryrun. NO CL will be uploaded.\n')
Ningning Xia84190b82018-04-16 15:01:40 -0700544
Ningning Xiaa043aad2018-04-23 15:07:09 -0700545 self.submit = options.submit
546 if self.submit and (self.dryrun or self.draft):
547 self.invalid_syntax('Can not set --dryrun or --draft when '
548 '--submit is set.')
549
550 # The change number of the inventory change CL.
551 self.change_number = None
552
553 self.environment = options.environment
554 translation_utils.validate_environment(self.environment)
555
556 self.keep_repo_dir = options.keep_repo_dir
557 self.inventory_repo_dir = options.inventory_repo_dir
558 if self.inventory_repo_dir is None:
559 self.temp_dir = autotemp.tempdir(
560 prefix='inventory_repo',
561 auto_clean=not self.keep_repo_dir)
Allen Li75e3d342018-08-06 19:04:31 +0000562
Ningning Xiaa043aad2018-04-23 15:07:09 -0700563 self.inventory_repo_dir = self.temp_dir.name
Allen Li75e3d342018-08-06 19:04:31 +0000564 if self.debug or self.keep_repo_dir:
565 print('The inventory_repo_dir is created at %s' %
Ningning Xiaa043aad2018-04-23 15:07:09 -0700566 self.inventory_repo_dir)
Ningning Xia84190b82018-04-16 15:01:40 -0700567
568
mbligh9deeefa2009-05-01 23:11:08 +0000569 def parse(self, parse_info=[], req_items=None):
Dan Shi3963caa2014-11-26 12:51:25 -0800570 """Parse command arguments.
mblighbe630eb2008-08-01 16:41:48 +0000571
Dan Shi3963caa2014-11-26 12:51:25 -0800572 parse_info is a list of item_parse_info objects.
mbligh9deeefa2009-05-01 23:11:08 +0000573 There should only be one use_leftover set to True in the list.
mblighbe630eb2008-08-01 16:41:48 +0000574
Dan Shi3963caa2014-11-26 12:51:25 -0800575 Also check that the req_items is not empty after parsing.
576
577 @param parse_info: A list of item_parse_info objects.
578 @param req_items: A list of required items.
579 """
mbligh9deeefa2009-05-01 23:11:08 +0000580 (options, leftover) = self.parse_global()
mblighbe630eb2008-08-01 16:41:48 +0000581
mbligh9deeefa2009-05-01 23:11:08 +0000582 all_parse_info = parse_info[:]
583 all_parse_info.append(self.topic_parse_info)
584
585 try:
586 for item_parse_info in all_parse_info:
587 values, leftover = item_parse_info.get_values(options,
588 leftover)
589 setattr(self, item_parse_info.attribute_name, values)
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700590 except CliError as s:
mbligh9deeefa2009-05-01 23:11:08 +0000591 self.invalid_syntax(s)
mblighbe630eb2008-08-01 16:41:48 +0000592
593 if (req_items and not getattr(self, req_items, None)):
594 self.invalid_syntax('%s %s requires at least one %s' %
595 (self.msg_topic,
596 self.usage_action,
597 self.msg_topic))
598
Ningning Xia84190b82018-04-16 15:01:40 -0700599 if self.allow_skylab:
600 self.parse_skylab_options(options)
601
Ningning Xia5aaca4a2018-05-16 12:18:31 -0700602 logging.getLogger().setLevel(LOGGING_LEVEL_MAP[options.log_level])
603
mblighbe630eb2008-08-01 16:41:48 +0000604 return (options, leftover)
605
606
mbligh9deeefa2009-05-01 23:11:08 +0000607 def parse_global(self):
608 """Parse the global arguments.
mblighbe630eb2008-08-01 16:41:48 +0000609
610 It consumes what the common object needs to know, and
611 let the children look at all the options. We could
612 remove the options that we have used, but there is no
613 harm in leaving them, and the children may need them
614 in the future.
615
616 Must be called from its children parse()"""
617 (options, leftover) = self.parser.parse_args()
618 # Handle our own options setup in __init__()
619 self.debug = options.debug
620 self.kill_on_failure = options.kill_on_failure
621
622 if options.parse:
623 suffix = '_parse'
624 else:
625 suffix = '_std'
626 for func in ['print_fields', 'print_table',
mblighdf75f8b2008-11-18 19:07:42 +0000627 'print_by_ids', 'print_list']:
mblighbe630eb2008-08-01 16:41:48 +0000628 setattr(self, func, getattr(self, func + suffix))
629
mbligh47dc4d22009-02-12 21:48:34 +0000630 self.parse_delim = options.parse_delim
631
mblighbe630eb2008-08-01 16:41:48 +0000632 self.verbose = options.verbose
Dan Shi25e1fd42014-12-19 14:36:42 -0800633 self.no_confirmation = options.no_confirmation
mblighbe630eb2008-08-01 16:41:48 +0000634 self.web_server = options.web_server
mblighb68405d2010-03-11 18:32:39 +0000635 try:
636 self.afe = rpc.afe_comm(self.web_server)
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700637 except rpc.AuthError as s:
mblighb68405d2010-03-11 18:32:39 +0000638 self.failure(str(s), fatal=True)
mblighbe630eb2008-08-01 16:41:48 +0000639
640 return (options, leftover)
641
642
643 def check_and_create_items(self, op_get, op_create,
644 items, **data_create):
Dan Shi3963caa2014-11-26 12:51:25 -0800645 """Create the items if they don't exist already.
646
647 @param op_get: Name of `get` RPC.
648 @param op_create: Name of `create` RPC.
649 @param items: Actionable items specified in CLI command, e.g., hostname,
650 to be passed to each RPC.
651 @param data_create: Data to be passed to `create` RPC.
652 """
mblighbe630eb2008-08-01 16:41:48 +0000653 for item in items:
654 ret = self.execute_rpc(op_get, name=item)
655
656 if len(ret) == 0:
657 try:
658 data_create['name'] = item
659 self.execute_rpc(op_create, **data_create)
660 except CliError:
661 continue
662
663
664 def execute_rpc(self, op, item='', **data):
Dan Shi3963caa2014-11-26 12:51:25 -0800665 """Execute RPC.
666
667 @param op: Name of the RPC.
668 @param item: Actionable item specified in CLI command.
669 @param data: Data to be passed to RPC.
670 """
mblighbe630eb2008-08-01 16:41:48 +0000671 retry = 2
672 while retry:
673 try:
674 return self.afe.run(op, **data)
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700675 except urllib2.URLError as err:
mbligh11efd232008-11-27 00:20:46 +0000676 if hasattr(err, 'reason'):
677 if 'timed out' not in err.reason:
678 self.invalid_syntax('Invalid server name %s: %s' %
679 (self.afe.web_server, err))
680 if hasattr(err, 'code'):
showard53d91e22010-01-15 00:18:27 +0000681 error_parts = [str(err)]
682 if self.debug:
683 error_parts.append(err.read()) # read the response body
684 self.failure('\n\n'.join(error_parts), item=item,
mbligh11efd232008-11-27 00:20:46 +0000685 what_failed=("Error received from web server"))
686 raise CliError("Error from web server")
mblighbe630eb2008-08-01 16:41:48 +0000687 if self.debug:
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700688 print('retrying: %r %d' % (data, retry))
mblighbe630eb2008-08-01 16:41:48 +0000689 retry -= 1
690 if retry == 0:
691 if item:
692 myerr = '%s timed out for %s' % (op, item)
693 else:
694 myerr = '%s timed out' % op
695 self.failure(myerr, item=item,
696 what_failed=("Timed-out contacting "
697 "the Autotest server"))
698 raise CliError("Timed-out contacting the Autotest server")
mblighcd26d042010-05-03 18:58:24 +0000699 except mock.CheckPlaybackError:
700 raise
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700701 except Exception as full_error:
mblighbe630eb2008-08-01 16:41:48 +0000702 # There are various exceptions throwns by JSON,
703 # urllib & httplib, so catch them all.
704 self.failure(full_error, item=item,
705 what_failed='Operation %s failed' % op)
706 raise CliError(str(full_error))
707
708
709 # There is no output() method in the atest object (yet?)
710 # but here are some helper functions to be used by its
711 # children
712 def print_wrapped(self, msg, values):
Dan Shi3963caa2014-11-26 12:51:25 -0800713 """Print given message and values in wrapped lines unless
714 AUTOTEST_CLI_NO_WRAP is specified in environment variables.
715
716 @param msg: Message to print.
717 @param values: A list of values to print.
718 """
mblighbe630eb2008-08-01 16:41:48 +0000719 if len(values) == 0:
720 return
721 elif len(values) == 1:
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700722 print(msg + ': ')
mblighbe630eb2008-08-01 16:41:48 +0000723 elif len(values) > 1:
724 if msg.endswith('s'):
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700725 print(msg + ': ')
mblighbe630eb2008-08-01 16:41:48 +0000726 else:
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700727 print(msg + 's: ')
mblighbe630eb2008-08-01 16:41:48 +0000728
729 values.sort()
mbligh552d2402009-09-18 19:35:23 +0000730
731 if 'AUTOTEST_CLI_NO_WRAP' in os.environ:
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700732 print('\n'.join(values))
mbligh552d2402009-09-18 19:35:23 +0000733 return
734
mblighbe630eb2008-08-01 16:41:48 +0000735 twrap = textwrap.TextWrapper(initial_indent='\t',
736 subsequent_indent='\t')
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700737 print(twrap.fill(', '.join(values)))
mblighbe630eb2008-08-01 16:41:48 +0000738
739
740 def __conv_value(self, type, value):
741 return KEYS_CONVERT.get(type, str)(value)
742
743
Anh Le004b0ab2020-11-04 21:20:45 +0000744 def print_fields_std(self, items, keys, title=None):
745 """Print the keys in each item, one on each line.
746
747 @param items: Items to print.
748 @param keys: Name of the keys to look up each item in items.
749 @param title: Title of the output, default to None.
750 """
751 if not items:
752 return
753 if title:
754 print(title)
755 for item in items:
756 for key in keys:
757 print('%s: %s' % (KEYS_TO_NAMES_EN[key],
758 self.__conv_value(key,
759 _get_item_key(item, key))))
760
761
762 def print_fields_parse(self, items, keys, title=None):
763 """Print the keys in each item as comma separated name=value
764
765 @param items: Items to print.
766 @param keys: Name of the keys to look up each item in items.
767 @param title: Title of the output, default to None.
768 """
769 for item in items:
770 values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
771 self.__conv_value(key,
772 _get_item_key(item, key)))
773 for key in keys
774 if self.__conv_value(key,
775 _get_item_key(item, key)) != '']
776 print(self.parse_delim.join(values))
777
778
mblighbe630eb2008-08-01 16:41:48 +0000779 def __find_justified_fmt(self, items, keys):
Dan Shi3963caa2014-11-26 12:51:25 -0800780 """Find the max length for each field.
781
782 @param items: Items to lookup for.
783 @param keys: Name of the keys to look up each item in items.
784 """
mblighbe630eb2008-08-01 16:41:48 +0000785 lens = {}
786 # Don't justify the last field, otherwise we have blank
787 # lines when the max is overlaps but the current values
788 # are smaller
789 if not items:
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700790 print("No results")
mblighbe630eb2008-08-01 16:41:48 +0000791 return
792 for key in keys[:-1]:
793 lens[key] = max(len(self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000794 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000795 for item in items)
796 lens[key] = max(lens[key], len(KEYS_TO_NAMES_EN[key]))
797 lens[keys[-1]] = 0
798
799 return ' '.join(["%%-%ds" % lens[key] for key in keys])
800
801
Simran Basi0739d682015-02-25 16:22:56 -0800802 def print_dict(self, items, title=None, line_before=False):
803 """Print a dictionary.
804
805 @param items: Dictionary to print.
806 @param title: Title of the output, default to None.
807 @param line_before: True to print an empty line before the output,
808 default to False.
809 """
810 if not items:
811 return
812 if line_before:
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700813 print()
814 print(title)
Simran Basi0739d682015-02-25 16:22:56 -0800815 for key, value in items.items():
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700816 print('%s : %s' % (key, value))
Simran Basi0739d682015-02-25 16:22:56 -0800817
818
mbligh838c7472009-05-13 20:56:50 +0000819 def print_table_std(self, items, keys_header, sublist_keys=()):
Dan Shi3963caa2014-11-26 12:51:25 -0800820 """Print a mix of header and lists in a user readable format.
821
822 The headers are justified, the sublist_keys are wrapped.
823
824 @param items: Items to print.
825 @param keys_header: Header of the keys, use to look up in items.
826 @param sublist_keys: Keys for sublist in each item.
827 """
mblighbe630eb2008-08-01 16:41:48 +0000828 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000829 return
830 fmt = self.__find_justified_fmt(items, keys_header)
831 header = tuple(KEYS_TO_NAMES_EN[key] for key in keys_header)
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700832 print(fmt % header)
mblighbe630eb2008-08-01 16:41:48 +0000833 for item in items:
showard088b8262009-07-01 22:12:35 +0000834 values = tuple(self.__conv_value(key,
835 _get_item_key(item, key))
mblighbe630eb2008-08-01 16:41:48 +0000836 for key in keys_header)
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700837 print(fmt % values)
mbligh838c7472009-05-13 20:56:50 +0000838 if sublist_keys:
mblighbe630eb2008-08-01 16:41:48 +0000839 for key in sublist_keys:
840 self.print_wrapped(KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000841 _get_item_key(item, key))
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700842 print('\n')
mblighbe630eb2008-08-01 16:41:48 +0000843
844
Anh Le004b0ab2020-11-04 21:20:45 +0000845 def print_table_parse(self, items, keys_header, sublist_keys=()):
846 """Print a mix of header and lists in a user readable format.
847
848 @param items: Items to print.
849 @param keys_header: Header of the keys, use to look up in items.
850 @param sublist_keys: Keys for sublist in each item.
851 """
852 for item in items:
853 values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
854 self.__conv_value(key, _get_item_key(item, key)))
855 for key in keys_header
856 if self.__conv_value(key,
857 _get_item_key(item, key)) != '']
858
859 if sublist_keys:
860 [values.append('%s=%s'% (KEYS_TO_NAMES_EN[key],
861 ','.join(_get_item_key(item, key))))
862 for key in sublist_keys
863 if len(_get_item_key(item, key))]
864
865 print(self.parse_delim.join(values))
866
867
868 def print_by_ids_std(self, items, title=None, line_before=False):
869 """Prints ID & names of items in a user readable form.
870
871 @param items: Items to print.
872 @param title: Title of the output, default to None.
873 @param line_before: True to print an empty line before the output,
874 default to False.
875 """
876 if not items:
877 return
878 if line_before:
879 print()
880 if title:
881 print(title + ':')
882 self.print_table_std(items, keys_header=['id', 'name'])
883
884
885 def print_by_ids_parse(self, items, title=None, line_before=False):
886 """Prints ID & names of items in a parseable format.
887
888 @param items: Items to print.
889 @param title: Title of the output, default to None.
890 @param line_before: True to print an empty line before the output,
891 default to False.
892 """
893 if not items:
894 return
895 if line_before:
896 print()
897 if title:
898 print(title + '='),
899 values = []
900 for item in items:
901 values += ['%s=%s' % (KEYS_TO_NAMES_EN[key],
902 self.__conv_value(key,
903 _get_item_key(item, key)))
904 for key in ['id', 'name']
905 if self.__conv_value(key,
906 _get_item_key(item, key)) != '']
907 print(self.parse_delim.join(values))
908
909
910 def print_list_std(self, items, key):
911 """Print a wrapped list of results
912
913 @param items: Items to to lookup for given key, could be a nested
914 dictionary.
915 @param key: Name of the key to look up for value.
916 """
917 if not items:
918 return
919 print(' '.join(_get_item_key(item, key) for item in items))
920
921
922 def print_list_parse(self, items, key):
923 """Print a wrapped list of results.
924
925 @param items: Items to to lookup for given key, could be a nested
926 dictionary.
927 @param key: Name of the key to look up for value.
928 """
929 if not items:
930 return
931 print('%s=%s' % (KEYS_TO_NAMES_EN[key],
932 ','.join(_get_item_key(item, key) for item in items)))
933
934
Dan Shi25e1fd42014-12-19 14:36:42 -0800935 @staticmethod
936 def prompt_confirmation(message=None):
937 """Prompt a question for user to confirm the action before proceeding.
938
939 @param message: A detailed message to explain possible impact of the
940 action.
941
942 @return: True to proceed or False to abort.
943 """
944 if message:
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700945 print(message)
Dan Shi25e1fd42014-12-19 14:36:42 -0800946 sys.stdout.write('Continue? [y/N] ')
947 read = raw_input().lower()
948 if read == 'y':
949 return True
950 else:
Gregory Nisbeta2c4c0e2020-07-14 16:17:46 -0700951 print('User did not confirm. Aborting...')
Dan Shi25e1fd42014-12-19 14:36:42 -0800952 return False
Anh Le004b0ab2020-11-04 21:20:45 +0000953
954
955 @staticmethod
956 def require_confirmation(message=None):
957 """Decorator to prompt a question for user to confirm action before
958 proceeding.
959
960 If user chooses not to proceed, do not call the function.
961
962 @param message: A detailed message to explain possible impact of the
963 action.
964
965 @return: A decorator wrapper for calling the actual function.
966 """
967 def deco_require_confirmation(func):
968 """Wrapper for the decorator.
969
970 @param func: Function to be called.
971
972 @return: the actual decorator to call the function.
973 """
974 def func_require_confirmation(*args, **kwargs):
975 """Decorator to prompt a question for user to confirm.
976
977 @param message: A detailed message to explain possible impact of
978 the action.
979 """
980 if (args[0].no_confirmation or
981 atest.prompt_confirmation(message)):
982 func(*args, **kwargs)
983
984 return func_require_confirmation
985 return deco_require_confirmation