Kuang-che Wu | 2ea804f | 2017-11-28 17:11:41 +0800 | [diff] [blame] | 1 | #!/usr/bin/env python2 |
Kuang-che Wu | 6e4beca | 2018-06-27 17:45:02 +0800 | [diff] [blame] | 2 | # -*- coding: utf-8 -*- |
Kuang-che Wu | 2ea804f | 2017-11-28 17:11:41 +0800 | [diff] [blame] | 3 | # 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 Wu | 68db08a | 2018-03-30 11:50:34 +0800 | [diff] [blame] | 6 | """Helper script to manipulate chromeos DUT or query info.""" |
Kuang-che Wu | 2ea804f | 2017-11-28 17:11:41 +0800 | [diff] [blame] | 7 | from __future__ import print_function |
| 8 | import argparse |
Kuang-che Wu | c45cfa4 | 2019-01-15 00:15:01 +0800 | [diff] [blame] | 9 | import collections |
| 10 | import functools |
Kuang-che Wu | 2ea804f | 2017-11-28 17:11:41 +0800 | [diff] [blame] | 11 | import json |
| 12 | import logging |
| 13 | |
Kuang-che Wu | fe1e88a | 2019-09-10 21:52:25 +0800 | [diff] [blame] | 14 | from bisect_kit import cli |
Kuang-che Wu | 2ea804f | 2017-11-28 17:11:41 +0800 | [diff] [blame] | 15 | from bisect_kit import common |
Kuang-che Wu | c45cfa4 | 2019-01-15 00:15:01 +0800 | [diff] [blame] | 16 | from bisect_kit import cros_lab_util |
Kuang-che Wu | 2ea804f | 2017-11-28 17:11:41 +0800 | [diff] [blame] | 17 | from bisect_kit import cros_util |
Kuang-che Wu | 22aa9d4 | 2019-01-25 10:35:33 +0800 | [diff] [blame] | 18 | from bisect_kit import errors |
Kuang-che Wu | 2ea804f | 2017-11-28 17:11:41 +0800 | [diff] [blame] | 19 | |
| 20 | logger = logging.getLogger(__name__) |
| 21 | |
| 22 | |
| 23 | def cmd_version_info(opts): |
| 24 | info = cros_util.version_info(opts.board, opts.version) |
| 25 | if opts.name: |
| 26 | if opts.name not in info: |
| 27 | logger.error('unknown name=%s', opts.name) |
| 28 | print(info[opts.name]) |
| 29 | else: |
| 30 | print(json.dumps(info, sort_keys=True, indent=4)) |
| 31 | |
| 32 | |
| 33 | def cmd_query_dut_board(opts): |
| 34 | assert cros_util.is_dut(opts.dut) |
| 35 | print(cros_util.query_dut_board(opts.dut)) |
| 36 | |
| 37 | |
| 38 | def cmd_reboot(opts): |
| 39 | assert cros_util.is_dut(opts.dut) |
| 40 | cros_util.reboot(opts.dut) |
| 41 | |
| 42 | |
Kuang-che Wu | c45cfa4 | 2019-01-15 00:15:01 +0800 | [diff] [blame] | 43 | def _get_label_by_prefix(info, prefix): |
| 44 | for label in info['Labels']: |
| 45 | if label.startswith(prefix + ':'): |
| 46 | return label |
| 47 | return None |
| 48 | |
| 49 | |
| 50 | def cmd_search_dut(opts): |
| 51 | labels = [] |
| 52 | if opts.label: |
| 53 | labels += opts.label.split(',') |
| 54 | |
| 55 | def match(info): |
| 56 | if not opts.condition: |
| 57 | return True |
| 58 | |
| 59 | for label in info['Labels']: |
| 60 | for condition in opts.condition: |
| 61 | if ':' in condition: |
| 62 | keys = [condition] |
| 63 | else: |
| 64 | keys = [ |
| 65 | 'board:' + condition, |
| 66 | 'model:' + condition, |
| 67 | 'sku:' + condition, |
| 68 | ] |
| 69 | for key in keys: |
| 70 | if label.lower().startswith(key.lower()): |
| 71 | return True |
| 72 | return False |
| 73 | |
| 74 | for pool in opts.pools.split(','): |
| 75 | print('pool:' + pool) |
| 76 | |
| 77 | group = collections.defaultdict(dict) |
| 78 | counter = collections.defaultdict(collections.Counter) |
Kuang-che Wu | 0768b97 | 2019-10-05 15:18:59 +0800 | [diff] [blame] | 79 | for host, info in cros_lab_util.list_host( |
| 80 | labels=labels + ['pool:' + pool]).items(): |
Kuang-che Wu | c45cfa4 | 2019-01-15 00:15:01 +0800 | [diff] [blame] | 81 | if not match(info): |
| 82 | continue |
| 83 | |
| 84 | model = _get_label_by_prefix(info, 'model') |
Kuang-che Wu | 22aa9d4 | 2019-01-25 10:35:33 +0800 | [diff] [blame] | 85 | if info.get('Locked'): |
Kuang-che Wu | c45cfa4 | 2019-01-15 00:15:01 +0800 | [diff] [blame] | 86 | state = 'Locked' |
| 87 | else: |
| 88 | state = info['Status'] |
| 89 | if state not in group[model]: |
| 90 | group[model][state] = {} |
| 91 | group[model][state][host] = info |
| 92 | counter[model][state] += 1 |
| 93 | |
| 94 | def availability(counter, model): |
| 95 | return -counter[model]['Ready'] |
| 96 | |
| 97 | for model in sorted(group, key=functools.partial(availability, counter)): |
| 98 | print('%s\t%s' % (model, dict(counter[model]))) |
| 99 | for host, info in group[model].get('Ready', {}).items(): |
| 100 | print('\t%s\t%s' % (host, _get_label_by_prefix(info, 'board'))) |
| 101 | if not opts.all: |
| 102 | break |
| 103 | print('-' * 30) |
| 104 | |
| 105 | if not opts.all: |
| 106 | print('Only list one host per model by default. Use --all to output all.') |
| 107 | |
| 108 | |
Kuang-che Wu | 22aa9d4 | 2019-01-25 10:35:33 +0800 | [diff] [blame] | 109 | def cmd_lock_dut(opts): |
| 110 | host = cros_lab_util.dut_host_name(opts.dut) |
| 111 | if opts.session: |
| 112 | reason = cros_lab_util.make_lock_reason(opts.session) |
| 113 | else: |
| 114 | reason = opts.reason |
| 115 | cros_lab_util.lock_host(host, reason) |
| 116 | logger.info('%s locked', host) |
| 117 | |
| 118 | |
| 119 | def cmd_unlock_dut(opts): |
| 120 | host = cros_lab_util.dut_host_name(opts.dut) |
| 121 | hosts = cros_lab_util.list_host(host=host) |
| 122 | assert hosts, 'host=%s does not exist' % host |
| 123 | info = hosts[host] |
| 124 | assert info['Locked'], '%s is not locked?' % host |
| 125 | if opts.session: |
| 126 | reason = cros_lab_util.make_lock_reason(opts.session) |
| 127 | assert info['Lock Reason'] == reason |
| 128 | |
| 129 | cros_lab_util.unlock_host(host) |
| 130 | logger.info('%s unlocked', host) |
| 131 | |
| 132 | |
| 133 | def do_allocate_dut(opts): |
| 134 | """Helper of cmd_allocate_dut. |
| 135 | |
| 136 | Returns: |
| 137 | (todo, host) |
| 138 | todo: 'ready' or 'wait' |
| 139 | host: locked host name |
| 140 | """ |
| 141 | if not opts.model and not opts.sku: |
| 142 | raise errors.ArgumentError('--model or --sku', 'need to be specified') |
| 143 | |
| 144 | reason = cros_lab_util.make_lock_reason(opts.session) |
| 145 | waiting = None |
| 146 | if opts.locked_dut: |
| 147 | host = cros_lab_util.dut_host_name(opts.locked_dut) |
| 148 | logger.info('check current status of previous locked host %s', host) |
| 149 | hosts = cros_lab_util.list_host(host=host) |
| 150 | if not hosts: |
| 151 | logger.warning('we have ever locked %s but it disappeared now', host) |
| 152 | waiting = None |
| 153 | else: |
| 154 | waiting = hosts[host] |
| 155 | |
| 156 | logger.info('current status=%r, locked=%s, by=%s, reason=%r', |
| 157 | waiting['Status'], waiting.get('Locked'), |
| 158 | waiting.get('Locked by'), waiting.get('Lock Reason')) |
| 159 | if not waiting['Locked'] or waiting['Lock Reason'] != reason: |
| 160 | # Special case: not locked by us, so do not unlock it. |
| 161 | opts.locked_dut = None |
| 162 | raise errors.ExternalError( |
| 163 | '%s should be locked by us with reason=%r' % (host, reason)) |
| 164 | |
| 165 | if waiting['Status'] == cros_lab_util.READY_STATE: |
| 166 | return 'ready', host |
| 167 | elif waiting['Status'] in cros_lab_util.BAD_STATES: |
| 168 | logger.warning('locked host=%s, became %s; give it up', host, |
| 169 | waiting['Status']) |
| 170 | waiting = None |
| 171 | |
| 172 | # No matter we have locked a host or not, check if any other hosts are |
| 173 | # available. |
| 174 | candidates = cros_lab_util.seek_host( |
| 175 | opts.pools.split(','), opts.model, opts.sku, opts.label) |
| 176 | |
| 177 | for info in candidates: |
| 178 | if info['Locked']: |
| 179 | continue |
| 180 | |
| 181 | to_lock = False |
| 182 | if info['Status'] == cros_lab_util.READY_STATE: |
| 183 | if waiting: |
| 184 | logger.info( |
| 185 | 'although we locked and are waiting for %s, ' |
| 186 | 'we found another host=%s is ready', waiting['Host'], info['Host']) |
| 187 | to_lock = True |
| 188 | elif info['Status'] in cros_lab_util.GOOD_STATES and not waiting: |
| 189 | to_lock = True |
| 190 | |
| 191 | if not to_lock: |
| 192 | continue |
| 193 | |
| 194 | after_lock = cros_lab_util.lock_host(info['Host'], reason) |
| 195 | |
| 196 | if after_lock['Status'] == cros_lab_util.READY_STATE: |
| 197 | # Lucky, became ready just before we lock. |
| 198 | return 'ready', after_lock['Host'] |
| 199 | |
| 200 | if waiting: |
| 201 | logger.info('but %s became %s just before we lock it', after_lock['Host'], |
| 202 | after_lock['Status']) |
| 203 | cros_lab_util.unlock_host(after_lock['Host']) |
| 204 | continue |
| 205 | |
| 206 | logger.info('locked %s and wait it ready', after_lock['Host']) |
| 207 | return 'wait', after_lock['Host'] |
| 208 | |
| 209 | if waiting: |
| 210 | logger.info('continue to wait %s', waiting['Host']) |
| 211 | return 'wait', waiting['Host'] |
| 212 | |
| 213 | if not candidates: |
| 214 | raise errors.NoDutAvailable('all are in bad states') |
| 215 | |
| 216 | logger.info( |
| 217 | 'we did not lock any hosts, but are waiting %d hosts in ' |
| 218 | 'transient states', len(candidates)) |
| 219 | return 'wait', None |
| 220 | |
| 221 | |
| 222 | def cmd_allocate_dut(opts): |
| 223 | locked_dut = None |
| 224 | try: |
| 225 | todo, host = do_allocate_dut(opts) |
| 226 | locked_dut = host + '.cros' if host else None |
| 227 | result = {'result': todo, 'locked_dut': locked_dut} |
| 228 | print(json.dumps(result)) |
| 229 | except Exception as e: |
| 230 | logger.exception('cmd_allocate_dut failed') |
| 231 | exception_name = e.__class__.__name__ |
| 232 | result = { |
| 233 | 'result': 'failed', |
| 234 | 'exception': exception_name, |
| 235 | 'text': str(e), |
| 236 | } |
| 237 | print(json.dumps(result)) |
| 238 | finally: |
| 239 | # For any reasons, if we locked a new DUT, unlock the previous one. |
| 240 | if opts.locked_dut and opts.locked_dut != locked_dut: |
| 241 | cros_lab_util.unlock_host(cros_lab_util.dut_host_name(opts.locked_dut)) |
| 242 | |
| 243 | |
Kuang-che Wu | a8c3c3e | 2019-08-28 18:49:28 +0800 | [diff] [blame] | 244 | def cmd_repair_dut(opts): |
| 245 | cros_lab_util.repair(opts.dut) |
| 246 | |
| 247 | |
Kuang-che Wu | fe1e88a | 2019-09-10 21:52:25 +0800 | [diff] [blame] | 248 | @cli.fatal_error_handler |
Kuang-che Wu | 2ea804f | 2017-11-28 17:11:41 +0800 | [diff] [blame] | 249 | def main(): |
| 250 | common.init() |
| 251 | parser = argparse.ArgumentParser() |
Kuang-che Wu | fe1e88a | 2019-09-10 21:52:25 +0800 | [diff] [blame] | 252 | cli.patching_argparser_exit(parser) |
Kuang-che Wu | 2ea804f | 2017-11-28 17:11:41 +0800 | [diff] [blame] | 253 | common.add_common_arguments(parser) |
| 254 | subparsers = parser.add_subparsers( |
| 255 | dest='command', title='commands', metavar='<command>') |
| 256 | |
| 257 | parser_version_info = subparsers.add_parser( |
| 258 | 'version_info', |
| 259 | help='Query version info of given chromeos build', |
| 260 | description='Given chromeos `board` and `version`, ' |
| 261 | 'print version information of components.') |
| 262 | parser_version_info.add_argument( |
| 263 | 'board', help='ChromeOS board name, like "samus".') |
| 264 | parser_version_info.add_argument( |
| 265 | 'version', |
| 266 | type=cros_util.argtype_cros_version, |
| 267 | help='ChromeOS version, like "9876.0.0" or "R62-9876.0.0"') |
| 268 | parser_version_info.add_argument( |
| 269 | 'name', |
| 270 | nargs='?', |
| 271 | help='Component name. If specified, output its version string. ' |
| 272 | 'Otherwise output all version info as dict in json format.') |
| 273 | parser_version_info.set_defaults(func=cmd_version_info) |
| 274 | |
| 275 | parser_query_dut_board = subparsers.add_parser( |
| 276 | 'query_dut_board', help='Query board name of given DUT') |
| 277 | parser_query_dut_board.add_argument('dut') |
| 278 | parser_query_dut_board.set_defaults(func=cmd_query_dut_board) |
| 279 | |
| 280 | parser_reboot = subparsers.add_parser( |
| 281 | 'reboot', |
| 282 | help='Reboot a DUT', |
| 283 | description='Reboot a DUT and verify the reboot is successful.') |
| 284 | parser_reboot.add_argument('dut') |
| 285 | parser_reboot.set_defaults(func=cmd_reboot) |
| 286 | |
Kuang-che Wu | 22aa9d4 | 2019-01-25 10:35:33 +0800 | [diff] [blame] | 287 | parser_lock_dut = subparsers.add_parser( |
| 288 | 'lock_dut', |
| 289 | help='Lock a DUT in the lab', |
| 290 | description='Lock a DUT in the lab. ' |
| 291 | 'This is simply wrapper of "atest" with additional checking.') |
| 292 | group = parser_lock_dut.add_mutually_exclusive_group(required=True) |
| 293 | group.add_argument('--session', help='session name; for creating lock reason') |
| 294 | group.add_argument('--reason', help='specify lock reason manually') |
| 295 | parser_lock_dut.add_argument('dut') |
| 296 | parser_lock_dut.set_defaults(func=cmd_lock_dut) |
| 297 | |
| 298 | parser_unlock_dut = subparsers.add_parser( |
| 299 | 'unlock_dut', |
| 300 | help='Unlock a DUT in the lab', |
| 301 | description='Unlock a DUT in the lab. ' |
| 302 | 'This is simply wrapper of "atest" with additional checking.') |
| 303 | parser_unlock_dut.add_argument( |
| 304 | '--session', help='session name; for checking lock reason before unlock') |
| 305 | parser_unlock_dut.add_argument('dut') |
| 306 | parser_unlock_dut.set_defaults(func=cmd_unlock_dut) |
| 307 | |
| 308 | parser_allocate_dut = subparsers.add_parser( |
| 309 | 'allocate_dut', |
| 310 | help='Allocate a DUT in the lab', |
| 311 | description='Allocate a DUT in the lab. It will lock a DUT in the lab ' |
| 312 | 'for bisecting. If no DUT is available (ready), it will lock one. The ' |
| 313 | 'caller (bisect-kit runner) of this command should keep note of the ' |
| 314 | 'locked DUT name and retry this command again later.') |
| 315 | parser_allocate_dut.add_argument( |
| 316 | '--session', required=True, help='session name') |
| 317 | parser_allocate_dut.add_argument( |
| 318 | '--pools', required=True, help='Pools to search dut, comma separated') |
| 319 | parser_allocate_dut.add_argument('--model', help='allocation criteria') |
| 320 | parser_allocate_dut.add_argument('--sku', help='allocation criteria') |
| 321 | parser_allocate_dut.add_argument( |
| 322 | '--label', '-b', help='Additional required labels, comma separated') |
| 323 | parser_allocate_dut.add_argument( |
| 324 | '--locked_dut', help='Locked DUT name by last run') |
| 325 | parser_allocate_dut.set_defaults(func=cmd_allocate_dut) |
| 326 | |
Kuang-che Wu | a8c3c3e | 2019-08-28 18:49:28 +0800 | [diff] [blame] | 327 | parser_repair_dut = subparsers.add_parser( |
| 328 | 'repair_dut', |
| 329 | help='Repair a DUT in the lab', |
| 330 | description='Repair a DUT in the lab. ' |
| 331 | 'This is simply wrapper of "deploy repair" with additional checking.') |
| 332 | parser_repair_dut.add_argument('dut') |
| 333 | parser_repair_dut.set_defaults(func=cmd_repair_dut) |
| 334 | |
Kuang-che Wu | c45cfa4 | 2019-01-15 00:15:01 +0800 | [diff] [blame] | 335 | parser_search_dut = subparsers.add_parser( |
| 336 | 'search_dut', |
| 337 | help='Search DUT with conditions', |
| 338 | description='Search hosts in the lab. Grouped by model and ordered by ' |
| 339 | 'availability. When your test could be run on many models, this command ' |
| 340 | 'help you to decide what model to use.') |
| 341 | parser_search_dut.add_argument( |
| 342 | '--pools', |
| 343 | default='performance,crosperf,suites', |
| 344 | help='Pools to search, comma separated. The searching will be performed ' |
| 345 | 'for each pool.') |
| 346 | parser_search_dut.add_argument( |
| 347 | '--label', |
| 348 | '-b', |
| 349 | help='Additional required labels, comma separated. ' |
| 350 | 'If more than one, all need to be satisfied.') |
| 351 | parser_search_dut.add_argument( |
| 352 | '--all', |
| 353 | action='store_true', |
| 354 | help='List all DUTs; (list only one DUT per board by default)') |
| 355 | parser_search_dut.add_argument( |
| 356 | 'condition', |
| 357 | nargs='*', |
| 358 | help='Conditions, ex. board name, model name, sku name, etc. If more ' |
| 359 | 'than one is specified, matching any one is sufficient. If none is ' |
| 360 | 'specified, all hosts will be listed.') |
| 361 | parser_search_dut.set_defaults(func=cmd_search_dut) |
| 362 | |
Kuang-che Wu | 2ea804f | 2017-11-28 17:11:41 +0800 | [diff] [blame] | 363 | opts = parser.parse_args() |
| 364 | common.config_logging(opts) |
| 365 | opts.func(opts) |
| 366 | |
| 367 | |
| 368 | if __name__ == '__main__': |
| 369 | main() |