Home | History | Annotate | Download | only in cli
      1 #
      2 # Copyright 2008 Google Inc. All Rights Reserved.
      3 
      4 """
      5 The host module contains the objects and method used to
      6 manage a host in Autotest.
      7 
      8 The valid actions are:
      9 create:  adds host(s)
     10 delete:  deletes host(s)
     11 list:    lists host(s)
     12 stat:    displays host(s) information
     13 mod:     modifies host(s)
     14 jobs:    lists all jobs that ran on host(s)
     15 
     16 The common options are:
     17 -M|--mlist:   file containing a list of machines
     18 
     19 
     20 See topic_common.py for a High Level Design and Algorithm.
     21 
     22 """
     23 import re
     24 
     25 from autotest_lib.cli import action_common, topic_common
     26 from autotest_lib.client.common_lib import host_protections
     27 
     28 
     29 class host(topic_common.atest):
     30     """Host class
     31     atest host [create|delete|list|stat|mod|jobs] <options>"""
     32     usage_action = '[create|delete|list|stat|mod|jobs]'
     33     topic = msg_topic = 'host'
     34     msg_items = '<hosts>'
     35 
     36     protections = host_protections.Protection.names
     37 
     38 
     39     def __init__(self):
     40         """Add to the parser the options common to all the
     41         host actions"""
     42         super(host, self).__init__()
     43 
     44         self.parser.add_option('-M', '--mlist',
     45                                help='File listing the machines',
     46                                type='string',
     47                                default=None,
     48                                metavar='MACHINE_FLIST')
     49 
     50         self.topic_parse_info = topic_common.item_parse_info(
     51             attribute_name='hosts',
     52             filename_option='mlist',
     53             use_leftover=True)
     54 
     55 
     56     def _parse_lock_options(self, options):
     57         if options.lock and options.unlock:
     58             self.invalid_syntax('Only specify one of '
     59                                 '--lock and --unlock.')
     60 
     61         if options.lock:
     62             self.data['locked'] = True
     63             self.messages.append('Locked host')
     64         elif options.unlock:
     65             self.data['locked'] = False
     66             self.data['lock_reason'] = ''
     67             self.messages.append('Unlocked host')
     68 
     69         if options.lock and options.lock_reason:
     70             self.data['lock_reason'] = options.lock_reason
     71 
     72 
     73     def _cleanup_labels(self, labels, platform=None):
     74         """Removes the platform label from the overall labels"""
     75         if platform:
     76             return [label for label in labels
     77                     if label != platform]
     78         else:
     79             try:
     80                 return [label for label in labels
     81                         if not label['platform']]
     82             except TypeError:
     83                 # This is a hack - the server will soon
     84                 # do this, so all this code should be removed.
     85                 return labels
     86 
     87 
     88     def get_items(self):
     89         return self.hosts
     90 
     91 
     92 class host_help(host):
     93     """Just here to get the atest logic working.
     94     Usage is set by its parent"""
     95     pass
     96 
     97 
     98 class host_list(action_common.atest_list, host):
     99     """atest host list [--mlist <file>|<hosts>] [--label <label>]
    100        [--status <status1,status2>] [--acl <ACL>] [--user <user>]"""
    101 
    102     def __init__(self):
    103         super(host_list, self).__init__()
    104 
    105         self.parser.add_option('-b', '--label',
    106                                default='',
    107                                help='Only list hosts with all these labels '
    108                                '(comma separated)')
    109         self.parser.add_option('-s', '--status',
    110                                default='',
    111                                help='Only list hosts with any of these '
    112                                'statuses (comma separated)')
    113         self.parser.add_option('-a', '--acl',
    114                                default='',
    115                                help='Only list hosts within this ACL')
    116         self.parser.add_option('-u', '--user',
    117                                default='',
    118                                help='Only list hosts available to this user')
    119         self.parser.add_option('-N', '--hostnames-only', help='Only return '
    120                                'hostnames for the machines queried.',
    121                                action='store_true')
    122         self.parser.add_option('--locked',
    123                                default=False,
    124                                help='Only list locked hosts',
    125                                action='store_true')
    126         self.parser.add_option('--unlocked',
    127                                default=False,
    128                                help='Only list unlocked hosts',
    129                                action='store_true')
    130 
    131 
    132 
    133     def parse(self):
    134         """Consume the specific options"""
    135         label_info = topic_common.item_parse_info(attribute_name='labels',
    136                                                   inline_option='label')
    137 
    138         (options, leftover) = super(host_list, self).parse([label_info])
    139 
    140         self.status = options.status
    141         self.acl = options.acl
    142         self.user = options.user
    143         self.hostnames_only = options.hostnames_only
    144 
    145         if options.locked and options.unlocked:
    146             self.invalid_syntax('--locked and --unlocked are '
    147                                 'mutually exclusive')
    148         self.locked = options.locked
    149         self.unlocked = options.unlocked
    150         return (options, leftover)
    151 
    152 
    153     def execute(self):
    154         filters = {}
    155         check_results = {}
    156         if self.hosts:
    157             filters['hostname__in'] = self.hosts
    158             check_results['hostname__in'] = 'hostname'
    159 
    160         if self.labels:
    161             if len(self.labels) == 1:
    162                 # This is needed for labels with wildcards (x86*)
    163                 filters['labels__name__in'] = self.labels
    164                 check_results['labels__name__in'] = None
    165             else:
    166                 filters['multiple_labels'] = self.labels
    167                 check_results['multiple_labels'] = None
    168 
    169         if self.status:
    170             statuses = self.status.split(',')
    171             statuses = [status.strip() for status in statuses
    172                         if status.strip()]
    173 
    174             filters['status__in'] = statuses
    175             check_results['status__in'] = None
    176 
    177         if self.acl:
    178             filters['aclgroup__name'] = self.acl
    179             check_results['aclgroup__name'] = None
    180         if self.user:
    181             filters['aclgroup__users__login'] = self.user
    182             check_results['aclgroup__users__login'] = None
    183 
    184         if self.locked or self.unlocked:
    185             filters['locked'] = self.locked
    186             check_results['locked'] = None
    187 
    188         return super(host_list, self).execute(op='get_hosts',
    189                                               filters=filters,
    190                                               check_results=check_results)
    191 
    192 
    193     def output(self, results):
    194         if results:
    195             # Remove the platform from the labels.
    196             for result in results:
    197                 result['labels'] = self._cleanup_labels(result['labels'],
    198                                                         result['platform'])
    199         if self.hostnames_only:
    200             self.print_list(results, key='hostname')
    201         else:
    202             keys = ['hostname', 'status',
    203                     'shard', 'locked', 'lock_reason', 'platform', 'labels']
    204             super(host_list, self).output(results, keys=keys)
    205 
    206 
    207 class host_stat(host):
    208     """atest host stat --mlist <file>|<hosts>"""
    209     usage_action = 'stat'
    210 
    211     def execute(self):
    212         results = []
    213         # Convert wildcards into real host stats.
    214         existing_hosts = []
    215         for host in self.hosts:
    216             if host.endswith('*'):
    217                 stats = self.execute_rpc('get_hosts',
    218                                          hostname__startswith=host.rstrip('*'))
    219                 if len(stats) == 0:
    220                     self.failure('No hosts matching %s' % host, item=host,
    221                                  what_failed='Failed to stat')
    222                     continue
    223             else:
    224                 stats = self.execute_rpc('get_hosts', hostname=host)
    225                 if len(stats) == 0:
    226                     self.failure('Unknown host %s' % host, item=host,
    227                                  what_failed='Failed to stat')
    228                     continue
    229             existing_hosts.extend(stats)
    230 
    231         for stat in existing_hosts:
    232             host = stat['hostname']
    233             # The host exists, these should succeed
    234             acls = self.execute_rpc('get_acl_groups', hosts__hostname=host)
    235 
    236             labels = self.execute_rpc('get_labels', host__hostname=host)
    237             results.append([[stat], acls, labels, stat['attributes']])
    238         return results
    239 
    240 
    241     def output(self, results):
    242         for stats, acls, labels, attributes in results:
    243             print '-'*5
    244             self.print_fields(stats,
    245                               keys=['hostname', 'platform',
    246                                     'status', 'locked', 'locked_by',
    247                                     'lock_time', 'lock_reason', 'protection',])
    248             self.print_by_ids(acls, 'ACLs', line_before=True)
    249             labels = self._cleanup_labels(labels)
    250             self.print_by_ids(labels, 'Labels', line_before=True)
    251             self.print_dict(attributes, 'Host Attributes', line_before=True)
    252 
    253 
    254 class host_jobs(host):
    255     """atest host jobs [--max-query] --mlist <file>|<hosts>"""
    256     usage_action = 'jobs'
    257 
    258     def __init__(self):
    259         super(host_jobs, self).__init__()
    260         self.parser.add_option('-q', '--max-query',
    261                                help='Limits the number of results '
    262                                '(20 by default)',
    263                                type='int', default=20)
    264 
    265 
    266     def parse(self):
    267         """Consume the specific options"""
    268         (options, leftover) = super(host_jobs, self).parse()
    269         self.max_queries = options.max_query
    270         return (options, leftover)
    271 
    272 
    273     def execute(self):
    274         results = []
    275         real_hosts = []
    276         for host in self.hosts:
    277             if host.endswith('*'):
    278                 stats = self.execute_rpc('get_hosts',
    279                                          hostname__startswith=host.rstrip('*'))
    280                 if len(stats) == 0:
    281                     self.failure('No host matching %s' % host, item=host,
    282                                  what_failed='Failed to stat')
    283                 [real_hosts.append(stat['hostname']) for stat in stats]
    284             else:
    285                 real_hosts.append(host)
    286 
    287         for host in real_hosts:
    288             queue_entries = self.execute_rpc('get_host_queue_entries',
    289                                              host__hostname=host,
    290                                              query_limit=self.max_queries,
    291                                              sort_by=['-job__id'])
    292             jobs = []
    293             for entry in queue_entries:
    294                 job = {'job_id': entry['job']['id'],
    295                        'job_owner': entry['job']['owner'],
    296                        'job_name': entry['job']['name'],
    297                        'status': entry['status']}
    298                 jobs.append(job)
    299             results.append((host, jobs))
    300         return results
    301 
    302 
    303     def output(self, results):
    304         for host, jobs in results:
    305             print '-'*5
    306             print 'Hostname: %s' % host
    307             self.print_table(jobs, keys_header=['job_id',
    308                                                 'job_owner',
    309                                                 'job_name',
    310                                                 'status'])
    311 
    312 
    313 class host_mod(host):
    314     """atest host mod --lock|--unlock|--force_modify_locking|--protection
    315     --mlist <file>|<hosts>"""
    316     usage_action = 'mod'
    317     attribute_regex = r'^(?P<attribute>\w+)=(?P<value>.+)?'
    318 
    319     def __init__(self):
    320         """Add the options specific to the mod action"""
    321         self.data = {}
    322         self.messages = []
    323         self.attribute = None
    324         self.value = None
    325         super(host_mod, self).__init__()
    326         self.parser.add_option('-l', '--lock',
    327                                help='Lock hosts',
    328                                action='store_true')
    329         self.parser.add_option('-u', '--unlock',
    330                                help='Unlock hosts',
    331                                action='store_true')
    332         self.parser.add_option('-f', '--force_modify_locking',
    333                                help='Forcefully lock\unlock a host',
    334                                action='store_true')
    335         self.parser.add_option('-r', '--lock_reason',
    336                                help='Reason for locking hosts',
    337                                default='')
    338         self.parser.add_option('-p', '--protection', type='choice',
    339                                help=('Set the protection level on a host.  '
    340                                      'Must be one of: %s' %
    341                                      ', '.join('"%s"' % p
    342                                                for p in self.protections)),
    343                                choices=self.protections)
    344         self.parser.add_option('--attribute', '-a', default='',
    345                                help=('Host attribute to add or change. Format '
    346                                      'is <attribute>=<value>. Value can be '
    347                                      'blank to delete attribute.'))
    348 
    349 
    350     def parse(self):
    351         """Consume the specific options"""
    352         (options, leftover) = super(host_mod, self).parse()
    353 
    354         self._parse_lock_options(options)
    355         if options.force_modify_locking:
    356              self.data['force_modify_locking'] = True
    357 
    358         if options.protection:
    359             self.data['protection'] = options.protection
    360             self.messages.append('Protection set to "%s"' % options.protection)
    361 
    362         if len(self.data) == 0 and not options.attribute:
    363             self.invalid_syntax('No modification requested')
    364 
    365         if options.attribute:
    366             match = re.match(self.attribute_regex, options.attribute)
    367             if not match:
    368                 self.invalid_syntax('Attributes must be in <attribute>=<value>'
    369                                     ' syntax!')
    370 
    371             self.attribute = match.group('attribute')
    372             self.value = match.group('value')
    373 
    374         return (options, leftover)
    375 
    376 
    377     def execute(self):
    378         successes = []
    379         for host in self.hosts:
    380             try:
    381                 res = self.execute_rpc('modify_host', item=host,
    382                                        id=host, **self.data)
    383                 if self.attribute:
    384                     self.execute_rpc('set_host_attribute',
    385                                      attribute=self.attribute,
    386                                      value=self.value, hostname=host)
    387                 # TODO: Make the AFE return True or False,
    388                 # especially for lock
    389                 successes.append(host)
    390             except topic_common.CliError, full_error:
    391                 # Already logged by execute_rpc()
    392                 pass
    393 
    394         return successes
    395 
    396 
    397     def output(self, hosts):
    398         for msg in self.messages:
    399             self.print_wrapped(msg, hosts)
    400 
    401 
    402 class host_create(host):
    403     """atest host create [--lock|--unlock --platform <arch>
    404     --labels <labels>|--blist <label_file>
    405     --acls <acls>|--alist <acl_file>
    406     --protection <protection_type>
    407     --mlist <mach_file>] <hosts>"""
    408     usage_action = 'create'
    409 
    410     def __init__(self):
    411         self.messages = []
    412         super(host_create, self).__init__()
    413         self.parser.add_option('-l', '--lock',
    414                                help='Create the hosts as locked',
    415                                action='store_true', default=False)
    416         self.parser.add_option('-u', '--unlock',
    417                                help='Create the hosts as '
    418                                'unlocked (default)',
    419                                action='store_true')
    420         self.parser.add_option('-r', '--lock_reason',
    421                                help='Reason for locking hosts',
    422                                default='')
    423         self.parser.add_option('-t', '--platform',
    424                                help='Sets the platform label')
    425         self.parser.add_option('-b', '--labels',
    426                                help='Comma separated list of labels')
    427         self.parser.add_option('-B', '--blist',
    428                                help='File listing the labels',
    429                                type='string',
    430                                metavar='LABEL_FLIST')
    431         self.parser.add_option('-a', '--acls',
    432                                help='Comma separated list of ACLs')
    433         self.parser.add_option('-A', '--alist',
    434                                help='File listing the acls',
    435                                type='string',
    436                                metavar='ACL_FLIST')
    437         self.parser.add_option('-p', '--protection', type='choice',
    438                                help=('Set the protection level on a host.  '
    439                                      'Must be one of: %s' %
    440                                      ', '.join('"%s"' % p
    441                                                for p in self.protections)),
    442                                choices=self.protections)
    443         self.parser.add_option('-s', '--serials',
    444                                help=('Comma separated list of adb-based device '
    445                                      'serials'))
    446 
    447 
    448     def parse(self):
    449         label_info = topic_common.item_parse_info(attribute_name='labels',
    450                                                  inline_option='labels',
    451                                                  filename_option='blist')
    452         acl_info = topic_common.item_parse_info(attribute_name='acls',
    453                                                 inline_option='acls',
    454                                                 filename_option='alist')
    455 
    456         (options, leftover) = super(host_create, self).parse([label_info,
    457                                                               acl_info],
    458                                                              req_items='hosts')
    459 
    460         self._parse_lock_options(options)
    461         self.locked = options.lock
    462         self.platform = getattr(options, 'platform', None)
    463         self.serials = getattr(options, 'serials', None)
    464         if self.serials:
    465             if len(self.hosts) > 1:
    466                 raise topic_common.CliError('Can not specify serials with '
    467                                             'multiple hosts')
    468             self.serials = self.serials.split(',')
    469         if options.protection:
    470             self.data['protection'] = options.protection
    471         return (options, leftover)
    472 
    473 
    474     def _execute_add_one_host(self, host):
    475         # Always add the hosts as locked to avoid the host
    476         # being picked up by the scheduler before it's ACL'ed.
    477         # We enforce lock reasons for each lock, so we
    478         # provide a 'dummy' if we are intending to unlock after.
    479         self.data['locked'] = True
    480         if not self.locked:
    481             self.data['lock_reason'] = 'Forced lock on device creation'
    482         self.execute_rpc('add_host', hostname=host,
    483                          status="Ready", **self.data)
    484 
    485         # Now add the platform label
    486         labels = self.labels[:]
    487         if self.platform:
    488             labels.append(self.platform)
    489         if len (labels):
    490             self.execute_rpc('host_add_labels', id=host, labels=labels)
    491 
    492 
    493     def _execute_add_hosts(self):
    494         successful_hosts = self.site_create_hosts_hook()
    495 
    496         if successful_hosts:
    497             for acl in self.acls:
    498                 self.execute_rpc('acl_group_add_hosts',
    499                                  id=acl,
    500                                  hosts=successful_hosts)
    501 
    502             if not self.locked:
    503                 for host in successful_hosts:
    504                     self.execute_rpc('modify_host', id=host, locked=False,
    505                                      lock_reason='')
    506         return successful_hosts
    507 
    508 
    509     def execute(self):
    510         # We need to check if these labels & ACLs exist,
    511         # and create them if not.
    512         if self.platform:
    513             self.check_and_create_items('get_labels', 'add_label',
    514                                         [self.platform],
    515                                         platform=True)
    516 
    517         if self.labels:
    518             self.check_and_create_items('get_labels', 'add_label',
    519                                         self.labels,
    520                                         platform=False)
    521 
    522         if self.acls:
    523             self.check_and_create_items('get_acl_groups',
    524                                         'add_acl_group',
    525                                         self.acls)
    526 
    527         return self._execute_add_hosts()
    528 
    529 
    530     def site_create_hosts_hook(self):
    531         successful_hosts = []
    532         for host in self.hosts:
    533             try:
    534                 self._execute_add_one_host(host)
    535                 successful_hosts.append(host)
    536             except topic_common.CliError:
    537                 pass
    538 
    539         return successful_hosts
    540 
    541 
    542     def output(self, hosts):
    543         self.print_wrapped('Added host', hosts)
    544 
    545 
    546 class host_delete(action_common.atest_delete, host):
    547     """atest host delete [--mlist <mach_file>] <hosts>"""
    548     pass
    549