blob: 69eaadf6b1683d66f57c56858e88b4f7a7bf5d4b [file] [log] [blame]
Kuang-che Wu875c89a2020-01-08 14:30:55 +08001#!/usr/bin/env python3
Kuang-che Wu6e4beca2018-06-27 17:45:02 +08002# -*- coding: utf-8 -*-
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08003# Copyright 2017 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
Kuang-che Wu68db08a2018-03-30 11:50:34 +08006"""Helper script to manipulate chromeos DUT or query info."""
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08007from __future__ import print_function
Kuang-che Wu5157dee2020-07-18 01:13:41 +08008import asyncio
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08009import argparse
10import json
11import logging
Kuang-che Wu0c9b7942019-10-30 16:55:39 +080012import random
13import time
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080014
Kuang-che Wufe1e88a2019-09-10 21:52:25 +080015from bisect_kit import cli
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080016from bisect_kit import common
Kuang-che Wuc45cfa42019-01-15 00:15:01 +080017from bisect_kit import cros_lab_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080018from bisect_kit import cros_util
Kuang-che Wu22aa9d42019-01-25 10:35:33 +080019from bisect_kit import errors
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080020
Zheng-Jie Chang17f36c82020-06-16 05:21:59 +080021DEFAULT_DUT_POOL = 'DUT_POOL_QUOTA'
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080022logger = logging.getLogger(__name__)
23
24
25def cmd_version_info(opts):
26 info = cros_util.version_info(opts.board, opts.version)
27 if opts.name:
28 if opts.name not in info:
29 logger.error('unknown name=%s', opts.name)
30 print(info[opts.name])
31 else:
32 print(json.dumps(info, sort_keys=True, indent=4))
33
34
35def cmd_query_dut_board(opts):
36 assert cros_util.is_dut(opts.dut)
37 print(cros_util.query_dut_board(opts.dut))
38
39
40def cmd_reboot(opts):
41 assert cros_util.is_dut(opts.dut)
Kuang-che Wu2ac9a922020-09-03 16:50:12 +080042 cros_util.reboot(
43 opts.dut, force_reboot_callback=cros_lab_util.reboot_via_servo)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080044
45
Kuang-che Wuc45cfa42019-01-15 00:15:01 +080046def _get_label_by_prefix(info, prefix):
47 for label in info['Labels']:
48 if label.startswith(prefix + ':'):
49 return label
50 return None
51
52
Kuang-che Wuca456462019-11-04 17:32:55 +080053def cmd_lease_dut(opts):
Kuang-che Wu5157dee2020-07-18 01:13:41 +080054 if opts.duration is not None and opts.duration < 60:
55 raise errors.ArgumentError('--duration', 'must be at least 60 seconds')
Kuang-che Wu22aa9d42019-01-25 10:35:33 +080056 host = cros_lab_util.dut_host_name(opts.dut)
Kuang-che Wu220cc162019-10-31 00:29:37 +080057 logger.info('trying to lease %s', host)
58 if cros_lab_util.skylab_lease_dut(host, opts.duration):
Kuang-che Wuca456462019-11-04 17:32:55 +080059 logger.info('leased %s', host)
Kuang-che Wu22aa9d42019-01-25 10:35:33 +080060 else:
Kuang-che Wuca456462019-11-04 17:32:55 +080061 raise Exception('unable to lease %s' % host)
Kuang-che Wu22aa9d42019-01-25 10:35:33 +080062
63
Kuang-che Wuca456462019-11-04 17:32:55 +080064def cmd_release_dut(opts):
Kuang-che Wu22aa9d42019-01-25 10:35:33 +080065 host = cros_lab_util.dut_host_name(opts.dut)
Kuang-che Wu220cc162019-10-31 00:29:37 +080066 cros_lab_util.skylab_release_dut(host)
Kuang-che Wuca456462019-11-04 17:32:55 +080067 logger.info('%s released', host)
Kuang-che Wu22aa9d42019-01-25 10:35:33 +080068
69
Kuang-che Wu1e56ce22020-06-29 11:21:51 +080070def verify_dimensions_by_lab(dimensions):
71 result = []
72 bots_dimensions = cros_lab_util.swarming_bots_dimensions()
73 for dimension in dimensions:
74 key, value = dimension.split(':', 1)
75 if value in bots_dimensions.get(key, []):
76 result.append(dimension)
77 else:
78 logger.warning('dimension=%s is unknown in the lab, typo? ignored',
79 dimension)
80 return result
81
82
Kuang-che Wu5157dee2020-07-18 01:13:41 +080083def select_available_bots_randomly(dimensions, variants, num=1, is_busy=None):
Kuang-che Wu1e56ce22020-06-29 11:21:51 +080084 bots = []
85 for variant in variants:
86 # There might be thousand bots available, set 'limit' to reduce swarming
87 # API cost. This is not uniform random, but should be good enough.
88 bots += cros_lab_util.swarming_bots_list(
89 dimensions + [variant], is_busy=is_busy, limit=10)
90 if not bots:
91 return None
Kuang-che Wu5157dee2020-07-18 01:13:41 +080092 return random.sample(bots, min(num, len(bots)))
93
94
Kuang-che Wub529d2d2020-09-10 12:26:56 +080095def filter_dimensions_by_board(boards_with_prebuilt, dimensions):
96 result = []
97 for dimension in dimensions:
98 bots = cros_lab_util.swarming_bots_list([dimension], is_busy=None, limit=1)
99 if not bots:
100 continue
101 board = bots[0]['dimensions']['label-board'][0]
102 if board not in boards_with_prebuilt:
103 logger.warning(
104 'dimension=%s (board=%s) does not have corresponding '
105 'prebuilt image, ignore', dimension, board)
106 continue
107 result.append(dimension)
108 return result
109
110
Kuang-che Wu5157dee2020-07-18 01:13:41 +0800111async def lease_dut_parallelly(duration, bots, timeout=None):
112 tasks = []
113 hosts = []
114 for bot in bots:
115 host = bot['dimensions']['dut_name'][0]
116 hosts.append(host)
117 tasks.append(
118 asyncio.create_task(cros_lab_util.async_lease(host, duration=duration)))
119
120 try:
121 logger.info('trying to lease %d DUTs: %s', len(hosts), hosts)
122 for coro in asyncio.as_completed(tasks, timeout=timeout):
123 host = await coro
124 if host:
125 logger.info('leased %s', host)
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800126 # Unfinished lease tasks will be cancelled when asyncio.run is
127 # finishing.
Kuang-che Wu5157dee2020-07-18 01:13:41 +0800128 return host
129 return None
130 except asyncio.TimeoutError:
131 return None
Kuang-che Wu1e56ce22020-06-29 11:21:51 +0800132
133
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800134def do_allocate_dut(opts):
135 """Helper of cmd_allocate_dut.
136
137 Returns:
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800138 (todo, host, board_to_build)
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800139 todo: 'ready' or 'wait'
Kuang-che Wuca456462019-11-04 17:32:55 +0800140 host: leased host name
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800141 board_to_build: board name for building image
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800142 """
Kuang-che Wu1e56ce22020-06-29 11:21:51 +0800143 if not opts.dut_name and not opts.pool:
144 raise errors.ArgumentError('--pool',
145 'need to be specified if not --dut_name')
Kuang-che Wu7e8abe62020-07-02 09:42:27 +0800146 if opts.version_hint:
147 for v in opts.version_hint.split(','):
148 if cros_util.is_cros_version(v) or cros_util.is_cros_snapshot_version(v):
149 continue
150 raise errors.ArgumentError(
151 '--version_hint',
152 'should be Chrome OS version numbers, separated by comma')
Kuang-che Wu5157dee2020-07-18 01:13:41 +0800153 if opts.duration is not None and opts.duration < 60:
154 raise errors.ArgumentError('--duration', 'must be at least 60 seconds')
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800155
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800156 t0 = time.time()
Zheng-Jie Chang17f36c82020-06-16 05:21:59 +0800157 dimensions = ['dut_state:ready']
Kuang-che Wu1e56ce22020-06-29 11:21:51 +0800158 if not opts.dut_name:
Zheng-Jie Chang17f36c82020-06-16 05:21:59 +0800159 dimensions.append('label-pool:' + opts.pool)
160
Kuang-che Wu1e56ce22020-06-29 11:21:51 +0800161 variants = []
162 if opts.board:
163 for board in opts.board.split(','):
Kuang-che Wu1e56ce22020-06-29 11:21:51 +0800164 variants.append('label-board:' +
165 cros_lab_util.normalize_board_name(board))
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800166 if opts.model:
Kuang-che Wu1e56ce22020-06-29 11:21:51 +0800167 for model in opts.model.split(','):
168 variants.append('label-model:' + model)
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800169 if opts.sku:
Kuang-che Wu1e56ce22020-06-29 11:21:51 +0800170 for sku in opts.sku.split(','):
171 variants.append('label-hwid_sku:' + cros_lab_util.normalize_sku_name(sku))
172 if opts.dut_name:
173 for dut_name in opts.dut_name.split(','):
174 variants.append('dut_name:' + dut_name)
175
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800176 variants = verify_dimensions_by_lab(variants)
177 variants = sorted(set(variants)) # dedup
178 if not variants:
179 raise errors.NoDutAvailable(
180 'Invalid constraints: %s;%s;%s;%s' %
181 (opts.board, opts.model, opts.sku, opts.dut_name))
182
183 # Filter variants by prebuilt images.
184 if opts.version_hint:
185 if not opts.builder_hint:
186 opts.builder_hint = opts.board
187 if not opts.builder_hint:
188 raise errors.ArgumentError('--builder_hint',
189 'must be specified along with --version_hint')
190 boards_with_prebuilt = []
191 versions = opts.version_hint.split(',')
192 for builder in opts.builder_hint.split(','):
193 if not all(cros_util.has_test_image(builder, v) for v in versions):
194 logger.warning(
195 'builder=%s does not have prebuilt test image for %s, ignore',
196 builder, opts.version_hint)
197 continue
198 boards_with_prebuilt.append(cros_lab_util.normalize_board_name(builder))
199 logger.info('boards with prebuilt: %s', boards_with_prebuilt)
200 if not boards_with_prebuilt:
201 raise errors.ArgumentError(
202 '--version_hint',
203 'given builders have no prebuilt for %s' % opts.version_hint)
204 variants = filter_dimensions_by_board(boards_with_prebuilt, variants)
205 if not variants:
206 raise errors.NoDutAvailable(
207 'Devices with specified constraints have no prebuilt. '
208 'Wrong version number?')
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800209
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800210 while True:
Kuang-che Wu5157dee2020-07-18 01:13:41 +0800211 # Query every time because each iteration takes a few minutes
212 bots = select_available_bots_randomly(
213 dimensions, variants, num=opts.parallel, is_busy=False)
214 if not bots:
215 bots = select_available_bots_randomly(
216 dimensions, variants, num=opts.parallel, is_busy=True)
217 if not bots:
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800218 raise errors.NoDutAvailable(
Kuang-che Wu1e56ce22020-06-29 11:21:51 +0800219 'no bots satisfy constraints; all are in maintenance state?')
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800220
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800221 remaining_time = opts.time_limit - (time.time() - t0)
222 if remaining_time <= 0:
223 break
Kuang-che Wu5157dee2020-07-18 01:13:41 +0800224 timeout = min(120, remaining_time)
225 host = asyncio.run(lease_dut_parallelly(opts.duration, bots, timeout))
226 if host:
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800227 # Resolve what board we should build during bisection.
228 board_to_build = None
229 bots = cros_lab_util.swarming_bots_list(['dut_name:' + host])
230 host_board = bots[0]['dimensions']['label-board'][0]
231 if opts.builder_hint:
232 for builder in opts.builder_hint.split(','):
233 if cros_lab_util.normalize_board_name(builder) == host_board:
234 board_to_build = builder
235 break
236 else:
237 board_to_build = host_board
238
239 return 'ready', host, board_to_build
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800240 time.sleep(1)
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800241
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800242 logger.warning('unable to lease DUT in time limit')
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800243 return 'wait', None, None
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800244
245
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800246def cmd_allocate_dut(opts):
Kuang-che Wuca456462019-11-04 17:32:55 +0800247 leased_dut = None
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800248 try:
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800249 todo, host, board = do_allocate_dut(opts)
Kuang-che Wu5157dee2020-07-18 01:13:41 +0800250 leased_dut = cros_lab_util.dut_name_to_address(host) if host else None
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800251 result = {'result': todo, 'leased_dut': leased_dut, 'board': board}
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800252 print(json.dumps(result))
253 except Exception as e:
254 logger.exception('cmd_allocate_dut failed')
255 exception_name = e.__class__.__name__
256 result = {
257 'result': 'failed',
258 'exception': exception_name,
259 'text': str(e),
260 }
261 print(json.dumps(result))
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800262
263
Kuang-che Wua8c3c3e2019-08-28 18:49:28 +0800264def cmd_repair_dut(opts):
265 cros_lab_util.repair(opts.dut)
266
267
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800268@cli.fatal_error_handler
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800269def main():
270 common.init()
271 parser = argparse.ArgumentParser()
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800272 cli.patching_argparser_exit(parser)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800273 common.add_common_arguments(parser)
274 subparsers = parser.add_subparsers(
275 dest='command', title='commands', metavar='<command>')
276
277 parser_version_info = subparsers.add_parser(
278 'version_info',
279 help='Query version info of given chromeos build',
280 description='Given chromeos `board` and `version`, '
281 'print version information of components.')
282 parser_version_info.add_argument(
283 'board', help='ChromeOS board name, like "samus".')
284 parser_version_info.add_argument(
285 'version',
286 type=cros_util.argtype_cros_version,
287 help='ChromeOS version, like "9876.0.0" or "R62-9876.0.0"')
288 parser_version_info.add_argument(
289 'name',
290 nargs='?',
291 help='Component name. If specified, output its version string. '
292 'Otherwise output all version info as dict in json format.')
293 parser_version_info.set_defaults(func=cmd_version_info)
294
295 parser_query_dut_board = subparsers.add_parser(
296 'query_dut_board', help='Query board name of given DUT')
297 parser_query_dut_board.add_argument('dut')
298 parser_query_dut_board.set_defaults(func=cmd_query_dut_board)
299
300 parser_reboot = subparsers.add_parser(
301 'reboot',
302 help='Reboot a DUT',
303 description='Reboot a DUT and verify the reboot is successful.')
304 parser_reboot.add_argument('dut')
305 parser_reboot.set_defaults(func=cmd_reboot)
306
Kuang-che Wuca456462019-11-04 17:32:55 +0800307 parser_lease_dut = subparsers.add_parser(
308 'lease_dut',
309 help='Lease a DUT in the lab',
310 description='Lease a DUT in the lab. '
311 'This is implemented by `skylab lease-dut` with additional checking.')
312 # "skylab lease-dut" doesn't take reason, so this is not required=True.
313 parser_lease_dut.add_argument('--session', help='session name')
314 parser_lease_dut.add_argument('dut')
315 parser_lease_dut.add_argument(
316 '--duration',
317 type=float,
318 help='duration in seconds; will be round to minutes')
319 parser_lease_dut.set_defaults(func=cmd_lease_dut)
320
321 parser_release_dut = subparsers.add_parser(
322 'release_dut',
323 help='Release a DUT in the lab',
324 description='Release a DUT in the lab. '
325 'This is implemented by `skylab release-dut` with additional checking.')
326 parser_release_dut.add_argument('--session', help='session name')
327 parser_release_dut.add_argument('dut')
328 parser_release_dut.set_defaults(func=cmd_release_dut)
329
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800330 parser_allocate_dut = subparsers.add_parser(
331 'allocate_dut',
332 help='Allocate a DUT in the lab',
Kuang-che Wuca456462019-11-04 17:32:55 +0800333 description='Allocate a DUT in the lab. It will lease a DUT in the lab '
334 'for bisecting. The caller (bisect-kit runner) of this command should '
335 'retry this command again later if no DUT available now.')
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800336 parser_allocate_dut.add_argument(
337 '--session', required=True, help='session name')
338 parser_allocate_dut.add_argument(
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800339 '--pool',
340 help='Pool to search DUT (default: %(default)s)',
341 default=DEFAULT_DUT_POOL)
Kuang-che Wu1e56ce22020-06-29 11:21:51 +0800342 group = parser_allocate_dut.add_mutually_exclusive_group(required=True)
343 group.add_argument('--board', help='allocation criteria; comma separated')
344 group.add_argument('--model', help='allocation criteria; comma separated')
345 group.add_argument('--sku', help='allocation criteria; comma separated')
346 group.add_argument('--dut_name', help='allocation criteria; comma separated')
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800347 parser_allocate_dut.add_argument(
Kuang-che Wu1e56ce22020-06-29 11:21:51 +0800348 '--version_hint', help='chromeos version; comma separated')
Kuang-che Wub529d2d2020-09-10 12:26:56 +0800349 parser_allocate_dut.add_argument(
350 '--builder_hint', help='chromeos builder; comma separated')
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800351 # Pubsub ack deadline is 10 minutes (b/143663659). Default 9 minutes with 1
352 # minute buffer.
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800353 parser_allocate_dut.add_argument(
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800354 '--time_limit',
355 type=int,
Kuang-che Wuc26dcdf2019-11-01 16:30:06 +0800356 default=9 * 60,
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800357 help='Time limit to attempt lease in seconds (default: %(default)s)')
358 parser_allocate_dut.add_argument(
359 '--duration',
360 type=float,
361 help='lease duration in seconds; will be round to minutes')
Kuang-che Wu5157dee2020-07-18 01:13:41 +0800362 parser_allocate_dut.add_argument(
363 '--parallel',
364 type=int,
365 default=1,
366 help='Submit multiple lease tasks to speed up (default: %(default)d)')
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800367 parser_allocate_dut.set_defaults(func=cmd_allocate_dut)
368
Kuang-che Wua8c3c3e2019-08-28 18:49:28 +0800369 parser_repair_dut = subparsers.add_parser(
370 'repair_dut',
371 help='Repair a DUT in the lab',
372 description='Repair a DUT in the lab. '
373 'This is simply wrapper of "deploy repair" with additional checking.')
374 parser_repair_dut.add_argument('dut')
375 parser_repair_dut.set_defaults(func=cmd_repair_dut)
376
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800377 opts = parser.parse_args()
378 common.config_logging(opts)
Kuang-che Wud3a4e842019-12-11 12:15:23 +0800379
380 # It's optional by default since python3.
381 if not opts.command:
382 parser.error('command is missing')
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800383 opts.func(opts)
384
385
386if __name__ == '__main__':
387 main()