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 contextlib
7import grp
8import httplib
9import json
10import logging
11import os
12import random
13import re
14import time
15import urllib2
16
17import common
18from autotest_lib.client.common_lib import utils
19from autotest_lib.client.common_lib import error
20from autotest_lib.client.common_lib import global_config
21from autotest_lib.client.common_lib import host_queue_entry_states
22from autotest_lib.client.common_lib import host_states
23from autotest_lib.server.cros import provision
24from autotest_lib.server.cros.dynamic_suite import constants
25from autotest_lib.server.cros.dynamic_suite import job_status
26
27try:
28    from chromite.lib import cros_build_lib
29    from chromite.lib import ts_mon_config
30except 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
38CONFIG = 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
46LAB_GOOD_STATES = ('open', 'throttled')
47
48ENABLE_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.
53IDLE_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.
58ANDROID_TARGET_TO_BOARD_MAP = {
59        'seed_l8150': 'gm4g_sprout',
60        'bat_land': 'bat'
61        }
62ANDROID_BOARD_TO_TARGET_MAP = {
63        'gm4g_sprout': 'seed_l8150',
64        'bat': 'bat_land'
65        }
66
67class TestLabException(Exception):
68    """Exception raised when the Test Lab blocks a test or suite."""
69    pass
70
71
72class ParseBuildNameException(Exception):
73    """Raised when ParseBuildName() cannot parse a build name."""
74    pass
75
76
77class 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
88class 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
104def 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
128def 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
148def 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
166def 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
181def 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
201def 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@chromium.org' % alias.replace(' ', '')
234                            for alias in ldaps.group(1).split(',')]
235    return sheriff_ids
236
237
238def 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
252def _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
274def _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
306def 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
315def 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
341def 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
378def 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
400def 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
442def 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
473def 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
484def 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
489def 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
512def 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
534def 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
579def 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
589def 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
599def 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
612def 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
668def 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
693def 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
699def 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
708def 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
719def 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
728def 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
748def 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
762def 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
782def 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
791def 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
829def 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
852def 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