# # Copyright 2008 Google Inc. All Rights Reserved. """ The host module contains the objects and method used to manage a host in Autotest. The valid actions are: create: adds host(s) delete: deletes host(s) list: lists host(s) stat: displays host(s) information mod: modifies host(s) jobs: lists all jobs that ran on host(s) The common options are: -M|--mlist: file containing a list of machines See topic_common.py for a High Level Design and Algorithm. """ import re from autotest_lib.cli import action_common, topic_common from autotest_lib.client.common_lib import host_protections class host(topic_common.atest): """Host class atest host [create|delete|list|stat|mod|jobs] <options>""" usage_action = '[create|delete|list|stat|mod|jobs]' topic = msg_topic = 'host' msg_items = '<hosts>' protections = host_protections.Protection.names def __init__(self): """Add to the parser the options common to all the host actions""" super(host, self).__init__() self.parser.add_option('-M', '--mlist', help='File listing the machines', type='string', default=None, metavar='MACHINE_FLIST') self.topic_parse_info = topic_common.item_parse_info( attribute_name='hosts', filename_option='mlist', use_leftover=True) def _parse_lock_options(self, options): if options.lock and options.unlock: self.invalid_syntax('Only specify one of ' '--lock and --unlock.') if options.lock: self.data['locked'] = True self.messages.append('Locked host') elif options.unlock: self.data['locked'] = False self.data['lock_reason'] = '' self.messages.append('Unlocked host') if options.lock and options.lock_reason: self.data['lock_reason'] = options.lock_reason def _cleanup_labels(self, labels, platform=None): """Removes the platform label from the overall labels""" if platform: return [label for label in labels if label != platform] else: try: return [label for label in labels if not label['platform']] except TypeError: # This is a hack - the server will soon # do this, so all this code should be removed. return labels def get_items(self): return self.hosts class host_help(host): """Just here to get the atest logic working. Usage is set by its parent""" pass class host_list(action_common.atest_list, host): """atest host list [--mlist <file>|<hosts>] [--label <label>] [--status <status1,status2>] [--acl <ACL>] [--user <user>]""" def __init__(self): super(host_list, self).__init__() self.parser.add_option('-b', '--label', default='', help='Only list hosts with all these labels ' '(comma separated)') self.parser.add_option('-s', '--status', default='', help='Only list hosts with any of these ' 'statuses (comma separated)') self.parser.add_option('-a', '--acl', default='', help='Only list hosts within this ACL') self.parser.add_option('-u', '--user', default='', help='Only list hosts available to this user') self.parser.add_option('-N', '--hostnames-only', help='Only return ' 'hostnames for the machines queried.', action='store_true') self.parser.add_option('--locked', default=False, help='Only list locked hosts', action='store_true') self.parser.add_option('--unlocked', default=False, help='Only list unlocked hosts', action='store_true') def parse(self): """Consume the specific options""" label_info = topic_common.item_parse_info(attribute_name='labels', inline_option='label') (options, leftover) = super(host_list, self).parse([label_info]) self.status = options.status self.acl = options.acl self.user = options.user self.hostnames_only = options.hostnames_only if options.locked and options.unlocked: self.invalid_syntax('--locked and --unlocked are ' 'mutually exclusive') self.locked = options.locked self.unlocked = options.unlocked return (options, leftover) def execute(self): filters = {} check_results = {} if self.hosts: filters['hostname__in'] = self.hosts check_results['hostname__in'] = 'hostname' if self.labels: if len(self.labels) == 1: # This is needed for labels with wildcards (x86*) filters['labels__name__in'] = self.labels check_results['labels__name__in'] = None else: filters['multiple_labels'] = self.labels check_results['multiple_labels'] = None if self.status: statuses = self.status.split(',') statuses = [status.strip() for status in statuses if status.strip()] filters['status__in'] = statuses check_results['status__in'] = None if self.acl: filters['aclgroup__name'] = self.acl check_results['aclgroup__name'] = None if self.user: filters['aclgroup__users__login'] = self.user check_results['aclgroup__users__login'] = None if self.locked or self.unlocked: filters['locked'] = self.locked check_results['locked'] = None return super(host_list, self).execute(op='get_hosts', filters=filters, check_results=check_results) def output(self, results): if results: # Remove the platform from the labels. for result in results: result['labels'] = self._cleanup_labels(result['labels'], result['platform']) if self.hostnames_only: self.print_list(results, key='hostname') else: keys = ['hostname', 'status', 'shard', 'locked', 'lock_reason', 'platform', 'labels'] super(host_list, self).output(results, keys=keys) class host_stat(host): """atest host stat --mlist <file>|<hosts>""" usage_action = 'stat' def execute(self): results = [] # Convert wildcards into real host stats. existing_hosts = [] for host in self.hosts: if host.endswith('*'): stats = self.execute_rpc('get_hosts', hostname__startswith=host.rstrip('*')) if len(stats) == 0: self.failure('No hosts matching %s' % host, item=host, what_failed='Failed to stat') continue else: stats = self.execute_rpc('get_hosts', hostname=host) if len(stats) == 0: self.failure('Unknown host %s' % host, item=host, what_failed='Failed to stat') continue existing_hosts.extend(stats) for stat in existing_hosts: host = stat['hostname'] # The host exists, these should succeed acls = self.execute_rpc('get_acl_groups', hosts__hostname=host) labels = self.execute_rpc('get_labels', host__hostname=host) results.append([[stat], acls, labels, stat['attributes']]) return results def output(self, results): for stats, acls, labels, attributes in results: print '-'*5 self.print_fields(stats, keys=['hostname', 'platform', 'status', 'locked', 'locked_by', 'lock_time', 'lock_reason', 'protection',]) self.print_by_ids(acls, 'ACLs', line_before=True) labels = self._cleanup_labels(labels) self.print_by_ids(labels, 'Labels', line_before=True) self.print_dict(attributes, 'Host Attributes', line_before=True) class host_jobs(host): """atest host jobs [--max-query] --mlist <file>|<hosts>""" usage_action = 'jobs' def __init__(self): super(host_jobs, self).__init__() self.parser.add_option('-q', '--max-query', help='Limits the number of results ' '(20 by default)', type='int', default=20) def parse(self): """Consume the specific options""" (options, leftover) = super(host_jobs, self).parse() self.max_queries = options.max_query return (options, leftover) def execute(self): results = [] real_hosts = [] for host in self.hosts: if host.endswith('*'): stats = self.execute_rpc('get_hosts', hostname__startswith=host.rstrip('*')) if len(stats) == 0: self.failure('No host matching %s' % host, item=host, what_failed='Failed to stat') [real_hosts.append(stat['hostname']) for stat in stats] else: real_hosts.append(host) for host in real_hosts: queue_entries = self.execute_rpc('get_host_queue_entries', host__hostname=host, query_limit=self.max_queries, sort_by=['-job__id']) jobs = [] for entry in queue_entries: job = {'job_id': entry['job']['id'], 'job_owner': entry['job']['owner'], 'job_name': entry['job']['name'], 'status': entry['status']} jobs.append(job) results.append((host, jobs)) return results def output(self, results): for host, jobs in results: print '-'*5 print 'Hostname: %s' % host self.print_table(jobs, keys_header=['job_id', 'job_owner', 'job_name', 'status']) class host_mod(host): """atest host mod --lock|--unlock|--force_modify_locking|--protection --mlist <file>|<hosts>""" usage_action = 'mod' attribute_regex = r'^(?P<attribute>\w+)=(?P<value>.+)?' def __init__(self): """Add the options specific to the mod action""" self.data = {} self.messages = [] self.attribute = None self.value = None super(host_mod, self).__init__() self.parser.add_option('-l', '--lock', help='Lock hosts', action='store_true') self.parser.add_option('-u', '--unlock', help='Unlock hosts', action='store_true') self.parser.add_option('-f', '--force_modify_locking', help='Forcefully lock\unlock a host', action='store_true') self.parser.add_option('-r', '--lock_reason', help='Reason for locking hosts', default='') self.parser.add_option('-p', '--protection', type='choice', help=('Set the protection level on a host. ' 'Must be one of: %s' % ', '.join('"%s"' % p for p in self.protections)), choices=self.protections) self.parser.add_option('--attribute', '-a', default='', help=('Host attribute to add or change. Format ' 'is <attribute>=<value>. Value can be ' 'blank to delete attribute.')) def parse(self): """Consume the specific options""" (options, leftover) = super(host_mod, self).parse() self._parse_lock_options(options) if options.force_modify_locking: self.data['force_modify_locking'] = True if options.protection: self.data['protection'] = options.protection self.messages.append('Protection set to "%s"' % options.protection) if len(self.data) == 0 and not options.attribute: self.invalid_syntax('No modification requested') if options.attribute: match = re.match(self.attribute_regex, options.attribute) if not match: self.invalid_syntax('Attributes must be in <attribute>=<value>' ' syntax!') self.attribute = match.group('attribute') self.value = match.group('value') return (options, leftover) def execute(self): successes = [] for host in self.hosts: try: res = self.execute_rpc('modify_host', item=host, id=host, **self.data) if self.attribute: self.execute_rpc('set_host_attribute', attribute=self.attribute, value=self.value, hostname=host) # TODO: Make the AFE return True or False, # especially for lock successes.append(host) except topic_common.CliError, full_error: # Already logged by execute_rpc() pass return successes def output(self, hosts): for msg in self.messages: self.print_wrapped(msg, hosts) class host_create(host): """atest host create [--lock|--unlock --platform <arch> --labels <labels>|--blist <label_file> --acls <acls>|--alist <acl_file> --protection <protection_type> --mlist <mach_file>] <hosts>""" usage_action = 'create' def __init__(self): self.messages = [] super(host_create, self).__init__() self.parser.add_option('-l', '--lock', help='Create the hosts as locked', action='store_true', default=False) self.parser.add_option('-u', '--unlock', help='Create the hosts as ' 'unlocked (default)', action='store_true') self.parser.add_option('-r', '--lock_reason', help='Reason for locking hosts', default='') self.parser.add_option('-t', '--platform', help='Sets the platform label') self.parser.add_option('-b', '--labels', help='Comma separated list of labels') self.parser.add_option('-B', '--blist', help='File listing the labels', type='string', metavar='LABEL_FLIST') self.parser.add_option('-a', '--acls', help='Comma separated list of ACLs') self.parser.add_option('-A', '--alist', help='File listing the acls', type='string', metavar='ACL_FLIST') self.parser.add_option('-p', '--protection', type='choice', help=('Set the protection level on a host. ' 'Must be one of: %s' % ', '.join('"%s"' % p for p in self.protections)), choices=self.protections) self.parser.add_option('-s', '--serials', help=('Comma separated list of adb-based device ' 'serials')) def parse(self): label_info = topic_common.item_parse_info(attribute_name='labels', inline_option='labels', filename_option='blist') acl_info = topic_common.item_parse_info(attribute_name='acls', inline_option='acls', filename_option='alist') (options, leftover) = super(host_create, self).parse([label_info, acl_info], req_items='hosts') self._parse_lock_options(options) self.locked = options.lock self.platform = getattr(options, 'platform', None) self.serials = getattr(options, 'serials', None) if self.serials: if len(self.hosts) > 1: raise topic_common.CliError('Can not specify serials with ' 'multiple hosts') self.serials = self.serials.split(',') if options.protection: self.data['protection'] = options.protection return (options, leftover) def _execute_add_one_host(self, host): # Always add the hosts as locked to avoid the host # being picked up by the scheduler before it's ACL'ed. # We enforce lock reasons for each lock, so we # provide a 'dummy' if we are intending to unlock after. self.data['locked'] = True if not self.locked: self.data['lock_reason'] = 'Forced lock on device creation' self.execute_rpc('add_host', hostname=host, status="Ready", **self.data) # Now add the platform label labels = self.labels[:] if self.platform: labels.append(self.platform) if len (labels): self.execute_rpc('host_add_labels', id=host, labels=labels) def _execute_add_hosts(self): successful_hosts = self.site_create_hosts_hook() if successful_hosts: for acl in self.acls: self.execute_rpc('acl_group_add_hosts', id=acl, hosts=successful_hosts) if not self.locked: for host in successful_hosts: self.execute_rpc('modify_host', id=host, locked=False, lock_reason='') return successful_hosts def execute(self): # We need to check if these labels & ACLs exist, # and create them if not. if self.platform: self.check_and_create_items('get_labels', 'add_label', [self.platform], platform=True) if self.labels: self.check_and_create_items('get_labels', 'add_label', self.labels, platform=False) if self.acls: self.check_and_create_items('get_acl_groups', 'add_acl_group', self.acls) return self._execute_add_hosts() def site_create_hosts_hook(self): successful_hosts = [] for host in self.hosts: try: self._execute_add_one_host(host) successful_hosts.append(host) except topic_common.CliError: pass return successful_hosts def output(self, hosts): self.print_wrapped('Added host', hosts) class host_delete(action_common.atest_delete, host): """atest host delete [--mlist <mach_file>] <hosts>""" pass