blob: 59bce056a08adce0f775e139fee0b2f61f03140f [file] [log] [blame]
Kuang-che Wu2ea804f2017-11-28 17:11:41 +08001#!/usr/bin/env python2
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
8import argparse
Kuang-che Wuc45cfa42019-01-15 00:15:01 +08009import collections
10import functools
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080011import json
12import logging
Kuang-che Wu0c9b7942019-10-30 16:55:39 +080013import random
14import time
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080015
Kuang-che Wufe1e88a2019-09-10 21:52:25 +080016from bisect_kit import cli
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080017from bisect_kit import common
Kuang-che Wuc45cfa42019-01-15 00:15:01 +080018from bisect_kit import cros_lab_util
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080019from bisect_kit import cros_util
Kuang-che Wu22aa9d42019-01-25 10:35:33 +080020from bisect_kit import errors
Kuang-che Wu2ea804f2017-11-28 17:11:41 +080021
22logger = 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)
42 cros_util.reboot(opts.dut)
43
44
Kuang-che Wuc45cfa42019-01-15 00:15:01 +080045def _get_label_by_prefix(info, prefix):
46 for label in info['Labels']:
47 if label.startswith(prefix + ':'):
48 return label
49 return None
50
51
52def cmd_search_dut(opts):
53 labels = []
54 if opts.label:
55 labels += opts.label.split(',')
56
57 def match(info):
58 if not opts.condition:
59 return True
60
61 for label in info['Labels']:
62 for condition in opts.condition:
63 if ':' in condition:
64 keys = [condition]
65 else:
66 keys = [
67 'board:' + condition,
68 'model:' + condition,
69 'sku:' + condition,
70 ]
71 for key in keys:
72 if label.lower().startswith(key.lower()):
73 return True
74 return False
75
76 for pool in opts.pools.split(','):
77 print('pool:' + pool)
78
79 group = collections.defaultdict(dict)
80 counter = collections.defaultdict(collections.Counter)
Kuang-che Wu0768b972019-10-05 15:18:59 +080081 for host, info in cros_lab_util.list_host(
82 labels=labels + ['pool:' + pool]).items():
Kuang-che Wuc45cfa42019-01-15 00:15:01 +080083 if not match(info):
84 continue
85
86 model = _get_label_by_prefix(info, 'model')
Kuang-che Wu22aa9d42019-01-25 10:35:33 +080087 if info.get('Locked'):
Kuang-che Wuc45cfa42019-01-15 00:15:01 +080088 state = 'Locked'
89 else:
90 state = info['Status']
91 if state not in group[model]:
92 group[model][state] = {}
93 group[model][state][host] = info
94 counter[model][state] += 1
95
96 def availability(counter, model):
97 return -counter[model]['Ready']
98
99 for model in sorted(group, key=functools.partial(availability, counter)):
100 print('%s\t%s' % (model, dict(counter[model])))
101 for host, info in group[model].get('Ready', {}).items():
102 print('\t%s\t%s' % (host, _get_label_by_prefix(info, 'board')))
103 if not opts.all:
104 break
105 print('-' * 30)
106
107 if not opts.all:
108 print('Only list one host per model by default. Use --all to output all.')
109
110
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800111def cmd_lock_dut(opts):
112 host = cros_lab_util.dut_host_name(opts.dut)
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800113 if cros_lab_util.is_skylab_dut(host):
114 logger.info('%s is a DUT in skylab', host)
115 logger.info('trying to lease %s', host)
116 if cros_lab_util.skylab_lease_dut(host, opts.duration):
117 logger.info('locked %s', host)
118 else:
119 raise Exception('unable to lock %s' % host)
120 return
121
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800122 if opts.session:
123 reason = cros_lab_util.make_lock_reason(opts.session)
124 else:
125 reason = opts.reason
126 cros_lab_util.lock_host(host, reason)
127 logger.info('%s locked', host)
128
129
130def cmd_unlock_dut(opts):
131 host = cros_lab_util.dut_host_name(opts.dut)
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800132 if cros_lab_util.is_skylab_dut(host):
133 cros_lab_util.skylab_release_dut(host)
134 logger.info('%s unlocked', host)
135 return
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800136 hosts = cros_lab_util.list_host(host=host)
137 assert hosts, 'host=%s does not exist' % host
138 info = hosts[host]
139 assert info['Locked'], '%s is not locked?' % host
140 if opts.session:
141 reason = cros_lab_util.make_lock_reason(opts.session)
142 assert info['Lock Reason'] == reason
143
144 cros_lab_util.unlock_host(host)
145 logger.info('%s unlocked', host)
146
147
148def do_allocate_dut(opts):
149 """Helper of cmd_allocate_dut.
150
151 Returns:
152 (todo, host)
153 todo: 'ready' or 'wait'
154 host: locked host name
155 """
156 if not opts.model and not opts.sku:
157 raise errors.ArgumentError('--model or --sku', 'need to be specified')
158
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800159 if opts.locked_dut:
160 assert not cros_lab_util.is_skylab_dut(opts.locked_dut)
161 return do_allocate_dut_autotest_lab(opts)
162
163 try:
164 return do_allocate_dut_skylab(opts)
165 except errors.NoDutAvailable:
166 logger.warning(
167 'failed to allocate DUT from skylab, fallback to autotest lab')
168 cros_lab_util.seek_host(
169 opts.pools.split(','), opts.model, opts.sku, opts.label)
170 return do_allocate_dut_autotest_lab(opts)
171
172
173def do_allocate_dut_skylab(opts):
174 dimensions = ['dut_state:ready', 'label-pool:DUT_POOL_QUOTA']
175 if opts.model:
176 dimensions.append('label-model:' + opts.model)
177 if opts.sku:
178 dimensions.append('label-hwid_sku:' +
179 cros_lab_util.normalize_sku_name(opts.sku))
180
181 t0 = time.time()
182 while time.time() - t0 < opts.time_limit:
183 # Query every time because each iteration takes 10+ miuntes
184 bots = cros_lab_util.swarming_bots_list(dimensions, is_busy=False)
185 if not bots:
186 bots = cros_lab_util.swarming_bots_list(dimensions)
187 if not bots:
188 raise errors.NoDutAvailable(
189 'no bots satisfy constraints; incorrect model/sku? %s' % dimensions)
190
191 bot = random.choice(bots)
192 host = bot['dimensions']['dut_name'][0]
193 logger.info('trying to lease %s', host)
194 if cros_lab_util.skylab_lease_dut(host, opts.duration):
195 logger.info('leased %s (bot_id=%s)', host, bot['bot_id'])
196 return 'ready', host
197
198 return 'wait', None
199
200
201def do_allocate_dut_autotest_lab(opts):
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800202 reason = cros_lab_util.make_lock_reason(opts.session)
203 waiting = None
204 if opts.locked_dut:
205 host = cros_lab_util.dut_host_name(opts.locked_dut)
206 logger.info('check current status of previous locked host %s', host)
207 hosts = cros_lab_util.list_host(host=host)
208 if not hosts:
209 logger.warning('we have ever locked %s but it disappeared now', host)
210 waiting = None
211 else:
212 waiting = hosts[host]
213
214 logger.info('current status=%r, locked=%s, by=%s, reason=%r',
215 waiting['Status'], waiting.get('Locked'),
216 waiting.get('Locked by'), waiting.get('Lock Reason'))
217 if not waiting['Locked'] or waiting['Lock Reason'] != reason:
218 # Special case: not locked by us, so do not unlock it.
219 opts.locked_dut = None
220 raise errors.ExternalError(
221 '%s should be locked by us with reason=%r' % (host, reason))
222
223 if waiting['Status'] == cros_lab_util.READY_STATE:
224 return 'ready', host
225 elif waiting['Status'] in cros_lab_util.BAD_STATES:
226 logger.warning('locked host=%s, became %s; give it up', host,
227 waiting['Status'])
228 waiting = None
229
230 # No matter we have locked a host or not, check if any other hosts are
231 # available.
232 candidates = cros_lab_util.seek_host(
233 opts.pools.split(','), opts.model, opts.sku, opts.label)
234
235 for info in candidates:
236 if info['Locked']:
237 continue
238
239 to_lock = False
240 if info['Status'] == cros_lab_util.READY_STATE:
241 if waiting:
242 logger.info(
243 'although we locked and are waiting for %s, '
244 'we found another host=%s is ready', waiting['Host'], info['Host'])
245 to_lock = True
246 elif info['Status'] in cros_lab_util.GOOD_STATES and not waiting:
247 to_lock = True
248
249 if not to_lock:
250 continue
251
252 after_lock = cros_lab_util.lock_host(info['Host'], reason)
253
254 if after_lock['Status'] == cros_lab_util.READY_STATE:
255 # Lucky, became ready just before we lock.
256 return 'ready', after_lock['Host']
257
258 if waiting:
259 logger.info('but %s became %s just before we lock it', after_lock['Host'],
260 after_lock['Status'])
261 cros_lab_util.unlock_host(after_lock['Host'])
262 continue
263
264 logger.info('locked %s and wait it ready', after_lock['Host'])
265 return 'wait', after_lock['Host']
266
267 if waiting:
268 logger.info('continue to wait %s', waiting['Host'])
269 return 'wait', waiting['Host']
270
271 if not candidates:
272 raise errors.NoDutAvailable('all are in bad states')
273
274 logger.info(
275 'we did not lock any hosts, but are waiting %d hosts in '
276 'transient states', len(candidates))
277 return 'wait', None
278
279
280def cmd_allocate_dut(opts):
281 locked_dut = None
282 try:
283 todo, host = do_allocate_dut(opts)
284 locked_dut = host + '.cros' if host else None
285 result = {'result': todo, 'locked_dut': locked_dut}
286 print(json.dumps(result))
287 except Exception as e:
288 logger.exception('cmd_allocate_dut failed')
289 exception_name = e.__class__.__name__
290 result = {
291 'result': 'failed',
292 'exception': exception_name,
293 'text': str(e),
294 }
295 print(json.dumps(result))
296 finally:
297 # For any reasons, if we locked a new DUT, unlock the previous one.
298 if opts.locked_dut and opts.locked_dut != locked_dut:
299 cros_lab_util.unlock_host(cros_lab_util.dut_host_name(opts.locked_dut))
300
301
Kuang-che Wua8c3c3e2019-08-28 18:49:28 +0800302def cmd_repair_dut(opts):
303 cros_lab_util.repair(opts.dut)
304
305
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800306@cli.fatal_error_handler
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800307def main():
308 common.init()
309 parser = argparse.ArgumentParser()
Kuang-che Wufe1e88a2019-09-10 21:52:25 +0800310 cli.patching_argparser_exit(parser)
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800311 common.add_common_arguments(parser)
312 subparsers = parser.add_subparsers(
313 dest='command', title='commands', metavar='<command>')
314
315 parser_version_info = subparsers.add_parser(
316 'version_info',
317 help='Query version info of given chromeos build',
318 description='Given chromeos `board` and `version`, '
319 'print version information of components.')
320 parser_version_info.add_argument(
321 'board', help='ChromeOS board name, like "samus".')
322 parser_version_info.add_argument(
323 'version',
324 type=cros_util.argtype_cros_version,
325 help='ChromeOS version, like "9876.0.0" or "R62-9876.0.0"')
326 parser_version_info.add_argument(
327 'name',
328 nargs='?',
329 help='Component name. If specified, output its version string. '
330 'Otherwise output all version info as dict in json format.')
331 parser_version_info.set_defaults(func=cmd_version_info)
332
333 parser_query_dut_board = subparsers.add_parser(
334 'query_dut_board', help='Query board name of given DUT')
335 parser_query_dut_board.add_argument('dut')
336 parser_query_dut_board.set_defaults(func=cmd_query_dut_board)
337
338 parser_reboot = subparsers.add_parser(
339 'reboot',
340 help='Reboot a DUT',
341 description='Reboot a DUT and verify the reboot is successful.')
342 parser_reboot.add_argument('dut')
343 parser_reboot.set_defaults(func=cmd_reboot)
344
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800345 parser_lock_dut = subparsers.add_parser(
346 'lock_dut',
347 help='Lock a DUT in the lab',
348 description='Lock a DUT in the lab. '
349 'This is simply wrapper of "atest" with additional checking.')
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800350 # "skylab lease-dut" doesn't take reason, so this is not required=True.
351 group = parser_lock_dut.add_mutually_exclusive_group()
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800352 group.add_argument('--session', help='session name; for creating lock reason')
353 group.add_argument('--reason', help='specify lock reason manually')
354 parser_lock_dut.add_argument('dut')
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800355 parser_lock_dut.add_argument(
356 '--duration',
357 type=float,
358 help='duration in seconds; will be round to minutes')
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800359 parser_lock_dut.set_defaults(func=cmd_lock_dut)
360
361 parser_unlock_dut = subparsers.add_parser(
362 'unlock_dut',
363 help='Unlock a DUT in the lab',
364 description='Unlock a DUT in the lab. '
365 'This is simply wrapper of "atest" with additional checking.')
366 parser_unlock_dut.add_argument(
367 '--session', help='session name; for checking lock reason before unlock')
368 parser_unlock_dut.add_argument('dut')
369 parser_unlock_dut.set_defaults(func=cmd_unlock_dut)
370
371 parser_allocate_dut = subparsers.add_parser(
372 'allocate_dut',
373 help='Allocate a DUT in the lab',
374 description='Allocate a DUT in the lab. It will lock a DUT in the lab '
375 'for bisecting. If no DUT is available (ready), it will lock one. The '
376 'caller (bisect-kit runner) of this command should keep note of the '
377 'locked DUT name and retry this command again later.')
378 parser_allocate_dut.add_argument(
379 '--session', required=True, help='session name')
380 parser_allocate_dut.add_argument(
381 '--pools', required=True, help='Pools to search dut, comma separated')
382 parser_allocate_dut.add_argument('--model', help='allocation criteria')
383 parser_allocate_dut.add_argument('--sku', help='allocation criteria')
384 parser_allocate_dut.add_argument(
385 '--label', '-b', help='Additional required labels, comma separated')
386 parser_allocate_dut.add_argument(
387 '--locked_dut', help='Locked DUT name by last run')
Kuang-che Wu0c9b7942019-10-30 16:55:39 +0800388 parser_allocate_dut.add_argument(
389 '--time_limit',
390 type=int,
391 default=15 * 60,
392 help='Time limit to attempt lease in seconds (default: %(default)s)')
393 parser_allocate_dut.add_argument(
394 '--duration',
395 type=float,
396 help='lease duration in seconds; will be round to minutes')
Kuang-che Wu22aa9d42019-01-25 10:35:33 +0800397 parser_allocate_dut.set_defaults(func=cmd_allocate_dut)
398
Kuang-che Wua8c3c3e2019-08-28 18:49:28 +0800399 parser_repair_dut = subparsers.add_parser(
400 'repair_dut',
401 help='Repair a DUT in the lab',
402 description='Repair a DUT in the lab. '
403 'This is simply wrapper of "deploy repair" with additional checking.')
404 parser_repair_dut.add_argument('dut')
405 parser_repair_dut.set_defaults(func=cmd_repair_dut)
406
Kuang-che Wuc45cfa42019-01-15 00:15:01 +0800407 parser_search_dut = subparsers.add_parser(
408 'search_dut',
409 help='Search DUT with conditions',
410 description='Search hosts in the lab. Grouped by model and ordered by '
411 'availability. When your test could be run on many models, this command '
412 'help you to decide what model to use.')
413 parser_search_dut.add_argument(
414 '--pools',
415 default='performance,crosperf,suites',
416 help='Pools to search, comma separated. The searching will be performed '
417 'for each pool.')
418 parser_search_dut.add_argument(
419 '--label',
420 '-b',
421 help='Additional required labels, comma separated. '
422 'If more than one, all need to be satisfied.')
423 parser_search_dut.add_argument(
424 '--all',
425 action='store_true',
426 help='List all DUTs; (list only one DUT per board by default)')
427 parser_search_dut.add_argument(
428 'condition',
429 nargs='*',
430 help='Conditions, ex. board name, model name, sku name, etc. If more '
431 'than one is specified, matching any one is sufficient. If none is '
432 'specified, all hosts will be listed.')
433 parser_search_dut.set_defaults(func=cmd_search_dut)
434
Kuang-che Wu2ea804f2017-11-28 17:11:41 +0800435 opts = parser.parse_args()
436 common.config_logging(opts)
437 opts.func(opts)
438
439
440if __name__ == '__main__':
441 main()