1*9c5db199SXin Li# 2*9c5db199SXin Li# Copyright 2008 Google Inc. All Rights Reserved. 3*9c5db199SXin Li# 4*9c5db199SXin Li""" 5*9c5db199SXin LiThis module contains the generic CLI object 6*9c5db199SXin Li 7*9c5db199SXin LiHigh Level Design: 8*9c5db199SXin Li 9*9c5db199SXin LiThe atest class contains attributes & method generic to all the CLI 10*9c5db199SXin Lioperations. 11*9c5db199SXin Li 12*9c5db199SXin LiThe class inheritance is shown here using the command 13*9c5db199SXin Li'atest server list ...' as an example: 14*9c5db199SXin Li 15*9c5db199SXin Liatest <-- server <-- server_list 16*9c5db199SXin Li 17*9c5db199SXin LiNote: The site_<topic>.py and its classes are only needed if you need 18*9c5db199SXin Lito override the common <topic>.py methods with your site specific ones. 19*9c5db199SXin Li 20*9c5db199SXin Li 21*9c5db199SXin LiHigh Level Algorithm: 22*9c5db199SXin Li 23*9c5db199SXin Li1. atest figures out the topic and action from the 2 first arguments 24*9c5db199SXin Li on the command line and imports the <topic> (or site_<topic>) 25*9c5db199SXin Li module. 26*9c5db199SXin Li 27*9c5db199SXin Li1. Init 28*9c5db199SXin Li The main atest module creates a <topic>_<action> object. The 29*9c5db199SXin Li __init__() function is used to setup the parser options, if this 30*9c5db199SXin Li <action> has some specific options to add to its <topic>. 31*9c5db199SXin Li 32*9c5db199SXin Li If it exists, the child __init__() method must call its parent 33*9c5db199SXin Li class __init__() before adding its own parser arguments. 34*9c5db199SXin Li 35*9c5db199SXin Li2. Parsing 36*9c5db199SXin Li If the child wants to validate the parsing (e.g. make sure that 37*9c5db199SXin Li there are hosts in the arguments), or if it wants to check the 38*9c5db199SXin Li options it added in its __init__(), it should implement a parse() 39*9c5db199SXin Li method. 40*9c5db199SXin Li 41*9c5db199SXin Li The child parser must call its parent parser and gets back the 42*9c5db199SXin Li options dictionary and the rest of the command line arguments 43*9c5db199SXin Li (leftover). Each level gets to see all the options, but the 44*9c5db199SXin Li leftovers can be deleted as they can be consumed by only one 45*9c5db199SXin Li object. 46*9c5db199SXin Li 47*9c5db199SXin Li3. Execution 48*9c5db199SXin Li This execute() method is specific to the child and should use the 49*9c5db199SXin Li self.execute_rpc() to send commands to the Autotest Front-End. It 50*9c5db199SXin Li should return results. 51*9c5db199SXin Li 52*9c5db199SXin Li4. Output 53*9c5db199SXin Li The child output() method is called with the execute() resutls as a 54*9c5db199SXin Li parameter. This is child-specific, but should leverage the 55*9c5db199SXin Li atest.print_*() methods. 56*9c5db199SXin Li""" 57*9c5db199SXin Li 58*9c5db199SXin Lifrom __future__ import print_function 59*9c5db199SXin Li 60*9c5db199SXin Liimport logging 61*9c5db199SXin Liimport optparse 62*9c5db199SXin Liimport os 63*9c5db199SXin Liimport re 64*9c5db199SXin Liimport sys 65*9c5db199SXin Liimport textwrap 66*9c5db199SXin Liimport traceback 67*9c5db199SXin Liimport urllib2 68*9c5db199SXin Li 69*9c5db199SXin Liimport common 70*9c5db199SXin Li 71*9c5db199SXin Lifrom autotest_lib.cli import rpc 72*9c5db199SXin Lifrom autotest_lib.cli import skylab_utils 73*9c5db199SXin Lifrom autotest_lib.client.common_lib.test_utils import mock 74*9c5db199SXin Lifrom autotest_lib.client.common_lib import autotemp 75*9c5db199SXin Li 76*9c5db199SXin Liskylab_inventory_imported = False 77*9c5db199SXin Litry: 78*9c5db199SXin Li from skylab_inventory import translation_utils 79*9c5db199SXin Li skylab_inventory_imported = True 80*9c5db199SXin Liexcept ImportError: 81*9c5db199SXin Li pass 82*9c5db199SXin Li 83*9c5db199SXin Li 84*9c5db199SXin Li# Maps the AFE keys to printable names. 85*9c5db199SXin LiKEYS_TO_NAMES_EN = {'hostname': 'Host', 86*9c5db199SXin Li 'platform': 'Platform', 87*9c5db199SXin Li 'status': 'Status', 88*9c5db199SXin Li 'locked': 'Locked', 89*9c5db199SXin Li 'locked_by': 'Locked by', 90*9c5db199SXin Li 'lock_time': 'Locked time', 91*9c5db199SXin Li 'lock_reason': 'Lock Reason', 92*9c5db199SXin Li 'labels': 'Labels', 93*9c5db199SXin Li 'description': 'Description', 94*9c5db199SXin Li 'hosts': 'Hosts', 95*9c5db199SXin Li 'users': 'Users', 96*9c5db199SXin Li 'id': 'Id', 97*9c5db199SXin Li 'name': 'Name', 98*9c5db199SXin Li 'invalid': 'Valid', 99*9c5db199SXin Li 'login': 'Login', 100*9c5db199SXin Li 'access_level': 'Access Level', 101*9c5db199SXin Li 'job_id': 'Job Id', 102*9c5db199SXin Li 'job_owner': 'Job Owner', 103*9c5db199SXin Li 'job_name': 'Job Name', 104*9c5db199SXin Li 'test_type': 'Test Type', 105*9c5db199SXin Li 'test_class': 'Test Class', 106*9c5db199SXin Li 'path': 'Path', 107*9c5db199SXin Li 'owner': 'Owner', 108*9c5db199SXin Li 'status_counts': 'Status Counts', 109*9c5db199SXin Li 'hosts_status': 'Host Status', 110*9c5db199SXin Li 'hosts_selected_status': 'Hosts filtered by Status', 111*9c5db199SXin Li 'priority': 'Priority', 112*9c5db199SXin Li 'control_type': 'Control Type', 113*9c5db199SXin Li 'created_on': 'Created On', 114*9c5db199SXin Li 'control_file': 'Control File', 115*9c5db199SXin Li 'only_if_needed': 'Use only if needed', 116*9c5db199SXin Li 'protection': 'Protection', 117*9c5db199SXin Li 'run_verify': 'Run verify', 118*9c5db199SXin Li 'reboot_before': 'Pre-job reboot', 119*9c5db199SXin Li 'reboot_after': 'Post-job reboot', 120*9c5db199SXin Li 'experimental': 'Experimental', 121*9c5db199SXin Li 'synch_count': 'Sync Count', 122*9c5db199SXin Li 'max_number_of_machines': 'Max. hosts to use', 123*9c5db199SXin Li 'parse_failed_repair': 'Include failed repair results', 124*9c5db199SXin Li 'shard': 'Shard', 125*9c5db199SXin Li } 126*9c5db199SXin Li 127*9c5db199SXin Li# In the failure, tag that will replace the item. 128*9c5db199SXin LiFAIL_TAG = '<XYZ>' 129*9c5db199SXin Li 130*9c5db199SXin Li# Global socket timeout: uploading kernels can take much, 131*9c5db199SXin Li# much longer than the default 132*9c5db199SXin LiUPLOAD_SOCKET_TIMEOUT = 60*30 133*9c5db199SXin Li 134*9c5db199SXin LiLOGGING_LEVEL_MAP = { 135*9c5db199SXin Li 'CRITICAL': logging.CRITICAL, 136*9c5db199SXin Li 'ERROR': logging.ERROR, 137*9c5db199SXin Li 'WARNING': logging.WARNING, 138*9c5db199SXin Li 'INFO': logging.INFO, 139*9c5db199SXin Li 'DEBUG': logging.DEBUG, 140*9c5db199SXin Li} 141*9c5db199SXin Li 142*9c5db199SXin Li 143*9c5db199SXin Li# Convertion functions to be called for printing, 144*9c5db199SXin Li# e.g. to print True/False for booleans. 145*9c5db199SXin Lidef __convert_platform(field): 146*9c5db199SXin Li if field is None: 147*9c5db199SXin Li return "" 148*9c5db199SXin Li elif isinstance(field, int): 149*9c5db199SXin Li # Can be 0/1 for False/True 150*9c5db199SXin Li return str(bool(field)) 151*9c5db199SXin Li else: 152*9c5db199SXin Li # Can be a platform name 153*9c5db199SXin Li return field 154*9c5db199SXin Li 155*9c5db199SXin Li 156*9c5db199SXin Lidef _int_2_bool_string(value): 157*9c5db199SXin Li return str(bool(value)) 158*9c5db199SXin Li 159*9c5db199SXin LiKEYS_CONVERT = {'locked': _int_2_bool_string, 160*9c5db199SXin Li 'invalid': lambda flag: str(bool(not flag)), 161*9c5db199SXin Li 'only_if_needed': _int_2_bool_string, 162*9c5db199SXin Li 'platform': __convert_platform, 163*9c5db199SXin Li 'labels': lambda labels: ', '.join(labels), 164*9c5db199SXin Li 'shards': lambda shard: shard.hostname if shard else ''} 165*9c5db199SXin Li 166*9c5db199SXin Li 167*9c5db199SXin Lidef _get_item_key(item, key): 168*9c5db199SXin Li """Allow for lookups in nested dictionaries using '.'s within a key.""" 169*9c5db199SXin Li if key in item: 170*9c5db199SXin Li return item[key] 171*9c5db199SXin Li nested_item = item 172*9c5db199SXin Li for subkey in key.split('.'): 173*9c5db199SXin Li if not subkey: 174*9c5db199SXin Li raise ValueError('empty subkey in %r' % key) 175*9c5db199SXin Li try: 176*9c5db199SXin Li nested_item = nested_item[subkey] 177*9c5db199SXin Li except KeyError as e: 178*9c5db199SXin Li raise KeyError('%r - looking up key %r in %r' % 179*9c5db199SXin Li (e, key, nested_item)) 180*9c5db199SXin Li else: 181*9c5db199SXin Li return nested_item 182*9c5db199SXin Li 183*9c5db199SXin Li 184*9c5db199SXin Liclass CliError(Exception): 185*9c5db199SXin Li """Error raised by cli calls. 186*9c5db199SXin Li """ 187*9c5db199SXin Li pass 188*9c5db199SXin Li 189*9c5db199SXin Li 190*9c5db199SXin Liclass item_parse_info(object): 191*9c5db199SXin Li """Object keeping track of the parsing options. 192*9c5db199SXin Li """ 193*9c5db199SXin Li 194*9c5db199SXin Li def __init__(self, attribute_name, inline_option='', 195*9c5db199SXin Li filename_option='', use_leftover=False): 196*9c5db199SXin Li """Object keeping track of the parsing options that will 197*9c5db199SXin Li make up the content of the atest attribute: 198*9c5db199SXin Li attribute_name: the atest attribute name to populate (label) 199*9c5db199SXin Li inline_option: the option containing the items (--label) 200*9c5db199SXin Li filename_option: the option containing the filename (--blist) 201*9c5db199SXin Li use_leftover: whether to add the leftover arguments or not.""" 202*9c5db199SXin Li self.attribute_name = attribute_name 203*9c5db199SXin Li self.filename_option = filename_option 204*9c5db199SXin Li self.inline_option = inline_option 205*9c5db199SXin Li self.use_leftover = use_leftover 206*9c5db199SXin Li 207*9c5db199SXin Li 208*9c5db199SXin Li def get_values(self, options, leftover=[]): 209*9c5db199SXin Li """Returns the value for that attribute by accumualting all 210*9c5db199SXin Li the values found through the inline option, the parsing of the 211*9c5db199SXin Li file and the leftover""" 212*9c5db199SXin Li 213*9c5db199SXin Li def __get_items(input, split_spaces=True): 214*9c5db199SXin Li """Splits a string of comma separated items. Escaped commas will not 215*9c5db199SXin Li be split. I.e. Splitting 'a, b\,c, d' will yield ['a', 'b,c', 'd']. 216*9c5db199SXin Li If split_spaces is set to False spaces will not be split. I.e. 217*9c5db199SXin Li Splitting 'a b, c\,d, e' will yield ['a b', 'c,d', 'e']""" 218*9c5db199SXin Li 219*9c5db199SXin Li # Replace escaped slashes with null characters so we don't misparse 220*9c5db199SXin Li # proceeding commas. 221*9c5db199SXin Li input = input.replace(r'\\', '\0') 222*9c5db199SXin Li 223*9c5db199SXin Li # Split on commas which are not preceded by a slash. 224*9c5db199SXin Li if not split_spaces: 225*9c5db199SXin Li split = re.split(r'(?<!\\),', input) 226*9c5db199SXin Li else: 227*9c5db199SXin Li split = re.split(r'(?<!\\),|\s', input) 228*9c5db199SXin Li 229*9c5db199SXin Li # Convert null characters to single slashes and escaped commas to 230*9c5db199SXin Li # just plain commas. 231*9c5db199SXin Li return (item.strip().replace('\0', '\\').replace(r'\,', ',') for 232*9c5db199SXin Li item in split if item.strip()) 233*9c5db199SXin Li 234*9c5db199SXin Li if self.use_leftover: 235*9c5db199SXin Li add_on = leftover 236*9c5db199SXin Li leftover = [] 237*9c5db199SXin Li else: 238*9c5db199SXin Li add_on = [] 239*9c5db199SXin Li 240*9c5db199SXin Li # Start with the add_on 241*9c5db199SXin Li result = set() 242*9c5db199SXin Li for items in add_on: 243*9c5db199SXin Li # Don't split on space here because the add-on 244*9c5db199SXin Li # may have some spaces (like the job name) 245*9c5db199SXin Li result.update(__get_items(items, split_spaces=False)) 246*9c5db199SXin Li 247*9c5db199SXin Li # Process the inline_option, if any 248*9c5db199SXin Li try: 249*9c5db199SXin Li items = getattr(options, self.inline_option) 250*9c5db199SXin Li result.update(__get_items(items)) 251*9c5db199SXin Li except (AttributeError, TypeError): 252*9c5db199SXin Li pass 253*9c5db199SXin Li 254*9c5db199SXin Li # Process the file list, if any and not empty 255*9c5db199SXin Li # The file can contain space and/or comma separated items 256*9c5db199SXin Li try: 257*9c5db199SXin Li flist = getattr(options, self.filename_option) 258*9c5db199SXin Li file_content = [] 259*9c5db199SXin Li for line in open(flist).readlines(): 260*9c5db199SXin Li file_content += __get_items(line) 261*9c5db199SXin Li if len(file_content) == 0: 262*9c5db199SXin Li raise CliError("Empty file %s" % flist) 263*9c5db199SXin Li result.update(file_content) 264*9c5db199SXin Li except (AttributeError, TypeError): 265*9c5db199SXin Li pass 266*9c5db199SXin Li except IOError: 267*9c5db199SXin Li raise CliError("Could not open file %s" % flist) 268*9c5db199SXin Li 269*9c5db199SXin Li return list(result), leftover 270*9c5db199SXin Li 271*9c5db199SXin Li 272*9c5db199SXin Liclass atest(object): 273*9c5db199SXin Li """Common class for generic processing 274*9c5db199SXin Li Should only be instantiated by itself for usage 275*9c5db199SXin Li references, otherwise, the <topic> objects should 276*9c5db199SXin Li be used.""" 277*9c5db199SXin Li msg_topic = '[acl|job|label|shard|test|user|server]' 278*9c5db199SXin Li usage_action = '[action]' 279*9c5db199SXin Li msg_items = '' 280*9c5db199SXin Li 281*9c5db199SXin Li def invalid_arg(self, header, follow_up=''): 282*9c5db199SXin Li """Fail the command with error that command line has invalid argument. 283*9c5db199SXin Li 284*9c5db199SXin Li @param header: Header of the error message. 285*9c5db199SXin Li @param follow_up: Extra error message, default to empty string. 286*9c5db199SXin Li """ 287*9c5db199SXin Li twrap = textwrap.TextWrapper(initial_indent=' ', 288*9c5db199SXin Li subsequent_indent=' ') 289*9c5db199SXin Li rest = twrap.fill(follow_up) 290*9c5db199SXin Li 291*9c5db199SXin Li if self.kill_on_failure: 292*9c5db199SXin Li self.invalid_syntax(header + rest) 293*9c5db199SXin Li else: 294*9c5db199SXin Li print(header + rest, file=sys.stderr) 295*9c5db199SXin Li 296*9c5db199SXin Li 297*9c5db199SXin Li def invalid_syntax(self, msg): 298*9c5db199SXin Li """Fail the command with error that the command line syntax is wrong. 299*9c5db199SXin Li 300*9c5db199SXin Li @param msg: Error message. 301*9c5db199SXin Li """ 302*9c5db199SXin Li print() 303*9c5db199SXin Li print(msg, file=sys.stderr) 304*9c5db199SXin Li print() 305*9c5db199SXin Li print("usage:") 306*9c5db199SXin Li print(self._get_usage()) 307*9c5db199SXin Li print() 308*9c5db199SXin Li sys.exit(1) 309*9c5db199SXin Li 310*9c5db199SXin Li 311*9c5db199SXin Li def generic_error(self, msg): 312*9c5db199SXin Li """Fail the command with a generic error. 313*9c5db199SXin Li 314*9c5db199SXin Li @param msg: Error message. 315*9c5db199SXin Li """ 316*9c5db199SXin Li if self.debug: 317*9c5db199SXin Li traceback.print_exc() 318*9c5db199SXin Li print >> sys.stderr, msg 319*9c5db199SXin Li sys.exit(1) 320*9c5db199SXin Li 321*9c5db199SXin Li 322*9c5db199SXin Li def parse_json_exception(self, full_error): 323*9c5db199SXin Li """Parses the JSON exception to extract the bad 324*9c5db199SXin Li items and returns them 325*9c5db199SXin Li This is very kludgy for the moment, but we would need 326*9c5db199SXin Li to refactor the exceptions sent from the front end 327*9c5db199SXin Li to make this better. 328*9c5db199SXin Li 329*9c5db199SXin Li @param full_error: The complete error message. 330*9c5db199SXin Li """ 331*9c5db199SXin Li errmsg = str(full_error).split('Traceback')[0].rstrip('\n') 332*9c5db199SXin Li parts = errmsg.split(':') 333*9c5db199SXin Li # Kludge: If there are 2 colons the last parts contains 334*9c5db199SXin Li # the items that failed. 335*9c5db199SXin Li if len(parts) != 3: 336*9c5db199SXin Li return [] 337*9c5db199SXin Li return [item.strip() for item in parts[2].split(',') if item.strip()] 338*9c5db199SXin Li 339*9c5db199SXin Li 340*9c5db199SXin Li def failure(self, full_error, item=None, what_failed='', fatal=False): 341*9c5db199SXin Li """If kill_on_failure, print this error and die, 342*9c5db199SXin Li otherwise, queue the error and accumulate all the items 343*9c5db199SXin Li that triggered the same error. 344*9c5db199SXin Li 345*9c5db199SXin Li @param full_error: The complete error message. 346*9c5db199SXin Li @param item: Name of the actionable item, e.g., hostname. 347*9c5db199SXin Li @param what_failed: Name of the failed item. 348*9c5db199SXin Li @param fatal: True to exit the program with failure. 349*9c5db199SXin Li """ 350*9c5db199SXin Li 351*9c5db199SXin Li if self.debug: 352*9c5db199SXin Li errmsg = str(full_error) 353*9c5db199SXin Li else: 354*9c5db199SXin Li errmsg = str(full_error).split('Traceback')[0].rstrip('\n') 355*9c5db199SXin Li 356*9c5db199SXin Li if self.kill_on_failure or fatal: 357*9c5db199SXin Li print >> sys.stderr, "%s\n %s" % (what_failed, errmsg) 358*9c5db199SXin Li sys.exit(1) 359*9c5db199SXin Li 360*9c5db199SXin Li # Build a dictionary with the 'what_failed' as keys. The 361*9c5db199SXin Li # values are dictionaries with the errmsg as keys and a set 362*9c5db199SXin Li # of items as values. 363*9c5db199SXin Li # self.failed = 364*9c5db199SXin Li # {'Operation delete_host_failed': {'AclAccessViolation: 365*9c5db199SXin Li # set('host0', 'host1')}} 366*9c5db199SXin Li # Try to gather all the same error messages together, 367*9c5db199SXin Li # even if they contain the 'item' 368*9c5db199SXin Li if item and item in errmsg: 369*9c5db199SXin Li errmsg = errmsg.replace(item, FAIL_TAG) 370*9c5db199SXin Li if self.failed.has_key(what_failed): 371*9c5db199SXin Li self.failed[what_failed].setdefault(errmsg, set()).add(item) 372*9c5db199SXin Li else: 373*9c5db199SXin Li self.failed[what_failed] = {errmsg: set([item])} 374*9c5db199SXin Li 375*9c5db199SXin Li 376*9c5db199SXin Li def show_all_failures(self): 377*9c5db199SXin Li """Print all failure information. 378*9c5db199SXin Li """ 379*9c5db199SXin Li if not self.failed: 380*9c5db199SXin Li return 0 381*9c5db199SXin Li for what_failed in self.failed.keys(): 382*9c5db199SXin Li print >> sys.stderr, what_failed + ':' 383*9c5db199SXin Li for (errmsg, items) in self.failed[what_failed].iteritems(): 384*9c5db199SXin Li if len(items) == 0: 385*9c5db199SXin Li print >> sys.stderr, errmsg 386*9c5db199SXin Li elif items == set(['']): 387*9c5db199SXin Li print >> sys.stderr, ' ' + errmsg 388*9c5db199SXin Li elif len(items) == 1: 389*9c5db199SXin Li # Restore the only item 390*9c5db199SXin Li if FAIL_TAG in errmsg: 391*9c5db199SXin Li errmsg = errmsg.replace(FAIL_TAG, items.pop()) 392*9c5db199SXin Li else: 393*9c5db199SXin Li errmsg = '%s (%s)' % (errmsg, items.pop()) 394*9c5db199SXin Li print >> sys.stderr, ' ' + errmsg 395*9c5db199SXin Li else: 396*9c5db199SXin Li print >> sys.stderr, ' ' + errmsg + ' with <XYZ> in:' 397*9c5db199SXin Li twrap = textwrap.TextWrapper(initial_indent=' ', 398*9c5db199SXin Li subsequent_indent=' ') 399*9c5db199SXin Li items = list(items) 400*9c5db199SXin Li items.sort() 401*9c5db199SXin Li print >> sys.stderr, twrap.fill(', '.join(items)) 402*9c5db199SXin Li return 1 403*9c5db199SXin Li 404*9c5db199SXin Li 405*9c5db199SXin Li def __init__(self): 406*9c5db199SXin Li """Setup the parser common options""" 407*9c5db199SXin Li # Initialized for unit tests. 408*9c5db199SXin Li self.afe = None 409*9c5db199SXin Li self.failed = {} 410*9c5db199SXin Li self.data = {} 411*9c5db199SXin Li self.debug = False 412*9c5db199SXin Li self.parse_delim = '|' 413*9c5db199SXin Li self.kill_on_failure = False 414*9c5db199SXin Li self.web_server = '' 415*9c5db199SXin Li self.verbose = False 416*9c5db199SXin Li self.no_confirmation = False 417*9c5db199SXin Li # Whether the topic or command supports skylab inventory repo. 418*9c5db199SXin Li self.allow_skylab = False 419*9c5db199SXin Li self.enforce_skylab = False 420*9c5db199SXin Li self.topic_parse_info = item_parse_info(attribute_name='not_used') 421*9c5db199SXin Li 422*9c5db199SXin Li self.parser = optparse.OptionParser(self._get_usage()) 423*9c5db199SXin Li self.parser.add_option('-g', '--debug', 424*9c5db199SXin Li help='Print debugging information', 425*9c5db199SXin Li action='store_true', default=False) 426*9c5db199SXin Li self.parser.add_option('--kill-on-failure', 427*9c5db199SXin Li help='Stop at the first failure', 428*9c5db199SXin Li action='store_true', default=False) 429*9c5db199SXin Li self.parser.add_option('--parse', 430*9c5db199SXin Li help='Print the output using | ' 431*9c5db199SXin Li 'separated key=value fields', 432*9c5db199SXin Li action='store_true', default=False) 433*9c5db199SXin Li self.parser.add_option('--parse-delim', 434*9c5db199SXin Li help='Delimiter to use to separate the ' 435*9c5db199SXin Li 'key=value fields', default='|') 436*9c5db199SXin Li self.parser.add_option('--no-confirmation', 437*9c5db199SXin Li help=('Skip all confirmation in when function ' 438*9c5db199SXin Li 'require_confirmation is called.'), 439*9c5db199SXin Li action='store_true', default=False) 440*9c5db199SXin Li self.parser.add_option('-v', '--verbose', 441*9c5db199SXin Li action='store_true', default=False) 442*9c5db199SXin Li self.parser.add_option('-w', '--web', 443*9c5db199SXin Li help='Specify the autotest server ' 444*9c5db199SXin Li 'to talk to', 445*9c5db199SXin Li action='store', type='string', 446*9c5db199SXin Li dest='web_server', default=None) 447*9c5db199SXin Li self.parser.add_option('--log-level', 448*9c5db199SXin Li help=('Set the logging level. Must be one of %s.' 449*9c5db199SXin Li ' Default to ERROR' % 450*9c5db199SXin Li LOGGING_LEVEL_MAP.keys()), 451*9c5db199SXin Li choices=LOGGING_LEVEL_MAP.keys(), 452*9c5db199SXin Li default='ERROR', 453*9c5db199SXin Li dest='log_level') 454*9c5db199SXin Li 455*9c5db199SXin Li 456*9c5db199SXin Li def add_skylab_options(self, enforce_skylab=True): 457*9c5db199SXin Li """Add options for reading and writing skylab inventory repository. 458*9c5db199SXin Li 459*9c5db199SXin Li The enforce_skylab parameter does nothing and is kept for compatibility. 460*9c5db199SXin Li """ 461*9c5db199SXin Li self.allow_skylab = True 462*9c5db199SXin Li self.enforce_skylab = True 463*9c5db199SXin Li 464*9c5db199SXin Li self.parser.add_option('--skylab', 465*9c5db199SXin Li help='Deprecated', 466*9c5db199SXin Li action='store_const', dest='skylab', 467*9c5db199SXin Li const=True) 468*9c5db199SXin Li self.parser.add_option('--env', 469*9c5db199SXin Li help=('Environment ("prod" or "staging") of the ' 470*9c5db199SXin Li 'machine. Default to "prod". %s' % 471*9c5db199SXin Li skylab_utils.MSG_ONLY_VALID_IN_SKYLAB), 472*9c5db199SXin Li dest='environment', 473*9c5db199SXin Li default='prod') 474*9c5db199SXin Li self.parser.add_option('--inventory-repo-dir', 475*9c5db199SXin Li help=('The path of directory to clone skylab ' 476*9c5db199SXin Li 'inventory repo into. It can be an empty ' 477*9c5db199SXin Li 'folder or an existing clean checkout of ' 478*9c5db199SXin Li 'infra_internal/skylab_inventory. ' 479*9c5db199SXin Li 'If not provided, a temporary dir will be ' 480*9c5db199SXin Li 'created and used as the repo dir. %s' % 481*9c5db199SXin Li skylab_utils.MSG_ONLY_VALID_IN_SKYLAB), 482*9c5db199SXin Li dest='inventory_repo_dir') 483*9c5db199SXin Li self.parser.add_option('--keep-repo-dir', 484*9c5db199SXin Li help=('Keep the inventory-repo-dir after the ' 485*9c5db199SXin Li 'action completes, otherwise the dir will ' 486*9c5db199SXin Li 'be cleaned up. %s' % 487*9c5db199SXin Li skylab_utils.MSG_ONLY_VALID_IN_SKYLAB), 488*9c5db199SXin Li action='store_true', 489*9c5db199SXin Li dest='keep_repo_dir') 490*9c5db199SXin Li self.parser.add_option('--draft', 491*9c5db199SXin Li help=('Upload a change CL as a draft. %s' % 492*9c5db199SXin Li skylab_utils.MSG_ONLY_VALID_IN_SKYLAB), 493*9c5db199SXin Li action='store_true', 494*9c5db199SXin Li dest='draft', 495*9c5db199SXin Li default=False) 496*9c5db199SXin Li self.parser.add_option('--dryrun', 497*9c5db199SXin Li help=('Execute the action as a dryrun. %s' % 498*9c5db199SXin Li skylab_utils.MSG_ONLY_VALID_IN_SKYLAB), 499*9c5db199SXin Li action='store_true', 500*9c5db199SXin Li dest='dryrun', 501*9c5db199SXin Li default=False) 502*9c5db199SXin Li self.parser.add_option('--submit', 503*9c5db199SXin Li help=('Submit a change CL directly without ' 504*9c5db199SXin Li 'reviewing and submitting it in Gerrit. %s' 505*9c5db199SXin Li % skylab_utils.MSG_ONLY_VALID_IN_SKYLAB), 506*9c5db199SXin Li action='store_true', 507*9c5db199SXin Li dest='submit', 508*9c5db199SXin Li default=False) 509*9c5db199SXin Li 510*9c5db199SXin Li 511*9c5db199SXin Li def _get_usage(self): 512*9c5db199SXin Li return "atest %s %s [options] %s" % (self.msg_topic.lower(), 513*9c5db199SXin Li self.usage_action, 514*9c5db199SXin Li self.msg_items) 515*9c5db199SXin Li 516*9c5db199SXin Li 517*9c5db199SXin Li def backward_compatibility(self, action, argv): 518*9c5db199SXin Li """To be overidden by subclass if their syntax changed. 519*9c5db199SXin Li 520*9c5db199SXin Li @param action: Name of the action. 521*9c5db199SXin Li @param argv: A list of arguments. 522*9c5db199SXin Li """ 523*9c5db199SXin Li return action 524*9c5db199SXin Li 525*9c5db199SXin Li 526*9c5db199SXin Li def parse_skylab_options(self, options): 527*9c5db199SXin Li """Parse skylab related options. 528*9c5db199SXin Li 529*9c5db199SXin Li @param: options: Option values parsed by the parser. 530*9c5db199SXin Li """ 531*9c5db199SXin Li self.skylab = True 532*9c5db199SXin Li 533*9c5db199SXin Li # TODO(nxia): crbug.com/837831 Add skylab_inventory to 534*9c5db199SXin Li # autotest-server-deps ebuilds to remove the ImportError check. 535*9c5db199SXin Li if not skylab_inventory_imported: 536*9c5db199SXin Li raise skylab_utils.SkylabInventoryNotImported( 537*9c5db199SXin Li "Please try to run utils/build_externals.py.") 538*9c5db199SXin Li 539*9c5db199SXin Li self.draft = options.draft 540*9c5db199SXin Li 541*9c5db199SXin Li self.dryrun = options.dryrun 542*9c5db199SXin Li if self.dryrun: 543*9c5db199SXin Li print('This is a dryrun. NO CL will be uploaded.\n') 544*9c5db199SXin Li 545*9c5db199SXin Li self.submit = options.submit 546*9c5db199SXin Li if self.submit and (self.dryrun or self.draft): 547*9c5db199SXin Li self.invalid_syntax('Can not set --dryrun or --draft when ' 548*9c5db199SXin Li '--submit is set.') 549*9c5db199SXin Li 550*9c5db199SXin Li # The change number of the inventory change CL. 551*9c5db199SXin Li self.change_number = None 552*9c5db199SXin Li 553*9c5db199SXin Li self.environment = options.environment 554*9c5db199SXin Li translation_utils.validate_environment(self.environment) 555*9c5db199SXin Li 556*9c5db199SXin Li self.keep_repo_dir = options.keep_repo_dir 557*9c5db199SXin Li self.inventory_repo_dir = options.inventory_repo_dir 558*9c5db199SXin Li if self.inventory_repo_dir is None: 559*9c5db199SXin Li self.temp_dir = autotemp.tempdir( 560*9c5db199SXin Li prefix='inventory_repo', 561*9c5db199SXin Li auto_clean=not self.keep_repo_dir) 562*9c5db199SXin Li 563*9c5db199SXin Li self.inventory_repo_dir = self.temp_dir.name 564*9c5db199SXin Li if self.debug or self.keep_repo_dir: 565*9c5db199SXin Li print('The inventory_repo_dir is created at %s' % 566*9c5db199SXin Li self.inventory_repo_dir) 567*9c5db199SXin Li 568*9c5db199SXin Li 569*9c5db199SXin Li def parse(self, parse_info=[], req_items=None): 570*9c5db199SXin Li """Parse command arguments. 571*9c5db199SXin Li 572*9c5db199SXin Li parse_info is a list of item_parse_info objects. 573*9c5db199SXin Li There should only be one use_leftover set to True in the list. 574*9c5db199SXin Li 575*9c5db199SXin Li Also check that the req_items is not empty after parsing. 576*9c5db199SXin Li 577*9c5db199SXin Li @param parse_info: A list of item_parse_info objects. 578*9c5db199SXin Li @param req_items: A list of required items. 579*9c5db199SXin Li """ 580*9c5db199SXin Li (options, leftover) = self.parse_global() 581*9c5db199SXin Li 582*9c5db199SXin Li all_parse_info = parse_info[:] 583*9c5db199SXin Li all_parse_info.append(self.topic_parse_info) 584*9c5db199SXin Li 585*9c5db199SXin Li try: 586*9c5db199SXin Li for item_parse_info in all_parse_info: 587*9c5db199SXin Li values, leftover = item_parse_info.get_values(options, 588*9c5db199SXin Li leftover) 589*9c5db199SXin Li setattr(self, item_parse_info.attribute_name, values) 590*9c5db199SXin Li except CliError as s: 591*9c5db199SXin Li self.invalid_syntax(s) 592*9c5db199SXin Li 593*9c5db199SXin Li if (req_items and not getattr(self, req_items, None)): 594*9c5db199SXin Li self.invalid_syntax('%s %s requires at least one %s' % 595*9c5db199SXin Li (self.msg_topic, 596*9c5db199SXin Li self.usage_action, 597*9c5db199SXin Li self.msg_topic)) 598*9c5db199SXin Li 599*9c5db199SXin Li if self.allow_skylab: 600*9c5db199SXin Li self.parse_skylab_options(options) 601*9c5db199SXin Li 602*9c5db199SXin Li logging.getLogger().setLevel(LOGGING_LEVEL_MAP[options.log_level]) 603*9c5db199SXin Li 604*9c5db199SXin Li return (options, leftover) 605*9c5db199SXin Li 606*9c5db199SXin Li 607*9c5db199SXin Li def parse_global(self): 608*9c5db199SXin Li """Parse the global arguments. 609*9c5db199SXin Li 610*9c5db199SXin Li It consumes what the common object needs to know, and 611*9c5db199SXin Li let the children look at all the options. We could 612*9c5db199SXin Li remove the options that we have used, but there is no 613*9c5db199SXin Li harm in leaving them, and the children may need them 614*9c5db199SXin Li in the future. 615*9c5db199SXin Li 616*9c5db199SXin Li Must be called from its children parse()""" 617*9c5db199SXin Li (options, leftover) = self.parser.parse_args() 618*9c5db199SXin Li # Handle our own options setup in __init__() 619*9c5db199SXin Li self.debug = options.debug 620*9c5db199SXin Li self.kill_on_failure = options.kill_on_failure 621*9c5db199SXin Li 622*9c5db199SXin Li if options.parse: 623*9c5db199SXin Li suffix = '_parse' 624*9c5db199SXin Li else: 625*9c5db199SXin Li suffix = '_std' 626*9c5db199SXin Li for func in ['print_fields', 'print_table', 627*9c5db199SXin Li 'print_by_ids', 'print_list']: 628*9c5db199SXin Li setattr(self, func, getattr(self, func + suffix)) 629*9c5db199SXin Li 630*9c5db199SXin Li self.parse_delim = options.parse_delim 631*9c5db199SXin Li 632*9c5db199SXin Li self.verbose = options.verbose 633*9c5db199SXin Li self.no_confirmation = options.no_confirmation 634*9c5db199SXin Li self.web_server = options.web_server 635*9c5db199SXin Li try: 636*9c5db199SXin Li self.afe = rpc.afe_comm(self.web_server) 637*9c5db199SXin Li except rpc.AuthError as s: 638*9c5db199SXin Li self.failure(str(s), fatal=True) 639*9c5db199SXin Li 640*9c5db199SXin Li return (options, leftover) 641*9c5db199SXin Li 642*9c5db199SXin Li 643*9c5db199SXin Li def check_and_create_items(self, op_get, op_create, 644*9c5db199SXin Li items, **data_create): 645*9c5db199SXin Li """Create the items if they don't exist already. 646*9c5db199SXin Li 647*9c5db199SXin Li @param op_get: Name of `get` RPC. 648*9c5db199SXin Li @param op_create: Name of `create` RPC. 649*9c5db199SXin Li @param items: Actionable items specified in CLI command, e.g., hostname, 650*9c5db199SXin Li to be passed to each RPC. 651*9c5db199SXin Li @param data_create: Data to be passed to `create` RPC. 652*9c5db199SXin Li """ 653*9c5db199SXin Li for item in items: 654*9c5db199SXin Li ret = self.execute_rpc(op_get, name=item) 655*9c5db199SXin Li 656*9c5db199SXin Li if len(ret) == 0: 657*9c5db199SXin Li try: 658*9c5db199SXin Li data_create['name'] = item 659*9c5db199SXin Li self.execute_rpc(op_create, **data_create) 660*9c5db199SXin Li except CliError: 661*9c5db199SXin Li continue 662*9c5db199SXin Li 663*9c5db199SXin Li 664*9c5db199SXin Li def execute_rpc(self, op, item='', **data): 665*9c5db199SXin Li """Execute RPC. 666*9c5db199SXin Li 667*9c5db199SXin Li @param op: Name of the RPC. 668*9c5db199SXin Li @param item: Actionable item specified in CLI command. 669*9c5db199SXin Li @param data: Data to be passed to RPC. 670*9c5db199SXin Li """ 671*9c5db199SXin Li retry = 2 672*9c5db199SXin Li while retry: 673*9c5db199SXin Li try: 674*9c5db199SXin Li return self.afe.run(op, **data) 675*9c5db199SXin Li except urllib2.URLError as err: 676*9c5db199SXin Li if hasattr(err, 'reason'): 677*9c5db199SXin Li if 'timed out' not in err.reason: 678*9c5db199SXin Li self.invalid_syntax('Invalid server name %s: %s' % 679*9c5db199SXin Li (self.afe.web_server, err)) 680*9c5db199SXin Li if hasattr(err, 'code'): 681*9c5db199SXin Li error_parts = [str(err)] 682*9c5db199SXin Li if self.debug: 683*9c5db199SXin Li error_parts.append(err.read()) # read the response body 684*9c5db199SXin Li self.failure('\n\n'.join(error_parts), item=item, 685*9c5db199SXin Li what_failed=("Error received from web server")) 686*9c5db199SXin Li raise CliError("Error from web server") 687*9c5db199SXin Li if self.debug: 688*9c5db199SXin Li print('retrying: %r %d' % (data, retry)) 689*9c5db199SXin Li retry -= 1 690*9c5db199SXin Li if retry == 0: 691*9c5db199SXin Li if item: 692*9c5db199SXin Li myerr = '%s timed out for %s' % (op, item) 693*9c5db199SXin Li else: 694*9c5db199SXin Li myerr = '%s timed out' % op 695*9c5db199SXin Li self.failure(myerr, item=item, 696*9c5db199SXin Li what_failed=("Timed-out contacting " 697*9c5db199SXin Li "the Autotest server")) 698*9c5db199SXin Li raise CliError("Timed-out contacting the Autotest server") 699*9c5db199SXin Li except mock.CheckPlaybackError: 700*9c5db199SXin Li raise 701*9c5db199SXin Li except Exception as full_error: 702*9c5db199SXin Li # There are various exceptions throwns by JSON, 703*9c5db199SXin Li # urllib & httplib, so catch them all. 704*9c5db199SXin Li self.failure(full_error, item=item, 705*9c5db199SXin Li what_failed='Operation %s failed' % op) 706*9c5db199SXin Li raise CliError(str(full_error)) 707*9c5db199SXin Li 708*9c5db199SXin Li 709*9c5db199SXin Li # There is no output() method in the atest object (yet?) 710*9c5db199SXin Li # but here are some helper functions to be used by its 711*9c5db199SXin Li # children 712*9c5db199SXin Li def print_wrapped(self, msg, values): 713*9c5db199SXin Li """Print given message and values in wrapped lines unless 714*9c5db199SXin Li AUTOTEST_CLI_NO_WRAP is specified in environment variables. 715*9c5db199SXin Li 716*9c5db199SXin Li @param msg: Message to print. 717*9c5db199SXin Li @param values: A list of values to print. 718*9c5db199SXin Li """ 719*9c5db199SXin Li if len(values) == 0: 720*9c5db199SXin Li return 721*9c5db199SXin Li elif len(values) == 1: 722*9c5db199SXin Li print(msg + ': ') 723*9c5db199SXin Li elif len(values) > 1: 724*9c5db199SXin Li if msg.endswith('s'): 725*9c5db199SXin Li print(msg + ': ') 726*9c5db199SXin Li else: 727*9c5db199SXin Li print(msg + 's: ') 728*9c5db199SXin Li 729*9c5db199SXin Li values.sort() 730*9c5db199SXin Li 731*9c5db199SXin Li if 'AUTOTEST_CLI_NO_WRAP' in os.environ: 732*9c5db199SXin Li print('\n'.join(values)) 733*9c5db199SXin Li return 734*9c5db199SXin Li 735*9c5db199SXin Li twrap = textwrap.TextWrapper(initial_indent='\t', 736*9c5db199SXin Li subsequent_indent='\t') 737*9c5db199SXin Li print(twrap.fill(', '.join(values))) 738*9c5db199SXin Li 739*9c5db199SXin Li 740*9c5db199SXin Li def __conv_value(self, type, value): 741*9c5db199SXin Li return KEYS_CONVERT.get(type, str)(value) 742*9c5db199SXin Li 743*9c5db199SXin Li 744*9c5db199SXin Li def print_fields_std(self, items, keys, title=None): 745*9c5db199SXin Li """Print the keys in each item, one on each line. 746*9c5db199SXin Li 747*9c5db199SXin Li @param items: Items to print. 748*9c5db199SXin Li @param keys: Name of the keys to look up each item in items. 749*9c5db199SXin Li @param title: Title of the output, default to None. 750*9c5db199SXin Li """ 751*9c5db199SXin Li if not items: 752*9c5db199SXin Li return 753*9c5db199SXin Li if title: 754*9c5db199SXin Li print(title) 755*9c5db199SXin Li for item in items: 756*9c5db199SXin Li for key in keys: 757*9c5db199SXin Li print('%s: %s' % (KEYS_TO_NAMES_EN[key], 758*9c5db199SXin Li self.__conv_value(key, 759*9c5db199SXin Li _get_item_key(item, key)))) 760*9c5db199SXin Li 761*9c5db199SXin Li 762*9c5db199SXin Li def print_fields_parse(self, items, keys, title=None): 763*9c5db199SXin Li """Print the keys in each item as comma separated name=value 764*9c5db199SXin Li 765*9c5db199SXin Li @param items: Items to print. 766*9c5db199SXin Li @param keys: Name of the keys to look up each item in items. 767*9c5db199SXin Li @param title: Title of the output, default to None. 768*9c5db199SXin Li """ 769*9c5db199SXin Li for item in items: 770*9c5db199SXin Li values = ['%s=%s' % (KEYS_TO_NAMES_EN[key], 771*9c5db199SXin Li self.__conv_value(key, 772*9c5db199SXin Li _get_item_key(item, key))) 773*9c5db199SXin Li for key in keys 774*9c5db199SXin Li if self.__conv_value(key, 775*9c5db199SXin Li _get_item_key(item, key)) != ''] 776*9c5db199SXin Li print(self.parse_delim.join(values)) 777*9c5db199SXin Li 778*9c5db199SXin Li 779*9c5db199SXin Li def __find_justified_fmt(self, items, keys): 780*9c5db199SXin Li """Find the max length for each field. 781*9c5db199SXin Li 782*9c5db199SXin Li @param items: Items to lookup for. 783*9c5db199SXin Li @param keys: Name of the keys to look up each item in items. 784*9c5db199SXin Li """ 785*9c5db199SXin Li lens = {} 786*9c5db199SXin Li # Don't justify the last field, otherwise we have blank 787*9c5db199SXin Li # lines when the max is overlaps but the current values 788*9c5db199SXin Li # are smaller 789*9c5db199SXin Li if not items: 790*9c5db199SXin Li print("No results") 791*9c5db199SXin Li return 792*9c5db199SXin Li for key in keys[:-1]: 793*9c5db199SXin Li lens[key] = max(len(self.__conv_value(key, 794*9c5db199SXin Li _get_item_key(item, key))) 795*9c5db199SXin Li for item in items) 796*9c5db199SXin Li lens[key] = max(lens[key], len(KEYS_TO_NAMES_EN[key])) 797*9c5db199SXin Li lens[keys[-1]] = 0 798*9c5db199SXin Li 799*9c5db199SXin Li return ' '.join(["%%-%ds" % lens[key] for key in keys]) 800*9c5db199SXin Li 801*9c5db199SXin Li 802*9c5db199SXin Li def print_dict(self, items, title=None, line_before=False): 803*9c5db199SXin Li """Print a dictionary. 804*9c5db199SXin Li 805*9c5db199SXin Li @param items: Dictionary to print. 806*9c5db199SXin Li @param title: Title of the output, default to None. 807*9c5db199SXin Li @param line_before: True to print an empty line before the output, 808*9c5db199SXin Li default to False. 809*9c5db199SXin Li """ 810*9c5db199SXin Li if not items: 811*9c5db199SXin Li return 812*9c5db199SXin Li if line_before: 813*9c5db199SXin Li print() 814*9c5db199SXin Li print(title) 815*9c5db199SXin Li for key, value in items.items(): 816*9c5db199SXin Li print('%s : %s' % (key, value)) 817*9c5db199SXin Li 818*9c5db199SXin Li 819*9c5db199SXin Li def print_table_std(self, items, keys_header, sublist_keys=()): 820*9c5db199SXin Li """Print a mix of header and lists in a user readable format. 821*9c5db199SXin Li 822*9c5db199SXin Li The headers are justified, the sublist_keys are wrapped. 823*9c5db199SXin Li 824*9c5db199SXin Li @param items: Items to print. 825*9c5db199SXin Li @param keys_header: Header of the keys, use to look up in items. 826*9c5db199SXin Li @param sublist_keys: Keys for sublist in each item. 827*9c5db199SXin Li """ 828*9c5db199SXin Li if not items: 829*9c5db199SXin Li return 830*9c5db199SXin Li fmt = self.__find_justified_fmt(items, keys_header) 831*9c5db199SXin Li header = tuple(KEYS_TO_NAMES_EN[key] for key in keys_header) 832*9c5db199SXin Li print(fmt % header) 833*9c5db199SXin Li for item in items: 834*9c5db199SXin Li values = tuple(self.__conv_value(key, 835*9c5db199SXin Li _get_item_key(item, key)) 836*9c5db199SXin Li for key in keys_header) 837*9c5db199SXin Li print(fmt % values) 838*9c5db199SXin Li if sublist_keys: 839*9c5db199SXin Li for key in sublist_keys: 840*9c5db199SXin Li self.print_wrapped(KEYS_TO_NAMES_EN[key], 841*9c5db199SXin Li _get_item_key(item, key)) 842*9c5db199SXin Li print('\n') 843*9c5db199SXin Li 844*9c5db199SXin Li 845*9c5db199SXin Li def print_table_parse(self, items, keys_header, sublist_keys=()): 846*9c5db199SXin Li """Print a mix of header and lists in a user readable format. 847*9c5db199SXin Li 848*9c5db199SXin Li @param items: Items to print. 849*9c5db199SXin Li @param keys_header: Header of the keys, use to look up in items. 850*9c5db199SXin Li @param sublist_keys: Keys for sublist in each item. 851*9c5db199SXin Li """ 852*9c5db199SXin Li for item in items: 853*9c5db199SXin Li values = ['%s=%s' % (KEYS_TO_NAMES_EN[key], 854*9c5db199SXin Li self.__conv_value(key, _get_item_key(item, key))) 855*9c5db199SXin Li for key in keys_header 856*9c5db199SXin Li if self.__conv_value(key, 857*9c5db199SXin Li _get_item_key(item, key)) != ''] 858*9c5db199SXin Li 859*9c5db199SXin Li if sublist_keys: 860*9c5db199SXin Li [values.append('%s=%s'% (KEYS_TO_NAMES_EN[key], 861*9c5db199SXin Li ','.join(_get_item_key(item, key)))) 862*9c5db199SXin Li for key in sublist_keys 863*9c5db199SXin Li if len(_get_item_key(item, key))] 864*9c5db199SXin Li 865*9c5db199SXin Li print(self.parse_delim.join(values)) 866*9c5db199SXin Li 867*9c5db199SXin Li 868*9c5db199SXin Li def print_by_ids_std(self, items, title=None, line_before=False): 869*9c5db199SXin Li """Prints ID & names of items in a user readable form. 870*9c5db199SXin Li 871*9c5db199SXin Li @param items: Items to print. 872*9c5db199SXin Li @param title: Title of the output, default to None. 873*9c5db199SXin Li @param line_before: True to print an empty line before the output, 874*9c5db199SXin Li default to False. 875*9c5db199SXin Li """ 876*9c5db199SXin Li if not items: 877*9c5db199SXin Li return 878*9c5db199SXin Li if line_before: 879*9c5db199SXin Li print() 880*9c5db199SXin Li if title: 881*9c5db199SXin Li print(title + ':') 882*9c5db199SXin Li self.print_table_std(items, keys_header=['id', 'name']) 883*9c5db199SXin Li 884*9c5db199SXin Li 885*9c5db199SXin Li def print_by_ids_parse(self, items, title=None, line_before=False): 886*9c5db199SXin Li """Prints ID & names of items in a parseable format. 887*9c5db199SXin Li 888*9c5db199SXin Li @param items: Items to print. 889*9c5db199SXin Li @param title: Title of the output, default to None. 890*9c5db199SXin Li @param line_before: True to print an empty line before the output, 891*9c5db199SXin Li default to False. 892*9c5db199SXin Li """ 893*9c5db199SXin Li if not items: 894*9c5db199SXin Li return 895*9c5db199SXin Li if line_before: 896*9c5db199SXin Li print() 897*9c5db199SXin Li if title: 898*9c5db199SXin Li print(title + '='), 899*9c5db199SXin Li values = [] 900*9c5db199SXin Li for item in items: 901*9c5db199SXin Li values += ['%s=%s' % (KEYS_TO_NAMES_EN[key], 902*9c5db199SXin Li self.__conv_value(key, 903*9c5db199SXin Li _get_item_key(item, key))) 904*9c5db199SXin Li for key in ['id', 'name'] 905*9c5db199SXin Li if self.__conv_value(key, 906*9c5db199SXin Li _get_item_key(item, key)) != ''] 907*9c5db199SXin Li print(self.parse_delim.join(values)) 908*9c5db199SXin Li 909*9c5db199SXin Li 910*9c5db199SXin Li def print_list_std(self, items, key): 911*9c5db199SXin Li """Print a wrapped list of results 912*9c5db199SXin Li 913*9c5db199SXin Li @param items: Items to to lookup for given key, could be a nested 914*9c5db199SXin Li dictionary. 915*9c5db199SXin Li @param key: Name of the key to look up for value. 916*9c5db199SXin Li """ 917*9c5db199SXin Li if not items: 918*9c5db199SXin Li return 919*9c5db199SXin Li print(' '.join(_get_item_key(item, key) for item in items)) 920*9c5db199SXin Li 921*9c5db199SXin Li 922*9c5db199SXin Li def print_list_parse(self, items, key): 923*9c5db199SXin Li """Print a wrapped list of results. 924*9c5db199SXin Li 925*9c5db199SXin Li @param items: Items to to lookup for given key, could be a nested 926*9c5db199SXin Li dictionary. 927*9c5db199SXin Li @param key: Name of the key to look up for value. 928*9c5db199SXin Li """ 929*9c5db199SXin Li if not items: 930*9c5db199SXin Li return 931*9c5db199SXin Li print('%s=%s' % (KEYS_TO_NAMES_EN[key], 932*9c5db199SXin Li ','.join(_get_item_key(item, key) for item in items))) 933*9c5db199SXin Li 934*9c5db199SXin Li 935*9c5db199SXin Li @staticmethod 936*9c5db199SXin Li def prompt_confirmation(message=None): 937*9c5db199SXin Li """Prompt a question for user to confirm the action before proceeding. 938*9c5db199SXin Li 939*9c5db199SXin Li @param message: A detailed message to explain possible impact of the 940*9c5db199SXin Li action. 941*9c5db199SXin Li 942*9c5db199SXin Li @return: True to proceed or False to abort. 943*9c5db199SXin Li """ 944*9c5db199SXin Li if message: 945*9c5db199SXin Li print(message) 946*9c5db199SXin Li sys.stdout.write('Continue? [y/N] ') 947*9c5db199SXin Li read = raw_input().lower() 948*9c5db199SXin Li if read == 'y': 949*9c5db199SXin Li return True 950*9c5db199SXin Li else: 951*9c5db199SXin Li print('User did not confirm. Aborting...') 952*9c5db199SXin Li return False 953*9c5db199SXin Li 954*9c5db199SXin Li 955*9c5db199SXin Li @staticmethod 956*9c5db199SXin Li def require_confirmation(message=None): 957*9c5db199SXin Li """Decorator to prompt a question for user to confirm action before 958*9c5db199SXin Li proceeding. 959*9c5db199SXin Li 960*9c5db199SXin Li If user chooses not to proceed, do not call the function. 961*9c5db199SXin Li 962*9c5db199SXin Li @param message: A detailed message to explain possible impact of the 963*9c5db199SXin Li action. 964*9c5db199SXin Li 965*9c5db199SXin Li @return: A decorator wrapper for calling the actual function. 966*9c5db199SXin Li """ 967*9c5db199SXin Li def deco_require_confirmation(func): 968*9c5db199SXin Li """Wrapper for the decorator. 969*9c5db199SXin Li 970*9c5db199SXin Li @param func: Function to be called. 971*9c5db199SXin Li 972*9c5db199SXin Li @return: the actual decorator to call the function. 973*9c5db199SXin Li """ 974*9c5db199SXin Li def func_require_confirmation(*args, **kwargs): 975*9c5db199SXin Li """Decorator to prompt a question for user to confirm. 976*9c5db199SXin Li 977*9c5db199SXin Li @param message: A detailed message to explain possible impact of 978*9c5db199SXin Li the action. 979*9c5db199SXin Li """ 980*9c5db199SXin Li if (args[0].no_confirmation or 981*9c5db199SXin Li atest.prompt_confirmation(message)): 982*9c5db199SXin Li func(*args, **kwargs) 983*9c5db199SXin Li 984*9c5db199SXin Li return func_require_confirmation 985*9c5db199SXin Li return deco_require_confirmation 986