atest host: remove skylab-related logic
As it's all related to the old git-based inventory
BUG=chromium:1119035
TEST=None
Change-Id: Ifc7c38fb908a1e7c3de01a44dc8e7fb7e7aa25a7
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/2606920
Commit-Queue: Xixuan Wu <xixuan@chromium.org>
Tested-by: Xixuan Wu <xixuan@chromium.org>
Reviewed-by: Anh Le <anhdle@chromium.org>
diff --git a/cli/host.py b/cli/host.py
deleted file mode 100644
index b10d87c..0000000
--- a/cli/host.py
+++ /dev/null
@@ -1,1465 +0,0 @@
-# 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.
-
-"""
-
-from __future__ import print_function
-
-import common
-import json
-import random
-import re
-import socket
-import sys
-import time
-
-from autotest_lib.cli import action_common, rpc, topic_common, skylab_utils
-from autotest_lib.client.bin import utils as bin_utils
-from autotest_lib.client.common_lib import error, host_protections
-from autotest_lib.server import frontend, hosts
-from autotest_lib.server.hosts import host_info
-from autotest_lib.server.lib.status_history import HostJobHistory
-from autotest_lib.server.lib.status_history import UNUSED, WORKING
-from autotest_lib.server.lib.status_history import BROKEN, UNKNOWN
-
-
-try:
- from skylab_inventory import text_manager
- from skylab_inventory.lib import device
- from skylab_inventory.lib import server as skylab_server
-except ImportError:
- pass
-
-
-MIGRATED_HOST_SUFFIX = '-migrated-do-not-use'
-
-
-ID_AUTOGEN_MESSAGE = ("[IGNORED]. Do not edit (crbug.com/950553). ID is "
- "auto-generated.")
-
-
-
-class host(topic_common.atest):
- """Host class
- atest host [create|delete|list|stat|mod|jobs|rename|migrate] <options>"""
- usage_action = '[create|delete|list|stat|mod|jobs|rename|migrate]'
- 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.')
-
- self.lock = options.lock
- self.unlock = options.unlock
- self.lock_reason = options.lock_reason
-
- 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). When --skylab is provided, '
- 'a label must be in the format of '
- 'label-key:label-value (e.g., board:lumpy).')
- 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. %s' %
- skylab_utils.MSG_INVALID_IN_SKYLAB))
- self.parser.add_option('-u', '--user',
- default='',
- help=('Only list hosts available to this user. '
- '%s' % skylab_utils.MSG_INVALID_IN_SKYLAB))
- 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')
- self.parser.add_option('--full-output',
- default=False,
- help=('Print out the full content of the hosts. '
- 'Only supported with --skylab.'),
- action='store_true',
- dest='full_output')
-
- self.add_skylab_options()
-
-
- 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
- self.label_map = None
-
- if self.skylab:
- if options.user or options.acl or options.status:
- self.invalid_syntax('--user, --acl or --status is not '
- 'supported with --skylab.')
- self.full_output = options.full_output
- if self.full_output and self.hostnames_only:
- self.invalid_syntax('--full-output is conflicted with '
- '--hostnames-only.')
-
- if self.labels:
- self.label_map = device.convert_to_label_map(self.labels)
- else:
- if options.full_output:
- self.invalid_syntax('--full_output is only supported with '
- '--skylab.')
-
- return (options, leftover)
-
-
- def execute_skylab(self):
- """Execute 'atest host list' with --skylab."""
- inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
- inventory_repo.initialize()
- lab = text_manager.load_lab(inventory_repo.get_data_dir())
-
- # TODO(nxia): support filtering on run-time labels and status.
- return device.get_devices(
- lab,
- 'duts',
- self.environment,
- label_map=self.label_map,
- hostnames=self.hosts,
- locked=self.locked,
- unlocked=self.unlocked)
-
-
- def execute(self):
- """Execute 'atest host list'."""
- if self.skylab:
- return self.execute_skylab()
-
- 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):
- """Print output of 'atest host list'.
-
- @param results: the results to be printed.
- """
- if results and not self.skylab:
- # Remove the platform from the labels.
- for result in results:
- result['labels'] = self._cleanup_labels(result['labels'],
- result['platform'])
- if self.skylab and self.full_output:
- print(results)
- return
-
- if self.skylab:
- results = device.convert_to_autotest_hosts(results)
-
- if self.hostnames_only:
- self.print_list(results, key='hostname')
- else:
- keys = ['hostname', 'status', 'shard', 'locked', 'lock_reason',
- 'locked_by', '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):
- """Execute 'atest host stat'."""
- 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):
- """Print output of 'atest host stat'.
-
- @param results: the results to be printed.
- """
- for stats, acls, labels, attributes in results:
- print('-'*5)
- self.print_fields(stats,
- keys=['hostname', 'id', '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_get_migration_plan(host_stat):
- """atest host get_migration_plan --mlist <file>|<hosts>"""
- usage_action = "get_migration_plan"
-
- def __init__(self):
- super(host_get_migration_plan, self).__init__()
- self.parser.add_option("--ratio", default=0.5, type=float, dest="ratio")
- self.add_skylab_options()
-
- def parse(self):
- (options, leftover) = super(host_get_migration_plan, self).parse()
- self.ratio = options.ratio
- return (options, leftover)
-
- def execute(self):
- afe = frontend.AFE()
- results = super(host_get_migration_plan, self).execute()
- working = []
- non_working = []
- for stats, _, _, _ in results:
- assert len(stats) == 1
- stats = stats[0]
- hostname = stats["hostname"]
- now = time.time()
- history = HostJobHistory.get_host_history(
- afe=afe,
- hostname=hostname,
- start_time=now,
- end_time=now - 24 * 60 * 60,
- )
- dut_status, _ = history.last_diagnosis()
- if dut_status in [UNUSED, WORKING]:
- working.append(hostname)
- elif dut_status == BROKEN:
- non_working.append(hostname)
- elif dut_status == UNKNOWN:
- # if it's unknown, randomly assign it to working or
- # nonworking, since we don't know.
- # The two choices aren't actually equiprobable, but it
- # should be fine.
- random.choice([working, non_working]).append(hostname)
- else:
- raise ValueError("unknown status %s" % dut_status)
- working_transfer, working_retain = fair_partition.partition(working, self.ratio)
- non_working_transfer, non_working_retain = \
- fair_partition.partition(non_working, self.ratio)
- return {
- "transfer": working_transfer + non_working_transfer,
- "retain": working_retain + non_working_retain,
- }
-
- def output(self, results):
- print(json.dumps(results, indent=4, sort_keys=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):
- """Execute 'atest host jobs'."""
- 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):
- """Print output of 'atest host jobs'.
-
- @param results: the results to be printed.
- """
- 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 BaseHostModCreate(host):
- """The base class for host_mod and host_create"""
- # Matches one attribute=value pair
- attribute_regex = r'(?P<attribute>\w+)=(?P<value>.+)?'
-
- def __init__(self):
- """Add the options shared between host mod and host create actions."""
- self.messages = []
- self.host_ids = {}
- super(BaseHostModCreate, self).__init__()
- self.parser.add_option('-l', '--lock',
- help='Lock hosts.',
- action='store_true')
- self.parser.add_option('-r', '--lock_reason',
- help='Reason for locking hosts.',
- default='')
- self.parser.add_option('-u', '--unlock',
- help='Unlock hosts.',
- action='store_true')
-
- self.parser.add_option('-p', '--protection', type='choice',
- help=('Set the protection level on a host. '
- 'Must be one of: %s. %s' %
- (', '.join('"%s"' % p
- for p in self.protections),
- skylab_utils.MSG_INVALID_IN_SKYLAB)),
- choices=self.protections)
- self._attributes = []
- self.parser.add_option('--attribute', '-i',
- help=('Host attribute to add or change. Format '
- 'is <attribute>=<value>. Multiple '
- 'attributes can be set by passing the '
- 'argument multiple times. Attributes can '
- 'be unset by providing an empty value.'),
- action='append')
- self.parser.add_option('-b', '--labels',
- help=('Comma separated list of labels. '
- 'When --skylab is provided, a label must '
- 'be in the format of label-key:label-value'
- ' (e.g., board:lumpy).'))
- 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. %s' %
- skylab_utils.MSG_INVALID_IN_SKYLAB))
- self.parser.add_option('-A', '--alist',
- help=('File listing the acls. %s' %
- skylab_utils.MSG_INVALID_IN_SKYLAB),
- type='string',
- metavar='ACL_FLIST')
- self.parser.add_option('-t', '--platform',
- help=('Sets the platform label. %s Please set '
- 'platform in labels (e.g., -b '
- 'platform:platform_name) with --skylab.' %
- skylab_utils.MSG_INVALID_IN_SKYLAB))
-
-
- def parse(self):
- """Consume the options common to host create and host mod.
- """
- 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(BaseHostModCreate, self).parse([label_info,
- acl_info],
- req_items='hosts')
-
- self._parse_lock_options(options)
-
- self.label_map = None
- if self.allow_skylab and self.skylab:
- # TODO(nxia): drop these flags when all hosts are migrated to skylab
- if (options.protection or options.acls or options.alist or
- options.platform):
- self.invalid_syntax(
- '--protection, --acls, --alist or --platform is not '
- 'supported with --skylab.')
-
- if self.labels:
- self.label_map = device.convert_to_label_map(self.labels)
-
- if options.protection:
- self.data['protection'] = options.protection
- self.messages.append('Protection set to "%s"' % options.protection)
-
- self.attributes = {}
- if options.attribute:
- for pair in options.attribute:
- m = re.match(self.attribute_regex, pair)
- if not m:
- raise topic_common.CliError('Attribute must be in key=value '
- 'syntax.')
- elif m.group('attribute') in self.attributes:
- raise topic_common.CliError(
- 'Multiple values provided for attribute '
- '%s.' % m.group('attribute'))
- self.attributes[m.group('attribute')] = m.group('value')
-
- self.platform = options.platform
- return (options, leftover)
-
-
- def _set_acls(self, hosts, acls):
- """Add hosts to acls (and remove from all other acls).
-
- @param hosts: list of hostnames
- @param acls: list of acl names
- """
- # Remove from all ACLs except 'Everyone' and ACLs in list
- # Skip hosts that don't exist
- for host in hosts:
- if host not in self.host_ids:
- continue
- host_id = self.host_ids[host]
- for a in self.execute_rpc('get_acl_groups', hosts=host_id):
- if a['name'] not in self.acls and a['id'] != 1:
- self.execute_rpc('acl_group_remove_hosts', id=a['id'],
- hosts=self.hosts)
-
- # Add hosts to the ACLs
- self.check_and_create_items('get_acl_groups', 'add_acl_group',
- self.acls)
- for a in acls:
- self.execute_rpc('acl_group_add_hosts', id=a, hosts=hosts)
-
-
- def _remove_labels(self, host, condition):
- """Remove all labels from host that meet condition(label).
-
- @param host: hostname
- @param condition: callable that returns bool when given a label
- """
- if host in self.host_ids:
- host_id = self.host_ids[host]
- labels_to_remove = []
- for l in self.execute_rpc('get_labels', host=host_id):
- if condition(l):
- labels_to_remove.append(l['id'])
- if labels_to_remove:
- self.execute_rpc('host_remove_labels', id=host_id,
- labels=labels_to_remove)
-
-
- def _set_labels(self, host, labels):
- """Apply labels to host (and remove all other labels).
-
- @param host: hostname
- @param labels: list of label names
- """
- condition = lambda l: l['name'] not in labels and not l['platform']
- self._remove_labels(host, condition)
- self.check_and_create_items('get_labels', 'add_label', labels)
- self.execute_rpc('host_add_labels', id=host, labels=labels)
-
-
- def _set_platform_label(self, host, platform_label):
- """Apply the platform label to host (and remove existing).
-
- @param host: hostname
- @param platform_label: platform label's name
- """
- self._remove_labels(host, lambda l: l['platform'])
- self.check_and_create_items('get_labels', 'add_label', [platform_label],
- platform=True)
- self.execute_rpc('host_add_labels', id=host, labels=[platform_label])
-
-
- def _set_attributes(self, host, attributes):
- """Set attributes on host.
-
- @param host: hostname
- @param attributes: attribute dictionary
- """
- for attr, value in self.attributes.iteritems():
- self.execute_rpc('set_host_attribute', attribute=attr,
- value=value, hostname=host)
-
-
-class host_mod(BaseHostModCreate):
- """atest host mod [--lock|--unlock --force_modify_locking
- --platform <arch>
- --labels <labels>|--blist <label_file>
- --acls <acls>|--alist <acl_file>
- --protection <protection_type>
- --attributes <attr>=<value>;<attr>=<value>
- --mlist <mach_file>] <hosts>"""
- usage_action = 'mod'
-
- def __init__(self):
- """Add the options specific to the mod action"""
- super(host_mod, self).__init__()
- self.parser.add_option('--unlock-lock-id',
- help=('Unlock the lock with the lock-id. %s' %
- skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
- default=None)
- self.parser.add_option('-f', '--force_modify_locking',
- help=r'Forcefully lock\unlock a host',
- action='store_true')
- self.parser.add_option('--remove_acls',
- help=('Remove all active acls. %s' %
- skylab_utils.MSG_INVALID_IN_SKYLAB),
- action='store_true')
- self.parser.add_option('--remove_labels',
- help='Remove all labels.',
- action='store_true')
-
- self.add_skylab_options()
- self.parser.add_option('--new-env',
- dest='new_env',
- choices=['staging', 'prod'],
- help=('The new environment ("staging" or '
- '"prod") of the hosts. %s' %
- skylab_utils.MSG_ONLY_VALID_IN_SKYLAB),
- default=None)
-
-
- def _parse_unlock_options(self, options):
- """Parse unlock related options."""
- if self.skylab and options.unlock and options.unlock_lock_id is None:
- self.invalid_syntax('Must provide --unlock-lock-id with "--skylab '
- '--unlock".')
-
- if (not (self.skylab and options.unlock) and
- options.unlock_lock_id is not None):
- self.invalid_syntax('--unlock-lock-id is only valid with '
- '"--skylab --unlock".')
-
- self.unlock_lock_id = options.unlock_lock_id
-
-
- def parse(self):
- """Consume the specific options"""
- (options, leftover) = super(host_mod, self).parse()
-
- self._parse_unlock_options(options)
-
- if options.force_modify_locking:
- self.data['force_modify_locking'] = True
-
- if self.skylab and options.remove_acls:
- # TODO(nxia): drop the flag when all hosts are migrated to skylab
- self.invalid_syntax('--remove_acls is not supported with --skylab.')
-
- self.remove_acls = options.remove_acls
- self.remove_labels = options.remove_labels
- self.new_env = options.new_env
-
- return (options, leftover)
-
-
- def execute_skylab(self):
- """Execute atest host mod with --skylab.
-
- @return A list of hostnames which have been successfully modified.
- """
- inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
- inventory_repo.initialize()
- data_dir = inventory_repo.get_data_dir()
- lab = text_manager.load_lab(data_dir)
-
- locked_by = None
- if self.lock:
- locked_by = inventory_repo.git_repo.config('user.email')
-
- successes = []
- for hostname in self.hosts:
- try:
- device.modify(
- lab,
- 'duts',
- hostname,
- self.environment,
- lock=self.lock,
- locked_by=locked_by,
- lock_reason = self.lock_reason,
- unlock=self.unlock,
- unlock_lock_id=self.unlock_lock_id,
- attributes=self.attributes,
- remove_labels=self.remove_labels,
- label_map=self.label_map,
- new_env=self.new_env)
- successes.append(hostname)
- except device.SkylabDeviceActionError as e:
- print('Cannot modify host %s: %s' % (hostname, e))
-
- if successes:
- text_manager.dump_lab(data_dir, lab)
-
- status = inventory_repo.git_repo.status()
- if not status:
- print('Nothing is changed for hosts %s.' % successes)
- return []
-
- message = skylab_utils.construct_commit_message(
- 'Modify %d hosts.\n\n%s' % (len(successes), successes))
- self.change_number = inventory_repo.upload_change(
- message, draft=self.draft, dryrun=self.dryrun,
- submit=self.submit)
-
- return successes
-
-
- def execute(self):
- """Execute 'atest host mod'."""
- if self.skylab:
- return self.execute_skylab()
-
- successes = []
- for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
- self.host_ids[host['hostname']] = host['id']
- for host in self.hosts:
- if host not in self.host_ids:
- self.failure('Cannot modify non-existant host %s.' % host)
- continue
- host_id = self.host_ids[host]
-
- try:
- if self.data:
- self.execute_rpc('modify_host', item=host,
- id=host, **self.data)
-
- if self.attributes:
- self._set_attributes(host, self.attributes)
-
- if self.labels or self.remove_labels:
- self._set_labels(host, self.labels)
-
- if self.platform:
- self._set_platform_label(host, self.platform)
-
- # TODO: Make the AFE return True or False,
- # especially for lock
- successes.append(host)
- except topic_common.CliError as full_error:
- # Already logged by execute_rpc()
- pass
-
- if self.acls or self.remove_acls:
- self._set_acls(self.hosts, self.acls)
-
- return successes
-
-
- def output(self, hosts):
- """Print output of 'atest host mod'.
-
- @param hosts: the host list to be printed.
- """
- for msg in self.messages:
- self.print_wrapped(msg, hosts)
-
- if hosts and self.skylab:
- print('Modified hosts: %s.' % ', '.join(hosts))
- if self.skylab and not self.dryrun and not self.submit:
- print(skylab_utils.get_cl_message(self.change_number))
-
-
-class HostInfo(object):
- """Store host information so we don't have to keep looking it up."""
- def __init__(self, hostname, platform, labels):
- self.hostname = hostname
- self.platform = platform
- self.labels = labels
-
-
-class host_create(BaseHostModCreate):
- """atest host create [--lock|--unlock --platform <arch>
- --labels <labels>|--blist <label_file>
- --acls <acls>|--alist <acl_file>
- --protection <protection_type>
- --attributes <attr>=<value>;<attr>=<value>
- --mlist <mach_file>] <hosts>"""
- usage_action = 'create'
-
- def parse(self):
- """Option logic specific to create action.
- """
- (options, leftovers) = super(host_create, self).parse()
- self.locked = options.lock
- if 'serials' in self.attributes:
- if len(self.hosts) > 1:
- raise topic_common.CliError('Can not specify serials with '
- 'multiple hosts.')
-
-
- @classmethod
- def construct_without_parse(
- cls, web_server, hosts, platform=None,
- locked=False, lock_reason='', labels=[], acls=[],
- protection=host_protections.Protection.NO_PROTECTION):
- """Construct a host_create object and fill in data from args.
-
- Do not need to call parse after the construction.
-
- Return an object of site_host_create ready to execute.
-
- @param web_server: A string specifies the autotest webserver url.
- It is needed to setup comm to make rpc.
- @param hosts: A list of hostnames as strings.
- @param platform: A string or None.
- @param locked: A boolean.
- @param lock_reason: A string.
- @param labels: A list of labels as strings.
- @param acls: A list of acls as strings.
- @param protection: An enum defined in host_protections.
- """
- obj = cls()
- obj.web_server = web_server
- try:
- # Setup stuff needed for afe comm.
- obj.afe = rpc.afe_comm(web_server)
- except rpc.AuthError as s:
- obj.failure(str(s), fatal=True)
- obj.hosts = hosts
- obj.platform = platform
- obj.locked = locked
- if locked and lock_reason.strip():
- obj.data['lock_reason'] = lock_reason.strip()
- obj.labels = labels
- obj.acls = acls
- if protection:
- obj.data['protection'] = protection
- obj.attributes = {}
- return obj
-
-
- def _detect_host_info(self, host):
- """Detect platform and labels from the host.
-
- @param host: hostname
-
- @return: HostInfo object
- """
- # Mock an afe_host object so that the host is constructed as if the
- # data was already in afe
- data = {'attributes': self.attributes, 'labels': self.labels}
- afe_host = frontend.Host(None, data)
- store = host_info.InMemoryHostInfoStore(
- host_info.HostInfo(labels=self.labels,
- attributes=self.attributes))
- machine = {
- 'hostname': host,
- 'afe_host': afe_host,
- 'host_info_store': store
- }
- try:
- if bin_utils.ping(host, tries=1, deadline=1) == 0:
- serials = self.attributes.get('serials', '').split(',')
- adb_serial = self.attributes.get('serials')
- host_dut = hosts.create_host(machine,
- adb_serial=adb_serial)
-
- info = HostInfo(host, host_dut.get_platform(),
- host_dut.get_labels())
- # Clean host to make sure nothing left after calling it,
- # e.g. tunnels.
- if hasattr(host_dut, 'close'):
- host_dut.close()
- else:
- # Can't ping the host, use default information.
- info = HostInfo(host, None, [])
- except (socket.gaierror, error.AutoservRunError,
- error.AutoservSSHTimeout):
- # We may be adding a host that does not exist yet or we can't
- # reach due to hostname/address issues or if the host is down.
- info = HostInfo(host, None, [])
- return info
-
-
- 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.
- 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)
-
- # If there are labels avaliable for host, use them.
- info = self._detect_host_info(host)
- labels = set(self.labels)
- if info.labels:
- labels.update(info.labels)
-
- if labels:
- self._set_labels(host, list(labels))
-
- # Now add the platform label.
- # If a platform was not provided and we were able to retrieve it
- # from the host, use the retrieved platform.
- platform = self.platform if self.platform else info.platform
- if platform:
- self._set_platform_label(host, platform)
-
- if self.attributes:
- self._set_attributes(host, self.attributes)
-
-
- def execute(self):
- """Execute 'atest host create'."""
- successful_hosts = []
- for host in self.hosts:
- try:
- self._execute_add_one_host(host)
- successful_hosts.append(host)
- except topic_common.CliError:
- pass
-
- if successful_hosts:
- self._set_acls(successful_hosts, self.acls)
-
- if not self.locked:
- for host in successful_hosts:
- self.execute_rpc('modify_host', id=host, locked=False,
- lock_reason='')
- return successful_hosts
-
-
- def output(self, hosts):
- """Print output of 'atest host create'.
-
- @param hosts: the added host list to be printed.
- """
- self.print_wrapped('Added host', hosts)
-
-
-class host_delete(action_common.atest_delete, host):
- """atest host delete [--mlist <mach_file>] <hosts>"""
-
- def __init__(self):
- super(host_delete, self).__init__()
-
- self.add_skylab_options()
-
-
- def execute_skylab(self):
- """Execute 'atest host delete' with '--skylab'.
-
- @return A list of hostnames which have been successfully deleted.
- """
- inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
- inventory_repo.initialize()
- data_dir = inventory_repo.get_data_dir()
- lab = text_manager.load_lab(data_dir)
-
- successes = []
- for hostname in self.hosts:
- try:
- device.delete(
- lab,
- 'duts',
- hostname,
- self.environment)
- successes.append(hostname)
- except device.SkylabDeviceActionError as e:
- print('Cannot delete host %s: %s' % (hostname, e))
-
- if successes:
- text_manager.dump_lab(data_dir, lab)
- message = skylab_utils.construct_commit_message(
- 'Delete %d hosts.\n\n%s' % (len(successes), successes))
- self.change_number = inventory_repo.upload_change(
- message, draft=self.draft, dryrun=self.dryrun,
- submit=self.submit)
-
- return successes
-
-
- def execute(self):
- """Execute 'atest host delete'.
-
- @return A list of hostnames which have been successfully deleted.
- """
- if self.skylab:
- return self.execute_skylab()
-
- return super(host_delete, self).execute()
-
-
-class InvalidHostnameError(Exception):
- """Cannot perform actions on the host because of invalid hostname."""
-
-
-def _add_hostname_suffix(hostname, suffix):
- """Add the suffix to the hostname."""
- if hostname.endswith(suffix):
- raise InvalidHostnameError(
- 'Cannot add "%s" as it already contains the suffix.' % suffix)
-
- return hostname + suffix
-
-
-def _remove_hostname_suffix_if_present(hostname, suffix):
- """Remove the suffix from the hostname."""
- if hostname.endswith(suffix):
- return hostname[:len(hostname) - len(suffix)]
- else:
- return hostname
-
-
-class host_rename(host):
- """Host rename is only for migrating hosts between skylab and AFE DB."""
-
- usage_action = 'rename'
-
- def __init__(self):
- """Add the options specific to the rename action."""
- super(host_rename, self).__init__()
-
- self.parser.add_option('--for-migration',
- help=('Rename hostnames for migration. Rename '
- 'each "hostname" to "hostname%s". '
- 'The original "hostname" must not contain '
- 'suffix.' % MIGRATED_HOST_SUFFIX),
- action='store_true',
- default=False)
- self.parser.add_option('--for-rollback',
- help=('Rename hostnames for migration rollback. '
- 'Rename each "hostname%s" to its original '
- '"hostname".' % MIGRATED_HOST_SUFFIX),
- action='store_true',
- default=False)
- self.parser.add_option('--dryrun',
- help='Execute the action as a dryrun.',
- action='store_true',
- default=False)
- self.parser.add_option('--non-interactive',
- help='run non-interactively',
- action='store_true',
- default=False)
-
-
- def parse(self):
- """Consume the options common to host rename."""
- (options, leftovers) = super(host_rename, self).parse()
- self.for_migration = options.for_migration
- self.for_rollback = options.for_rollback
- self.dryrun = options.dryrun
- self.interactive = not options.non_interactive
- self.host_ids = {}
-
- if not (self.for_migration ^ self.for_rollback):
- self.invalid_syntax('--for-migration and --for-rollback are '
- 'exclusive, and one of them must be enabled.')
-
- if not self.hosts:
- self.invalid_syntax('Must provide hostname(s).')
-
- if self.dryrun:
- print('This will be a dryrun and will not rename hostnames.')
-
- return (options, leftovers)
-
-
- def execute(self):
- """Execute 'atest host rename'."""
- if self.interactive:
- if self.prompt_confirmation():
- pass
- else:
- return
-
- successes = []
- for host in self.execute_rpc('get_hosts', hostname__in=self.hosts):
- self.host_ids[host['hostname']] = host['id']
- for host in self.hosts:
- if host not in self.host_ids:
- self.failure('Cannot rename non-existant host %s.' % host,
- item=host, what_failed='Failed to rename')
- continue
- try:
- host_id = self.host_ids[host]
- if self.for_migration:
- new_hostname = _add_hostname_suffix(
- host, MIGRATED_HOST_SUFFIX)
- else:
- #for_rollback
- new_hostname = _remove_hostname_suffix_if_present(
- host, MIGRATED_HOST_SUFFIX)
-
- if not self.dryrun:
- # TODO(crbug.com/850737): delete and abort HQE.
- data = {'hostname': new_hostname}
- self.execute_rpc('modify_host', item=host, id=host_id,
- **data)
- successes.append((host, new_hostname))
- except InvalidHostnameError as e:
- self.failure('Cannot rename host %s: %s' % (host, e), item=host,
- what_failed='Failed to rename')
- except topic_common.CliError as full_error:
- # Already logged by execute_rpc()
- pass
-
- return successes
-
-
- def output(self, results):
- """Print output of 'atest host rename'."""
- if results:
- print('Successfully renamed:')
- for old_hostname, new_hostname in results:
- print('%s to %s' % (old_hostname, new_hostname))
-
-
-class host_migrate(action_common.atest_list, host):
- """'atest host migrate' to migrate or rollback hosts."""
-
- usage_action = 'migrate'
-
- def __init__(self):
- super(host_migrate, self).__init__()
-
- self.parser.add_option('--migration',
- dest='migration',
- help='Migrate the hosts to skylab.',
- action='store_true',
- default=False)
- self.parser.add_option('--rollback',
- dest='rollback',
- help='Rollback the hosts migrated to skylab.',
- action='store_true',
- default=False)
- self.parser.add_option('--model',
- help='Model of the hosts to migrate.',
- dest='model',
- default=None)
- self.parser.add_option('--board',
- help='Board of the hosts to migrate.',
- dest='board',
- default=None)
- self.parser.add_option('--pool',
- help=('Pool of the hosts to migrate. Must '
- 'specify --model for the pool.'),
- dest='pool',
- default=None)
-
- self.add_skylab_options(enforce_skylab=True)
-
-
- def parse(self):
- """Consume the specific options"""
- (options, leftover) = super(host_migrate, self).parse()
-
- self.migration = options.migration
- self.rollback = options.rollback
- self.model = options.model
- self.pool = options.pool
- self.board = options.board
- self.host_ids = {}
-
- if not (self.migration ^ self.rollback):
- self.invalid_syntax('--migration and --rollback are exclusive, '
- 'and one of them must be enabled.')
-
- if self.pool is not None and (self.model is None and
- self.board is None):
- self.invalid_syntax('Must provide --model or --board with --pool.')
-
- if not self.hosts and not (self.model or self.board):
- self.invalid_syntax('Must provide hosts or --model or --board.')
-
- return (options, leftover)
-
-
- def _remove_invalid_hostnames(self, hostnames, log_failure=False):
- """Remove hostnames with MIGRATED_HOST_SUFFIX.
-
- @param hostnames: A list of hostnames.
- @param log_failure: Bool indicating whether to log invalid hostsnames.
-
- @return A list of valid hostnames.
- """
- invalid_hostnames = set()
- for hostname in hostnames:
- if hostname.endswith(MIGRATED_HOST_SUFFIX):
- if log_failure:
- self.failure('Cannot migrate host with suffix "%s" %s.' %
- (MIGRATED_HOST_SUFFIX, hostname),
- item=hostname, what_failed='Failed to rename')
- invalid_hostnames.add(hostname)
-
- hostnames = list(set(hostnames) - invalid_hostnames)
-
- return hostnames
-
-
- def execute(self):
- """Execute 'atest host migrate'."""
- hostnames = self._remove_invalid_hostnames(self.hosts, log_failure=True)
-
- filters = {}
- check_results = {}
- if hostnames:
- check_results['hostname__in'] = 'hostname'
- if self.migration:
- filters['hostname__in'] = hostnames
- else:
- # rollback
- hostnames_with_suffix = [
- _add_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
- for h in hostnames]
- filters['hostname__in'] = hostnames_with_suffix
- else:
- # TODO(nxia): add exclude_filter {'hostname__endswith':
- # MIGRATED_HOST_SUFFIX} for --migration
- if self.rollback:
- filters['hostname__endswith'] = MIGRATED_HOST_SUFFIX
-
- labels = []
- if self.model:
- labels.append('model:%s' % self.model)
- if self.pool:
- labels.append('pool:%s' % self.pool)
- if self.board:
- labels.append('board:%s' % self.board)
-
- if labels:
- if len(labels) == 1:
- filters['labels__name__in'] = labels
- check_results['labels__name__in'] = None
- else:
- filters['multiple_labels'] = labels
- check_results['multiple_labels'] = None
-
- results = super(host_migrate, self).execute(
- op='get_hosts', filters=filters, check_results=check_results)
- hostnames = [h['hostname'] for h in results]
-
- if self.migration:
- hostnames = self._remove_invalid_hostnames(hostnames)
- else:
- # rollback
- hostnames = [_remove_hostname_suffix(h, MIGRATED_HOST_SUFFIX)
- for h in hostnames]
-
- return self.execute_skylab_migration(hostnames)
-
-
- def assign_duts_to_drone(self, infra, devices, environment):
- """Assign uids of the devices to a random skylab drone.
-
- @param infra: An instance of lab_pb2.Infrastructure.
- @param devices: A list of device_pb2.Device to be assigned to the drone.
- @param environment: 'staging' or 'prod'.
- """
- skylab_drones = skylab_server.get_servers(
- infra, environment, role='skylab_drone', status='primary')
-
- if len(skylab_drones) == 0:
- raise device.SkylabDeviceActionError(
- 'No skylab drone is found in primary status and staging '
- 'environment. Please confirm there is at least one valid skylab'
- ' drone added in skylab inventory.')
-
- for device in devices:
- # Randomly distribute each device to a skylab_drone.
- skylab_drone = random.choice(skylab_drones)
- skylab_server.add_dut_uids(skylab_drone, [device])
-
-
- def remove_duts_from_drone(self, infra, devices):
- """Remove uids of the devices from their skylab drones.
-
- @param infra: An instance of lab_pb2.Infrastructure.
- @devices: A list of device_pb2.Device to be remove from the drone.
- """
- skylab_drones = skylab_server.get_servers(
- infra, 'staging', role='skylab_drone', status='primary')
-
- for skylab_drone in skylab_drones:
- skylab_server.remove_dut_uids(skylab_drone, devices)
-
-
- def execute_skylab_migration(self, hostnames):
- """Execute migration in skylab_inventory.
-
- @param hostnames: A list of hostnames to migrate.
- @return If there're hosts to migrate, return a list of the hostnames and
- a message instructing actions after the migration; else return
- None.
- """
- if not hostnames:
- return
-
- inventory_repo = skylab_utils.InventoryRepo(self.inventory_repo_dir)
- inventory_repo.initialize()
-
- subdirs = ['skylab', 'prod', 'staging']
- data_dirs = skylab_data_dir, prod_data_dir, staging_data_dir = [
- inventory_repo.get_data_dir(data_subdir=d) for d in subdirs]
- skylab_lab, prod_lab, staging_lab = [
- text_manager.load_lab(d) for d in data_dirs]
- infra = text_manager.load_infrastructure(skylab_data_dir)
-
- label_map = None
- labels = []
- if self.board:
- labels.append('board:%s' % self.board)
- if self.model:
- labels.append('model:%s' % self.model)
- if self.pool:
- labels.append('critical_pool:%s' % self.pool)
- if labels:
- label_map = device.convert_to_label_map(labels)
-
- if self.migration:
- prod_devices = device.move_devices(
- prod_lab, skylab_lab, 'duts', label_map=label_map,
- hostnames=hostnames)
- staging_devices = device.move_devices(
- staging_lab, skylab_lab, 'duts', label_map=label_map,
- hostnames=hostnames)
-
- all_devices = prod_devices + staging_devices
- # Hostnames in afe_hosts tabel.
- device_hostnames = [str(d.common.hostname) for d in all_devices]
- message = (
- 'Migration: move %s hosts into skylab_inventory.\n\n'
- 'Please run this command after the CL is submitted:\n'
- 'atest host rename --for-migration %s' %
- (len(all_devices), ' '.join(device_hostnames)))
-
- self.assign_duts_to_drone(infra, prod_devices, 'prod')
- self.assign_duts_to_drone(infra, staging_devices, 'staging')
- else:
- # rollback
- prod_devices = device.move_devices(
- skylab_lab, prod_lab, 'duts', environment='prod',
- label_map=label_map, hostnames=hostnames)
- staging_devices = device.move_devices(
- skylab_lab, staging_lab, 'duts', environment='staging',
- label_map=label_map, hostnames=hostnames)
-
- all_devices = prod_devices + staging_devices
- # Hostnames in afe_hosts tabel.
- device_hostnames = [_add_hostname_suffix(str(d.common.hostname),
- MIGRATED_HOST_SUFFIX)
- for d in all_devices]
- message = (
- 'Rollback: remove %s hosts from skylab_inventory.\n\n'
- 'Please run this command after the CL is submitted:\n'
- 'atest host rename --for-rollback %s' %
- (len(all_devices), ' '.join(device_hostnames)))
-
- self.remove_duts_from_drone(infra, all_devices)
-
- if all_devices:
- text_manager.dump_infrastructure(skylab_data_dir, infra)
-
- if prod_devices:
- text_manager.dump_lab(prod_data_dir, prod_lab)
-
- if staging_devices:
- text_manager.dump_lab(staging_data_dir, staging_lab)
-
- text_manager.dump_lab(skylab_data_dir, skylab_lab)
-
- self.change_number = inventory_repo.upload_change(
- message, draft=self.draft, dryrun=self.dryrun,
- submit=self.submit)
-
- return all_devices, message
-
-
- def output(self, result):
- """Print output of 'atest host list'.
-
- @param result: the result to be printed.
- """
- if result:
- devices, message = result
-
- if devices:
- hostnames = [h.common.hostname for h in devices]
- if self.migration:
- print('Migrating hosts: %s' % ','.join(hostnames))
- else:
- # rollback
- print('Rolling back hosts: %s' % ','.join(hostnames))
-
- if not self.dryrun:
- if not self.submit:
- print(skylab_utils.get_cl_message(self.change_number))
- else:
- # Print the instruction command for renaming hosts.
- print('%s' % message)
- else:
- print('No hosts were migrated.')
diff --git a/cli/topic_common.py b/cli/topic_common.py
index 9604dc0..c85a2e3 100644
--- a/cli/topic_common.py
+++ b/cli/topic_common.py
@@ -10,9 +10,9 @@
operations.
The class inheritance is shown here using the command
-'atest host create ...' as an example:
+'atest server list ...' as an example:
-atest <-- host <-- host_create <-- site_host_create
+atest <-- server <-- server_list
Note: The site_<topic>.py and its classes are only needed if you need
to override the common <topic>.py methods with your site specific ones.
@@ -274,7 +274,7 @@
Should only be instantiated by itself for usage
references, otherwise, the <topic> objects should
be used."""
- msg_topic = '[acl|host|job|label|shard|test|user|server]'
+ msg_topic = '[acl|job|label|shard|test|user|server]'
usage_action = '[action]'
msg_items = ''