Home | History | Annotate | Download | only in server
      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