| # Copyright 2018 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Yet another domain specific client for Swarming.""" |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import json |
| import os |
| import urllib |
| |
| from lucifer import autotest |
| from skylab_staging import errors |
| |
| # This is hard-coded everywhere -- on builders requesting skylab suite as well |
| # as in commands to be generated by users requesting one off suites. |
| _SKYLAB_RUN_SUITE_PATH = '/usr/local/autotest/bin/run_suite_skylab' |
| _SWARMING_POOL_SKYLAB_BOTS = 'ChromeOSSkylab' |
| _SWARMING_POOL_SKYLAB_SUITE_BOTS = 'ChromeOSSkylab-suite' |
| # Test push creates all suites at the highest allowed non-admin task priority. |
| # This ensures that test push tasks are prioritized over any user created tasks |
| # in the staging lab. |
| _TEST_PUSH_SUITE_PRIORITY = 50 |
| |
| |
| |
| class Client(object): |
| """A domain specific client for Swarming service.""" |
| |
| def __init__(self, cli_path, host, service_account_json=None): |
| self._cli_path = cli_path |
| self._host = host |
| self._service_account_json = service_account_json |
| |
| def num_ready_duts(self, board, pool): |
| """Count the number of DUTs in the given board, pool in dut_state ready. |
| |
| @param board: The board autotest label of the DUTs. |
| @param pool: The pool autotest label of the DUTs. |
| @returns number of DUTs in dut_state ready. |
| """ |
| qargs = [ |
| ('dimensions', 'pool:%s' % _SWARMING_POOL_SKYLAB_BOTS), |
| ('dimensions', 'label-board:%s' % board), |
| ('dimensions', 'label-pool:%s' % pool), |
| ('dimensions', 'dut_state:ready'), |
| ] |
| result = self.query('bots/count', qargs) |
| if not result: |
| return 0 |
| return int(result['count']) - (int(result['busy']) |
| + int(result['dead']) |
| + int(result['quarantined']) |
| + int(result['maintenance'])) |
| |
| def query(self, path, qargs): |
| """Run a Swarming 'query' call. |
| |
| @param path: Path of the query RPC call. |
| @qargs: Arguments for the RPC call. |
| @returns: json response from the Swarming call. |
| """ |
| cros_build_lib = autotest.chromite_load('cros_build_lib') |
| cmdarg = path |
| if qargs: |
| cmdarg += "?%s" % urllib.urlencode(qargs) |
| |
| cmd = self._base_cmd('query') + [cmdarg] |
| |
| result = cros_build_lib.RunCommand(cmd, capture_output=True) |
| return json.loads(result.output) |
| |
| def trigger_suite(self, board, pool, build, suite_name, timeout_s): |
| """Trigger an autotest suite. Use wait_for_suite to wait for results. |
| |
| @param board: The board autotest label of the DUTs. |
| @param pool: The pool autotest label of the DUTs. |
| @param build: The build to test, e.g. link-paladin/R70-10915.0.0-rc1. |
| @param suite_name: The name of the suite to run, e.g. provision. |
| @param timeout_s: Timeout for the suite, in seconds. |
| @returns: The task ID of the kicked off suite. |
| """ |
| raw_cmd = self._suite_cmd_common(board, pool, build, suite_name, timeout_s) |
| raw_cmd += ['--create_and_return'] |
| return self._run(board, pool, build, suite_name, timeout_s, raw_cmd) |
| |
| def wait_for_suite(self, task_id, board, pool, build, suite_name, timeout_s): |
| """Wait for a suite previously kicked off via trigger_suite(). |
| |
| @param task_id: Task ID of the suite, as returned by trigger_suite(). |
| @param board: The board autotest label of the DUTs. |
| @param pool: The pool autotest label of the DUTs. |
| @param build: The build to test, e.g. link-paladin/R70-10915.0.0-rc1. |
| @param suite_name: The name of the suite to run, e.g. provision. |
| @param timeout_s: Timeout for the suite, in seconds. |
| @returns: The task ID of the kicked off suite. |
| """ |
| raw_cmd = self._suite_cmd_common(board, pool, build, suite_name, timeout_s) |
| raw_cmd += ['--suite_id', task_id] |
| return self._run(board, pool, build, suite_name, timeout_s, raw_cmd) |
| |
| def _suite_cmd_common(self, board, pool, build, suite_name, timeout_s): |
| return [ |
| _SKYLAB_RUN_SUITE_PATH, |
| '--board', board, |
| '--build', build, |
| '--max_retries', '5', |
| '--pool', _old_style_pool_label(pool), |
| '--priority', str(_TEST_PUSH_SUITE_PRIORITY), |
| '--suite_args', json.dumps({'num_required': 1}), |
| '--suite_name', suite_name, |
| '--test_retry', |
| '--timeout_mins', str(int(timeout_s / 60)), |
| ] |
| |
| def _run(self, board, pool, build, suite_name, timeout_s, raw_cmd): |
| timeout_s = str(int(timeout_s)) |
| task_name = '%s-%s' % (build, suite_name) |
| # This is a subset of the tags used by builders when creating suites. |
| # These tags are used by the result reporting pipeline in various ways. |
| tags = { |
| 'board': board, |
| 'build': build, |
| # Required for proper rendering of MILO UI. |
| 'luci_project': 'chromeos', |
| 'skylab': 'run_suite', |
| 'skylab': 'staging', |
| 'suite': suite_name, |
| 'task_name': task_name, |
| } |
| osutils = autotest.chromite_load('osutils') |
| with osutils.TempDir() as tempdir: |
| summary_file = os.path.join(tempdir, 'summary.json') |
| cmd = self._base_cmd('run') + [ |
| '--dimension', 'pool', _SWARMING_POOL_SKYLAB_SUITE_BOTS, |
| '--expiration', timeout_s, |
| '--io-timeout', timeout_s, |
| '--hard-timeout', timeout_s, |
| '--print-status-update', |
| '--priority', str(_TEST_PUSH_SUITE_PRIORITY), |
| '--raw-cmd', |
| '--task-name', task_name, |
| '--task-summary-json', summary_file, |
| '--timeout', timeout_s, |
| ] |
| for key, val in tags.iteritems(): |
| cmd += ['--tags', '%s:%s' % (key, val)] |
| cmd += ['--'] + raw_cmd |
| |
| cros_build_lib = autotest.chromite_load('cros_build_lib') |
| cros_build_lib.RunCommand(cmd, error_code_ok=True) |
| return _extract_run_id(summary_file) |
| |
| |
| def _base_cmd(self, subcommand): |
| cmd = [ |
| self._cli_path, subcommand, |
| '--swarming', self._host, |
| ] |
| if self._service_account_json is not None: |
| cmd += ['--auth-service-account-json', self._service_account_json] |
| return cmd |
| |
| |
| def task_url(self, task_id): |
| """Generate the task url based on task id.""" |
| return '%s/user/task/%s' % (self._host, task_id) |
| |
| |
| def _extract_run_id(path): |
| if not os.path.isfile(path): |
| raise errors.TestPushError('No task summary at %s' % path) |
| with open(path) as f: |
| summary = json.load(f) |
| if not summary.get('shards') or len(summary['shards']) != 1: |
| raise errors.TestPushError('Corrupted task summary at %s' % path) |
| run_id = summary['shards'][0].get('run_id') |
| if not run_id: |
| raise errors.TestPushError('No run_id in task summary at %s' % path) |
| return run_id |
| |
| |
| def _old_style_pool_label(label): |
| _POOL_LABEL_PREFIX = 'dut_pool_' |
| label = label.lower() |
| if label.startswith(_POOL_LABEL_PREFIX): |
| return label[len(_POOL_LABEL_PREFIX):] |
| return label |