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