xref: /aosp_15_r20/external/autotest/cli/topic_common.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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