atest host skylab_migrate command MVP
MVP functionality for `atest host skylab_migration` command.
`atest` post-this-CL is capable of migrating explicitly specified hosts and writes a JSON report of what it did to stdout, e.g.
```
{
"duts": {
"migrated": [
"chromeos1-row4-rack9-host2"
],
"needs_add_to_skylab": [],
"needs_drone": [],
"needs_rename": [],
"not_locked": []
},
"failed_step": null,
"locked_success": true,
"plan": {
"retain": [],
"transfer": [
"chromeos1-row4-rack9-host2"
]
}
}
```
That being said, I had to remove a couple of bugs in order to get it to work end to end... and all of those fixes are in this diff.
That's why the diff is relatively large.
Non-MVP functionality such as specifying boards and pools and models has a draft implementation here, but hasn't really been tested.
Bug: chromium:989689
Change-Id: I8b573791d266851121ab3ed0aef94ad1474f213a
Reviewed-on: https://chromium-review.googlesource.com/1728425
Tested-by: Gregory Nisbet <gregorynisbet@google.com>
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Legacy-Commit-Queue: Commit Bot <commit-bot@chromium.org>
Reviewed-by: Aviv Keshet <akeshet@chromium.org>
Reviewed-by: Gregory Nisbet <gregorynisbet@google.com>
diff --git a/cli/host.py b/cli/host.py
index ea4538c..923637b 100644
--- a/cli/host.py
+++ b/cli/host.py
@@ -26,7 +26,7 @@
import socket
import time
-from autotest_lib.cli import action_common, rpc, topic_common, skylab_utils
+from autotest_lib.cli import action_common, rpc, topic_common, skylab_utils, skylab_migration
from autotest_lib.cli import fair_partition
from autotest_lib.client.bin import utils as bin_utils
from autotest_lib.cli.skylab_json_utils import process_labels
@@ -56,8 +56,8 @@
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]'
+ atest host [create|delete|list|stat|mod|jobs|rename|migrate|skylab_migrate|statjson] <options>"""
+ usage_action = '[create|delete|list|stat|mod|jobs|rename|migrate|skylab_migrate|statjson]'
topic = msg_topic = 'host'
msg_items = '<hosts>'
@@ -1507,3 +1507,136 @@
print('%s' % message)
else:
print('No hosts were migrated.')
+
+
+class host_skylab_migrate(action_common.atest_list, host):
+ usage_action = 'skylab_migrate'
+
+ def __init__(self):
+ super(host_skylab_migrate, self).__init__()
+ self.parser.add_option('--dry-run',
+ help='Dry run. Show only candidate hosts.',
+ action='store_true',
+ dest='dry_run')
+ self.parser.add_option('--ratio',
+ help='ratio of hosts to migrate as number from 0 to 1.',
+ type=float,
+ dest='ratio',
+ default=1)
+ self.parser.add_option('--bug-number',
+ help='bug number for tracking purposes.',
+ dest='bug_number',
+ default=None)
+ self.parser.add_option('--board',
+ help='Board of the hosts to migrate',
+ dest='board',
+ default=None)
+ self.parser.add_option('--model',
+ help='Model of the hosts to migrate',
+ dest='model',
+ default=None)
+ self.parser.add_option('--pool',
+ help='Pool of the hosts to migrate',
+ dest='pool',
+ default=None)
+
+ def parse(self):
+ (options, leftover) = super(host_skylab_migrate, self).parse()
+ self.dry_run = options.dry_run
+ self.ratio = options.ratio
+ self.bug_number = options.bug_number
+ self.model = options.model
+ self.pool = options.pool
+ self.board = options.board
+ self._reason = "migration to skylab: %s" % self.bug_number
+ return (options, leftover)
+
+
+ def _host_skylab_migrate_get_hostnames(self, model=None, pool=None, board=None):
+ """
+ @params : in 'model', 'pool', 'board'
+
+ """
+ # TODO(gregorynisbet)
+ # this just gets all the hostnames, it doesn't filter by
+ # presence or absence of migrated-do-not-use.
+ labels = []
+ for key, value in {'model': model, 'board': board, 'pool': pool}:
+ if value:
+ labels.append(key + ":" + value)
+ filters = {}
+ check_results = {}
+ # Copy the filter and check_results initialization logic from
+ # the 'execute' method of the class 'host_migrate'.
+ if not labels:
+ return []
+ elif len(labels) == 1:
+ filters['labels__name__in'] = labels
+ check_results['labels__name__in'] = None
+ elif len(labels) > 1:
+ filters['multiple_labels'] = labels
+ check_results['multiple_labels'] = None
+ else:
+ assert False
+
+ results = super(host_skylab_migrate, self).execute(
+ op='get_hosts', filters=filters, check_results=check_results)
+ return [result['hostname'] for result in results]
+
+
+ def _validate_one_hostname_source(self):
+ """Validate that hostname source is explicit hostnames or valid query.
+
+ Hostnames must either be provided explicitly or be the result of a
+ query defined by 'model', 'board', and 'pool'.
+
+ @returns : whether the hostnames come from exactly one valid source.
+ """
+ has_criteria = any([(self.model and self.board), self.board, self.pool])
+ has_command_line_hosts = bool(self.hosts)
+ if has_criteria != has_command_line_hosts:
+ # all good, one data source
+ return True
+ if has_criteria and has_command_line_hosts:
+ self.failure(
+ '--model/host/board and explicit hostnames are alternatives. Provide exactly one.',
+ item='cli',
+ what_failed='user')
+ return False
+ self.failure(
+ 'no explicit hosts and no criteria provided.',
+ item='cli',
+ what_failed='user')
+ return False
+
+
+ def execute(self):
+ if not self._validate_one_hostname_source():
+ return None
+ if self.hosts:
+ hostnames = self.hosts
+ else:
+ hostnames = self.__get_hostnames(
+ model=self.model,
+ board=self.board,
+ pool=self.pool,
+ )
+ if self.dry_run:
+ return hostnames
+ if not hostnames:
+ return {'error': 'no hosts to migrate'}
+ res = skylab_migration.migrate(
+ ratio=self.ratio,
+ reason=self._reason,
+ hostnames=hostnames,
+ max_duration=10 * 60,
+ interval_len=2,
+ min_ready_intervals=10,
+ immediately=True,
+ )
+ return res
+
+
+ def output(self, result):
+ if result is not None:
+ print json.dumps(result, indent=4, sort_keys=True)