1 # Copyright (c) 2013 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 6 import contextlib 7 import grp 8 import httplib 9 import json 10 import logging 11 import os 12 import random 13 import re 14 import time 15 import urllib2 16 17 import common 18 from autotest_lib.client.common_lib import utils 19 from autotest_lib.client.common_lib import error 20 from autotest_lib.client.common_lib import global_config 21 from autotest_lib.client.common_lib import host_queue_entry_states 22 from autotest_lib.client.common_lib import host_states 23 from autotest_lib.server.cros import provision 24 from autotest_lib.server.cros.dynamic_suite import constants 25 from autotest_lib.server.cros.dynamic_suite import job_status 26 27 try: 28 from chromite.lib import cros_build_lib 29 from chromite.lib import ts_mon_config 30 except ImportError: 31 logging.warn('Unable to import chromite. Monarch is disabled.') 32 # Init the module variable to None. Access to this module can check if it 33 # is not None before making calls. 34 cros_build_lib = None 35 ts_mon_config = None 36 37 38 CONFIG = global_config.global_config 39 40 _SHERIFF_JS = CONFIG.get_config_value('NOTIFICATIONS', 'sheriffs', default='') 41 _LAB_SHERIFF_JS = CONFIG.get_config_value( 42 'NOTIFICATIONS', 'lab_sheriffs', default='') 43 _CHROMIUM_BUILD_URL = CONFIG.get_config_value( 44 'NOTIFICATIONS', 'chromium_build_url', default='') 45 46 LAB_GOOD_STATES = ('open', 'throttled') 47 48 ENABLE_DRONE_IN_RESTRICTED_SUBNET = CONFIG.get_config_value( 49 'CROS', 'enable_drone_in_restricted_subnet', type=bool, 50 default=False) 51 52 # Wait at most 10 mins for duts to go idle. 53 IDLE_DUT_WAIT_TIMEOUT = 600 54 55 # Mapping between board name and build target. This is for special case handling 56 # for certain Android board that the board name and build target name does not 57 # match. 58 ANDROID_TARGET_TO_BOARD_MAP = { 59 'seed_l8150': 'gm4g_sprout', 60 'bat_land': 'bat' 61 } 62 ANDROID_BOARD_TO_TARGET_MAP = { 63 'gm4g_sprout': 'seed_l8150', 64 'bat': 'bat_land' 65 } 66 67 class TestLabException(Exception): 68 """Exception raised when the Test Lab blocks a test or suite.""" 69 pass 70 71 72 class ParseBuildNameException(Exception): 73 """Raised when ParseBuildName() cannot parse a build name.""" 74 pass 75 76 77 class Singleton(type): 78 """Enforce that only one client class is instantiated per process.""" 79 _instances = {} 80 81 def __call__(cls, *args, **kwargs): 82 """Fetch the instance of a class to use for subsequent calls.""" 83 if cls not in cls._instances: 84 cls._instances[cls] = super(Singleton, cls).__call__( 85 *args, **kwargs) 86 return cls._instances[cls] 87 88 class EmptyAFEHost(object): 89 """Object to represent an AFE host object when there is no AFE.""" 90 91 def __init__(self): 92 """ 93 We'll be setting the instance attributes as we use them. Right now 94 we only use attributes and labels but as time goes by and other 95 attributes are used from an actual AFE Host object (check 96 rpc_interfaces.get_hosts()), we'll add them in here so users won't be 97 perplexed why their host's afe_host object complains that attribute 98 doesn't exist. 99 """ 100 self.attributes = {} 101 self.labels = [] 102 103 104 def ParseBuildName(name): 105 """Format a build name, given board, type, milestone, and manifest num. 106 107 @param name: a build name, e.g. 'x86-alex-release/R20-2015.0.0' or a 108 relative build name, e.g. 'x86-alex-release/LATEST' 109 110 @return board: board the manifest is for, e.g. x86-alex. 111 @return type: one of 'release', 'factory', or 'firmware' 112 @return milestone: (numeric) milestone the manifest was associated with. 113 Will be None for relative build names. 114 @return manifest: manifest number, e.g. '2015.0.0'. 115 Will be None for relative build names. 116 117 """ 118 match = re.match(r'(trybot-)?(?P<board>[\w-]+?)(?:-chrome)?(?:-chromium)?' 119 r'-(?P<type>\w+)/(R(?P<milestone>\d+)-' 120 r'(?P<manifest>[\d.ab-]+)|LATEST)', 121 name) 122 if match and len(match.groups()) >= 5: 123 return (match.group('board'), match.group('type'), 124 match.group('milestone'), match.group('manifest')) 125 raise ParseBuildNameException('%s is a malformed build name.' % name) 126 127 128 def get_labels_from_afe(hostname, label_prefix, afe): 129 """Retrieve a host's specific labels from the AFE. 130 131 Looks for the host labels that have the form <label_prefix>:<value> 132 and returns the "<value>" part of the label. None is returned 133 if there is not a label matching the pattern 134 135 @param hostname: hostname of given DUT. 136 @param label_prefix: prefix of label to be matched, e.g., |board:| 137 @param afe: afe instance. 138 139 @returns A list of labels that match the prefix or 'None' 140 141 """ 142 labels = afe.get_labels(name__startswith=label_prefix, 143 host__hostname__in=[hostname]) 144 if labels: 145 return [l.name.split(label_prefix, 1)[1] for l in labels] 146 147 148 def get_label_from_afe(hostname, label_prefix, afe): 149 """Retrieve a host's specific label from the AFE. 150 151 Looks for a host label that has the form <label_prefix>:<value> 152 and returns the "<value>" part of the label. None is returned 153 if there is not a label matching the pattern 154 155 @param hostname: hostname of given DUT. 156 @param label_prefix: prefix of label to be matched, e.g., |board:| 157 @param afe: afe instance. 158 @returns the label that matches the prefix or 'None' 159 160 """ 161 labels = get_labels_from_afe(hostname, label_prefix, afe) 162 if labels and len(labels) == 1: 163 return labels[0] 164 165 166 def get_board_from_afe(hostname, afe): 167 """Retrieve given host's board from its labels in the AFE. 168 169 Looks for a host label of the form "board:<board>", and 170 returns the "<board>" part of the label. `None` is returned 171 if there is not a single, unique label matching the pattern. 172 173 @param hostname: hostname of given DUT. 174 @param afe: afe instance. 175 @returns board from label, or `None`. 176 177 """ 178 return get_label_from_afe(hostname, constants.BOARD_PREFIX, afe) 179 180 181 def get_build_from_afe(hostname, afe): 182 """Retrieve the current build for given host from the AFE. 183 184 Looks through the host's labels in the AFE to determine its build. 185 186 @param hostname: hostname of given DUT. 187 @param afe: afe instance. 188 @returns The current build or None if it could not find it or if there 189 were multiple build labels assigned to this host. 190 191 """ 192 for prefix in [provision.CROS_VERSION_PREFIX, 193 provision.ANDROID_BUILD_VERSION_PREFIX]: 194 build = get_label_from_afe(hostname, prefix + ':', afe) 195 if build: 196 return build 197 return None 198 199 200 # TODO(fdeng): fix get_sheriffs crbug.com/483254 201 def get_sheriffs(lab_only=False): 202 """ 203 Polls the javascript file that holds the identity of the sheriff and 204 parses it's output to return a list of chromium sheriff email addresses. 205 The javascript file can contain the ldap of more than one sheriff, eg: 206 document.write('sheriff_one, sheriff_two'). 207 208 @param lab_only: if True, only pulls lab sheriff. 209 @return: A list of chroium.org sheriff email addresses to cc on the bug. 210 An empty list if failed to parse the javascript. 211 """ 212 sheriff_ids = [] 213 sheriff_js_list = _LAB_SHERIFF_JS.split(',') 214 if not lab_only: 215 sheriff_js_list.extend(_SHERIFF_JS.split(',')) 216 217 for sheriff_js in sheriff_js_list: 218 try: 219 url_content = utils.urlopen('%s%s'% ( 220 _CHROMIUM_BUILD_URL, sheriff_js)).read() 221 except (ValueError, IOError) as e: 222 logging.warning('could not parse sheriff from url %s%s: %s', 223 _CHROMIUM_BUILD_URL, sheriff_js, str(e)) 224 except (urllib2.URLError, httplib.HTTPException) as e: 225 logging.warning('unexpected error reading from url "%s%s": %s', 226 _CHROMIUM_BUILD_URL, sheriff_js, str(e)) 227 else: 228 ldaps = re.search(r"document.write\('(.*)'\)", url_content) 229 if not ldaps: 230 logging.warning('Could not retrieve sheriff ldaps for: %s', 231 url_content) 232 continue 233 sheriff_ids += ['%s (at] chromium.org' % alias.replace(' ', '') 234 for alias in ldaps.group(1).split(',')] 235 return sheriff_ids 236 237 238 def remote_wget(source_url, dest_path, ssh_cmd): 239 """wget source_url from localhost to dest_path on remote host using ssh. 240 241 @param source_url: The complete url of the source of the package to send. 242 @param dest_path: The path on the remote host's file system where we would 243 like to store the package. 244 @param ssh_cmd: The ssh command to use in performing the remote wget. 245 """ 246 wget_cmd = ("wget -O - %s | %s 'cat >%s'" % 247 (source_url, ssh_cmd, dest_path)) 248 utils.run(wget_cmd) 249 250 251 _MAX_LAB_STATUS_ATTEMPTS = 5 252 def _get_lab_status(status_url): 253 """Grabs the current lab status and message. 254 255 @returns The JSON object obtained from the given URL. 256 257 """ 258 retry_waittime = 1 259 for _ in range(_MAX_LAB_STATUS_ATTEMPTS): 260 try: 261 response = urllib2.urlopen(status_url) 262 except IOError as e: 263 logging.debug('Error occurred when grabbing the lab status: %s.', 264 e) 265 time.sleep(retry_waittime) 266 continue 267 # Check for successful response code. 268 if response.getcode() == 200: 269 return json.load(response) 270 time.sleep(retry_waittime) 271 return None 272 273 274 def _decode_lab_status(lab_status, build): 275 """Decode lab status, and report exceptions as needed. 276 277 Take a deserialized JSON object from the lab status page, and 278 interpret it to determine the actual lab status. Raise 279 exceptions as required to report when the lab is down. 280 281 @param build: build name that we want to check the status of. 282 283 @raises TestLabException Raised if a request to test for the given 284 status and build should be blocked. 285 """ 286 # First check if the lab is up. 287 if not lab_status['general_state'] in LAB_GOOD_STATES: 288 raise TestLabException('Chromium OS Test Lab is closed: ' 289 '%s.' % lab_status['message']) 290 291 # Check if the build we wish to use is disabled. 292 # Lab messages should be in the format of: 293 # Lab is 'status' [regex ...] (comment) 294 # If the build name matches any regex, it will be blocked. 295 build_exceptions = re.search('\[(.*)\]', lab_status['message']) 296 if not build_exceptions or not build: 297 return 298 for build_pattern in build_exceptions.group(1).split(): 299 if re.match(build_pattern, build): 300 raise TestLabException('Chromium OS Test Lab is closed: ' 301 '%s matches %s.' % ( 302 build, build_pattern)) 303 return 304 305 306 def is_in_lab(): 307 """Check if current Autotest instance is in lab 308 309 @return: True if the Autotest instance is in lab. 310 """ 311 test_server_name = CONFIG.get_config_value('SERVER', 'hostname') 312 return test_server_name.startswith('cautotest') 313 314 315 def check_lab_status(build): 316 """Check if the lab status allows us to schedule for a build. 317 318 Checks if the lab is down, or if testing for the requested build 319 should be blocked. 320 321 @param build: Name of the build to be scheduled for testing. 322 323 @raises TestLabException Raised if a request to test for the given 324 status and build should be blocked. 325 326 """ 327 # Ensure we are trying to schedule on the actual lab. 328 if not is_in_lab(): 329 return 330 331 # Download the lab status from its home on the web. 332 status_url = CONFIG.get_config_value('CROS', 'lab_status_url') 333 json_status = _get_lab_status(status_url) 334 if json_status is None: 335 # We go ahead and say the lab is open if we can't get the status. 336 logging.warning('Could not get a status from %s', status_url) 337 return 338 _decode_lab_status(json_status, build) 339 340 341 def lock_host_with_labels(afe, lock_manager, labels): 342 """Lookup and lock one host that matches the list of input labels. 343 344 @param afe: An instance of the afe class, as defined in server.frontend. 345 @param lock_manager: A lock manager capable of locking hosts, eg the 346 one defined in server.cros.host_lock_manager. 347 @param labels: A list of labels to look for on hosts. 348 349 @return: The hostname of a host matching all labels, and locked through the 350 lock_manager. The hostname will be as specified in the database the afe 351 object is associated with, i.e if it exists in afe_hosts with a .cros 352 suffix, the hostname returned will contain a .cros suffix. 353 354 @raises: error.NoEligibleHostException: If no hosts matching the list of 355 input labels are available. 356 @raises: error.TestError: If unable to lock a host matching the labels. 357 """ 358 potential_hosts = afe.get_hosts(multiple_labels=labels) 359 if not potential_hosts: 360 raise error.NoEligibleHostException( 361 'No devices found with labels %s.' % labels) 362 363 # This prevents errors where a fault might seem repeatable 364 # because we lock, say, the same packet capturer for each test run. 365 random.shuffle(potential_hosts) 366 for host in potential_hosts: 367 if lock_manager.lock([host.hostname]): 368 logging.info('Locked device %s with labels %s.', 369 host.hostname, labels) 370 return host.hostname 371 else: 372 logging.info('Unable to lock device %s with labels %s.', 373 host.hostname, labels) 374 375 raise error.TestError('Could not lock a device with labels %s' % labels) 376 377 378 def get_test_views_from_tko(suite_job_id, tko): 379 """Get test name and result for given suite job ID. 380 381 @param suite_job_id: ID of suite job. 382 @param tko: an instance of TKO as defined in server/frontend.py. 383 @return: A dictionary of test status keyed by test name, e.g., 384 {'dummy_Fail.Error': 'ERROR', 'dummy_Fail.NAError': 'TEST_NA'} 385 @raise: Exception when there is no test view found. 386 387 """ 388 views = tko.run('get_detailed_test_views', afe_job_id=suite_job_id) 389 relevant_views = filter(job_status.view_is_relevant, views) 390 if not relevant_views: 391 raise Exception('Failed to retrieve job results.') 392 393 test_views = {} 394 for view in relevant_views: 395 test_views[view['test_name']] = view['status'] 396 397 return test_views 398 399 400 def get_data_key(prefix, suite, build, board): 401 """ 402 Constructs a key string from parameters. 403 404 @param prefix: Prefix for the generating key. 405 @param suite: a suite name. e.g., bvt-cq, bvt-inline, dummy 406 @param build: The build string. This string should have a consistent 407 format eg: x86-mario-release/R26-3570.0.0. If the format of this 408 string changes such that we can't determine build_type or branch 409 we give up and use the parametes we're sure of instead (suite, 410 board). eg: 411 1. build = x86-alex-pgo-release/R26-3570.0.0 412 branch = 26 413 build_type = pgo-release 414 2. build = lumpy-paladin/R28-3993.0.0-rc5 415 branch = 28 416 build_type = paladin 417 @param board: The board that this suite ran on. 418 @return: The key string used for a dictionary. 419 """ 420 try: 421 _board, build_type, branch = ParseBuildName(build)[:3] 422 except ParseBuildNameException as e: 423 logging.error(str(e)) 424 branch = 'Unknown' 425 build_type = 'Unknown' 426 else: 427 embedded_str = re.search(r'x86-\w+-(.*)', _board) 428 if embedded_str: 429 build_type = embedded_str.group(1) + '-' + build_type 430 431 data_key_dict = { 432 'prefix': prefix, 433 'board': board, 434 'branch': branch, 435 'build_type': build_type, 436 'suite': suite, 437 } 438 return ('%(prefix)s.%(board)s.%(build_type)s.%(branch)s.%(suite)s' 439 % data_key_dict) 440 441 442 def setup_logging(logfile=None, prefix=False): 443 """Setup basic logging with all logging info stripped. 444 445 Calls to logging will only show the message. No severity is logged. 446 447 @param logfile: If specified dump output to a file as well. 448 @param prefix: Flag for log prefix. Set to True to add prefix to log 449 entries to include timestamp and log level. Default is False. 450 """ 451 # Remove all existing handlers. client/common_lib/logging_config adds 452 # a StreamHandler to logger when modules are imported, e.g., 453 # autotest_lib.client.bin.utils. A new StreamHandler will be added here to 454 # log only messages, not severity. 455 logging.getLogger().handlers = [] 456 457 if prefix: 458 log_format = '%(asctime)s %(levelname)-5s| %(message)s' 459 else: 460 log_format = '%(message)s' 461 462 screen_handler = logging.StreamHandler() 463 screen_handler.setFormatter(logging.Formatter(log_format)) 464 logging.getLogger().addHandler(screen_handler) 465 logging.getLogger().setLevel(logging.INFO) 466 if logfile: 467 file_handler = logging.FileHandler(logfile) 468 file_handler.setFormatter(logging.Formatter(log_format)) 469 file_handler.setLevel(logging.DEBUG) 470 logging.getLogger().addHandler(file_handler) 471 472 473 def is_shard(): 474 """Determines if this instance is running as a shard. 475 476 Reads the global_config value shard_hostname in the section SHARD. 477 478 @return True, if shard_hostname is set, False otherwise. 479 """ 480 hostname = CONFIG.get_config_value('SHARD', 'shard_hostname', default=None) 481 return bool(hostname) 482 483 484 def get_global_afe_hostname(): 485 """Read the hostname of the global AFE from the global configuration.""" 486 return CONFIG.get_config_value('SERVER', 'global_afe_hostname') 487 488 489 def is_restricted_user(username): 490 """Determines if a user is in a restricted group. 491 492 User in restricted group only have access to master. 493 494 @param username: A string, representing a username. 495 496 @returns: True if the user is in a restricted group. 497 """ 498 if not username: 499 return False 500 501 restricted_groups = CONFIG.get_config_value( 502 'AUTOTEST_WEB', 'restricted_groups', default='').split(',') 503 for group in restricted_groups: 504 try: 505 if group and username in grp.getgrnam(group).gr_mem: 506 return True 507 except KeyError as e: 508 logging.debug("%s is not a valid group.", group) 509 return False 510 511 512 def get_special_task_status(is_complete, success, is_active): 513 """Get the status of a special task. 514 515 Emulate a host queue entry status for a special task 516 Although SpecialTasks are not HostQueueEntries, it is helpful to 517 the user to present similar statuses. 518 519 @param is_complete Boolean if the task is completed. 520 @param success Boolean if the task succeeded. 521 @param is_active Boolean if the task is active. 522 523 @return The status of a special task. 524 """ 525 if is_complete: 526 if success: 527 return host_queue_entry_states.Status.COMPLETED 528 return host_queue_entry_states.Status.FAILED 529 if is_active: 530 return host_queue_entry_states.Status.RUNNING 531 return host_queue_entry_states.Status.QUEUED 532 533 534 def get_special_task_exec_path(hostname, task_id, task_name, time_requested): 535 """Get the execution path of the SpecialTask. 536 537 This method returns different paths depending on where a 538 the task ran: 539 * Master: hosts/hostname/task_id-task_type 540 * Shard: Master_path/time_created 541 This is to work around the fact that a shard can fail independent 542 of the master, and be replaced by another shard that has the same 543 hosts. Without the time_created stamp the logs of the tasks running 544 on the second shard will clobber the logs from the first in google 545 storage, because task ids are not globally unique. 546 547 @param hostname Hostname 548 @param task_id Special task id 549 @param task_name Special task name (e.g., Verify, Repair, etc) 550 @param time_requested Special task requested time. 551 552 @return An execution path for the task. 553 """ 554 results_path = 'hosts/%s/%s-%s' % (hostname, task_id, task_name.lower()) 555 556 # If we do this on the master it will break backward compatibility, 557 # as there are tasks that currently don't have timestamps. If a host 558 # or job has been sent to a shard, the rpc for that host/job will 559 # be redirected to the shard, so this global_config check will happen 560 # on the shard the logs are on. 561 if not is_shard(): 562 return results_path 563 564 # Generate a uid to disambiguate special task result directories 565 # in case this shard fails. The simplest uid is the job_id, however 566 # in rare cases tasks do not have jobs associated with them (eg: 567 # frontend verify), so just use the creation timestamp. The clocks 568 # between a shard and master should always be in sync. Any discrepancies 569 # will be brought to our attention in the form of job timeouts. 570 uid = time_requested.strftime('%Y%d%m%H%M%S') 571 572 # TODO: This is a hack, however it is the easiest way to achieve 573 # correctness. There is currently some debate over the future of 574 # tasks in our infrastructure and refactoring everything right 575 # now isn't worth the time. 576 return '%s/%s' % (results_path, uid) 577 578 579 def get_job_tag(id, owner): 580 """Returns a string tag for a job. 581 582 @param id Job id 583 @param owner Job owner 584 585 """ 586 return '%s-%s' % (id, owner) 587 588 589 def get_hqe_exec_path(tag, execution_subdir): 590 """Returns a execution path to a HQE's results. 591 592 @param tag Tag string for a job associated with a HQE. 593 @param execution_subdir Execution sub-directory string of a HQE. 594 595 """ 596 return os.path.join(tag, execution_subdir) 597 598 599 def is_inside_chroot(): 600 """Check if the process is running inside chroot. 601 602 This is a wrapper around chromite.lib.cros_build_lib.IsInsideChroot(). The 603 method checks if cros_build_lib can be imported first. 604 605 @return: True if the process is running inside chroot or cros_build_lib 606 cannot be imported. 607 608 """ 609 return not cros_build_lib or cros_build_lib.IsInsideChroot() 610 611 612 def parse_job_name(name): 613 """Parse job name to get information including build, board and suite etc. 614 615 Suite job created by run_suite follows the naming convention of: 616 [build]-test_suites/control.[suite] 617 For example: lumpy-release/R46-7272.0.0-test_suites/control.bvt 618 The naming convention is defined in rpc_interface.create_suite_job. 619 620 Test job created by suite job follows the naming convention of: 621 [build]/[suite]/[test name] 622 For example: lumpy-release/R46-7272.0.0/bvt/login_LoginSuccess 623 The naming convention is defined in 624 server/cros/dynamic_suite/tools.create_job_name 625 626 Note that pgo and chrome-perf builds will fail the method. Since lab does 627 not run test for these builds, they can be ignored. 628 Also, tests for Launch Control builds have different naming convention. 629 The build ID will be used as build_version. 630 631 @param name: Name of the job. 632 633 @return: A dictionary containing the test information. The keyvals include: 634 build: Name of the build, e.g., lumpy-release/R46-7272.0.0 635 build_version: The version of the build, e.g., R46-7272.0.0 636 board: Name of the board, e.g., lumpy 637 suite: Name of the test suite, e.g., bvt 638 639 """ 640 info = {} 641 suite_job_regex = '([^/]*/[^/]*(?:/\d+)?)-test_suites/control\.(.*)' 642 test_job_regex = '([^/]*/[^/]*(?:/\d+)?)/([^/]+)/.*' 643 match = re.match(suite_job_regex, name) 644 if not match: 645 match = re.match(test_job_regex, name) 646 if match: 647 info['build'] = match.groups()[0] 648 info['suite'] = match.groups()[1] 649 info['build_version'] = info['build'].split('/')[1] 650 try: 651 info['board'], _, _, _ = ParseBuildName(info['build']) 652 except ParseBuildNameException: 653 # Try to parse it as Launch Control build 654 # Launch Control builds have name format: 655 # branch/build_target-build_type/build_id. 656 try: 657 _, target, build_id = utils.parse_launch_control_build( 658 info['build']) 659 build_target, _ = utils.parse_launch_control_target(target) 660 if build_target: 661 info['board'] = build_target 662 info['build_version'] = build_id 663 except ValueError: 664 pass 665 return info 666 667 668 def add_label_detector(label_function_list, label_list=None, label=None): 669 """Decorator used to group functions together into the provided list. 670 671 This is a helper function to automatically add label functions that have 672 the label decorator. This is to help populate the class list of label 673 functions to be retrieved by the get_labels class method. 674 675 @param label_function_list: List of label detecting functions to add 676 decorated function to. 677 @param label_list: List of detectable labels to add detectable labels to. 678 (Default: None) 679 @param label: Label string that is detectable by this detection function 680 (Default: None) 681 """ 682 def add_func(func): 683 """ 684 @param func: The function to be added as a detector. 685 """ 686 label_function_list.append(func) 687 if label and label_list is not None: 688 label_list.append(label) 689 return func 690 return add_func 691 692 693 def verify_not_root_user(): 694 """Simple function to error out if running with uid == 0""" 695 if os.getuid() == 0: 696 raise error.IllegalUser('This script can not be ran as root.') 697 698 699 def get_hostname_from_machine(machine): 700 """Lookup hostname from a machine string or dict. 701 702 @returns: Machine hostname in string format. 703 """ 704 hostname, _ = get_host_info_from_machine(machine) 705 return hostname 706 707 708 def get_host_info_from_machine(machine): 709 """Lookup host information from a machine string or dict. 710 711 @returns: Tuple of (hostname, afe_host) 712 """ 713 if isinstance(machine, dict): 714 return (machine['hostname'], machine['afe_host']) 715 else: 716 return (machine, EmptyAFEHost()) 717 718 719 def get_afe_host_from_machine(machine): 720 """Return the afe_host from the machine dict if possible. 721 722 @returns: AFE host object. 723 """ 724 _, afe_host = get_host_info_from_machine(machine) 725 return afe_host 726 727 728 def get_creds_abspath(creds_file): 729 """Returns the abspath of the credentials file. 730 731 If creds_file is already an absolute path, just return it. 732 Otherwise, assume it is located in the creds directory 733 specified in global_config and return the absolute path. 734 735 @param: creds_path, a path to the credentials. 736 @return: An absolute path to the credentials file. 737 """ 738 if not creds_file: 739 return None 740 if os.path.isabs(creds_file): 741 return creds_file 742 creds_dir = CONFIG.get_config_value('SERVER', 'creds_dir', default='') 743 if not creds_dir or not os.path.exists(creds_dir): 744 creds_dir = common.autotest_dir 745 return os.path.join(creds_dir, creds_file) 746 747 748 def machine_is_testbed(machine): 749 """Checks if the machine is a testbed. 750 751 The signal we use to determine if the machine is a testbed 752 is if the host attributes contain more than 1 serial. 753 754 @param machine: is a list of dicts 755 756 @return: True if the machine is a testbed, False otherwise. 757 """ 758 _, afe_host = get_host_info_from_machine(machine) 759 return len(afe_host.attributes.get('serials', '').split(',')) > 1 760 761 762 def SetupTsMonGlobalState(*args, **kwargs): 763 """Import-safe wrap around chromite.lib.ts_mon_config's setup function. 764 765 @param *args: Args to pass through. 766 @param **kwargs: Kwargs to pass through. 767 """ 768 if ts_mon_config: 769 try: 770 context = ts_mon_config.SetupTsMonGlobalState(*args, **kwargs) 771 if hasattr(context, '__exit__'): 772 return context 773 except Exception as e: 774 logging.warning('Caught an exception trying to setup ts_mon, ' 775 'monitoring is disabled: %s', e, exc_info=True) 776 return TrivialContextManager() 777 else: 778 return TrivialContextManager() 779 780 781 @contextlib.contextmanager 782 def TrivialContextManager(*args, **kwargs): 783 """Context manager that does nothing. 784 785 @param *args: Ignored args 786 @param **kwargs: Ignored kwargs. 787 """ 788 yield 789 790 791 def wait_for_idle_duts(duts, afe, max_wait=IDLE_DUT_WAIT_TIMEOUT): 792 """Wait for the hosts to all go idle. 793 794 @param duts: List of duts to check for idle state. 795 @param afe: afe instance. 796 @param max_wait: Max wait time in seconds. 797 798 @returns Boolean True if all hosts are idle or False if any hosts did not 799 go idle within max_wait. 800 """ 801 start_time = time.time() 802 # We make a shallow copy since we're going to be modifying active_dut_list. 803 active_dut_list = duts[:] 804 while active_dut_list: 805 # Let's rate-limit how often we hit the AFE. 806 time.sleep(1) 807 808 # Check if we've waited too long. 809 if (time.time() - start_time) > max_wait: 810 return False 811 812 idle_duts = [] 813 # Get the status for the duts and see if they're in the idle state. 814 afe_hosts = afe.get_hosts(active_dut_list) 815 idle_duts = [afe_host.hostname for afe_host in afe_hosts 816 if afe_host.status in host_states.IDLE_STATES] 817 818 # Take out idle duts so we don't needlessly check them 819 # next time around. 820 for idle_dut in idle_duts: 821 active_dut_list.remove(idle_dut) 822 823 logging.info('still waiting for following duts to go idle: %s', 824 active_dut_list) 825 return True 826 827 828 @contextlib.contextmanager 829 def lock_duts_and_wait(duts, afe, lock_msg='default lock message', 830 max_wait=IDLE_DUT_WAIT_TIMEOUT): 831 """Context manager to lock the duts and wait for them to go idle. 832 833 @param duts: List of duts to lock. 834 @param afe: afe instance. 835 836 @returns Boolean lock_success where True if all duts locked successfully or 837 False if we timed out waiting too long for hosts to go idle. 838 """ 839 try: 840 locked_duts = [] 841 duts.sort() 842 for dut in duts: 843 if afe.lock_host(dut, lock_msg, fail_if_locked=True): 844 locked_duts.append(dut) 845 else: 846 logging.info('%s already locked', dut) 847 yield wait_for_idle_duts(locked_duts, afe, max_wait) 848 finally: 849 afe.unlock_hosts(locked_duts) 850 851 852 def board_labels_allowed(boards): 853 """Check if the list of board labels can be set to a single host. 854 855 The only case multiple board labels can be set to a single host is for 856 testbed, which may have a list of board labels like 857 board:angler-1, board:angler-2, board:angler-3, board:marlin-1' 858 859 @param boards: A list of board labels (may include platform label). 860 861 @returns True if the the list of boards can be set to a single host. 862 """ 863 # Filter out any non-board labels 864 boards = [b for b in boards if re.match('board:.*', b)] 865 if len(boards) <= 1: 866 return True 867 for board in boards: 868 if not re.match('board:[^-]+-\d+', board): 869 return False 870 return True 871