site_utils.py revision fa705d2882fb4de845957d1a064c0c268b9d1b2a
1# Copyright (c) 2012 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
5import inspect
6import logging
7import os
8import re
9import signal
10import socket
11import struct
12import time
13import urllib2
14import uuid
15
16from autotest_lib.client.common_lib import base_utils
17from autotest_lib.client.common_lib import error
18from autotest_lib.client.common_lib import global_config
19from autotest_lib.client.common_lib import lsbrelease_utils
20from autotest_lib.client.cros import constants
21
22
23CONFIG = global_config.global_config
24
25# Keep checking if the pid is alive every second until the timeout (in seconds)
26CHECK_PID_IS_ALIVE_TIMEOUT = 6
27
28_LOCAL_HOST_LIST = ('localhost', '127.0.0.1')
29
30# The default address of a vm gateway.
31DEFAULT_VM_GATEWAY = '10.0.2.2'
32
33# Google Storage bucket URI to store results in.
34DEFAULT_OFFLOAD_GSURI = CONFIG.get_config_value(
35        'CROS', 'results_storage_server', default=None)
36
37# Default Moblab Ethernet Interface.
38MOBLAB_ETH = 'eth0'
39
40# A list of subnets that requires dedicated devserver and drone in the same
41# subnet. Each item is a tuple of (subnet_ip, mask_bits), e.g.,
42# ('192.168.0.0', 24))
43RESTRICTED_SUBNETS = []
44restricted_subnets_list = CONFIG.get_config_value(
45        'CROS', 'restricted_subnets', type=list, default=[])
46# TODO(dshi): Remove the code to split subnet with `:` after R51 is off stable
47# channel, and update shadow config to use `/` as delimiter for consistency.
48for subnet in restricted_subnets_list:
49    ip, mask_bits = subnet.split('/') if '/' in subnet else subnet.split(':')
50    RESTRICTED_SUBNETS.append((ip, int(mask_bits)))
51
52# regex pattern for CLIENT/wireless_ssid_ config. For example, global config
53# can have following config in CLIENT section to indicate that hosts in subnet
54# 192.168.0.1/24 should use wireless ssid of `ssid_1`
55# wireless_ssid_192.168.0.1/24: ssid_1
56WIRELESS_SSID_PATTERN = 'wireless_ssid_(.*)/(\d+)'
57
58def ping(host, deadline=None, tries=None, timeout=60):
59    """Attempt to ping |host|.
60
61    Shell out to 'ping' to try to reach |host| for |timeout| seconds.
62    Returns exit code of ping.
63
64    Per 'man ping', if you specify BOTH |deadline| and |tries|, ping only
65    returns 0 if we get responses to |tries| pings within |deadline| seconds.
66
67    Specifying |deadline| or |count| alone should return 0 as long as
68    some packets receive responses.
69
70    @param host: the host to ping.
71    @param deadline: seconds within which |tries| pings must succeed.
72    @param tries: number of pings to send.
73    @param timeout: number of seconds after which to kill 'ping' command.
74    @return exit code of ping command.
75    """
76    args = [host]
77    if deadline:
78        args.append('-w%d' % deadline)
79    if tries:
80        args.append('-c%d' % tries)
81    return base_utils.run('ping', args=args,
82                          ignore_status=True, timeout=timeout,
83                          stdout_tee=base_utils.TEE_TO_LOGS,
84                          stderr_tee=base_utils.TEE_TO_LOGS).exit_status
85
86
87def host_is_in_lab_zone(hostname):
88    """Check if the host is in the CLIENT.dns_zone.
89
90    @param hostname: The hostname to check.
91    @returns True if hostname.dns_zone resolves, otherwise False.
92    """
93    host_parts = hostname.split('.')
94    dns_zone = CONFIG.get_config_value('CLIENT', 'dns_zone', default=None)
95    fqdn = '%s.%s' % (host_parts[0], dns_zone)
96    try:
97        socket.gethostbyname(fqdn)
98        return True
99    except socket.gaierror:
100        return False
101
102
103def host_could_be_in_afe(hostname):
104    """Check if the host could be in Autotest Front End.
105
106    Report whether or not a host could be in AFE, without actually
107    consulting AFE. This method exists because some systems are in the
108    lab zone, but not actually managed by AFE.
109
110    @param hostname: The hostname to check.
111    @returns True if hostname is in lab zone, and does not match *-dev-*
112    """
113    # Do the 'dev' check first, so that we skip DNS lookup if the
114    # hostname matches. This should give us greater resilience to lab
115    # failures.
116    return (hostname.find('-dev-') == -1) and host_is_in_lab_zone(hostname)
117
118
119def get_chrome_version(job_views):
120    """
121    Retrieves the version of the chrome binary associated with a job.
122
123    When a test runs we query the chrome binary for it's version and drop
124    that value into a client keyval. To retrieve the chrome version we get all
125    the views associated with a test from the db, including those of the
126    server and client jobs, and parse the version out of the first test view
127    that has it. If we never ran a single test in the suite the job_views
128    dictionary will not contain a chrome version.
129
130    This method cannot retrieve the chrome version from a dictionary that
131    does not conform to the structure of an autotest tko view.
132
133    @param job_views: a list of a job's result views, as returned by
134                      the get_detailed_test_views method in rpc_interface.
135    @return: The chrome version string, or None if one can't be found.
136    """
137
138    # Aborted jobs have no views.
139    if not job_views:
140        return None
141
142    for view in job_views:
143        if (view.get('attributes')
144            and constants.CHROME_VERSION in view['attributes'].keys()):
145
146            return view['attributes'].get(constants.CHROME_VERSION)
147
148    logging.warning('Could not find chrome version for failure.')
149    return None
150
151
152def get_interface_mac_address(interface):
153    """Return the MAC address of a given interface.
154
155    @param interface: Interface to look up the MAC address of.
156    """
157    interface_link = base_utils.run(
158            'ip addr show %s | grep link/ether' % interface).stdout
159    # The output will be in the format of:
160    # 'link/ether <mac> brd ff:ff:ff:ff:ff:ff'
161    return interface_link.split()[1]
162
163
164def get_moblab_id():
165    """Gets the moblab random id.
166
167    The random id file is cached on disk. If it does not exist, a new file is
168    created the first time.
169
170    @returns the moblab random id.
171    """
172    moblab_id_filepath = '/home/moblab/.moblab_id'
173    if os.path.exists(moblab_id_filepath):
174        with open(moblab_id_filepath, 'r') as moblab_id_file:
175            random_id = moblab_id_file.read()
176    else:
177        random_id = uuid.uuid1()
178        with open(moblab_id_filepath, 'w') as moblab_id_file:
179            moblab_id_file.write('%s' % random_id)
180    return random_id
181
182
183def get_offload_gsuri():
184    """Return the GSURI to offload test results to.
185
186    For the normal use case this is the results_storage_server in the
187    global_config.
188
189    However partners using Moblab will be offloading their results to a
190    subdirectory of their image storage buckets. The subdirectory is
191    determined by the MAC Address of the Moblab device.
192
193    @returns gsuri to offload test results to.
194    """
195    # For non-moblab, use results_storage_server or default.
196    if not lsbrelease_utils.is_moblab():
197        return DEFAULT_OFFLOAD_GSURI
198
199    # For moblab, use results_storage_server or image_storage_server as bucket
200    # name and mac-address/moblab_id as path.
201    gsuri = DEFAULT_OFFLOAD_GSURI
202    if not gsuri:
203        gsuri = CONFIG.get_config_value('CROS', 'image_storage_server')
204
205    return '%sresults/%s/%s/' % (
206            gsuri, get_interface_mac_address(MOBLAB_ETH), get_moblab_id())
207
208
209# TODO(petermayo): crosbug.com/31826 Share this with _GsUpload in
210# //chromite.git/buildbot/prebuilt.py somewhere/somehow
211def gs_upload(local_file, remote_file, acl, result_dir=None,
212              transfer_timeout=300, acl_timeout=300):
213    """Upload to GS bucket.
214
215    @param local_file: Local file to upload
216    @param remote_file: Remote location to upload the local_file to.
217    @param acl: name or file used for controlling access to the uploaded
218                file.
219    @param result_dir: Result directory if you want to add tracing to the
220                       upload.
221    @param transfer_timeout: Timeout for this upload call.
222    @param acl_timeout: Timeout for the acl call needed to confirm that
223                        the uploader has permissions to execute the upload.
224
225    @raise CmdError: the exit code of the gsutil call was not 0.
226
227    @returns True/False - depending on if the upload succeeded or failed.
228    """
229    # https://developers.google.com/storage/docs/accesscontrol#extension
230    CANNED_ACLS = ['project-private', 'private', 'public-read',
231                   'public-read-write', 'authenticated-read',
232                   'bucket-owner-read', 'bucket-owner-full-control']
233    _GSUTIL_BIN = 'gsutil'
234    acl_cmd = None
235    if acl in CANNED_ACLS:
236        cmd = '%s cp -a %s %s %s' % (_GSUTIL_BIN, acl, local_file, remote_file)
237    else:
238        # For private uploads we assume that the overlay board is set up
239        # properly and a googlestore_acl.xml is present, if not this script
240        # errors
241        cmd = '%s cp -a private %s %s' % (_GSUTIL_BIN, local_file, remote_file)
242        if not os.path.exists(acl):
243            logging.error('Unable to find ACL File %s.', acl)
244            return False
245        acl_cmd = '%s setacl %s %s' % (_GSUTIL_BIN, acl, remote_file)
246    if not result_dir:
247        base_utils.run(cmd, timeout=transfer_timeout, verbose=True)
248        if acl_cmd:
249            base_utils.run(acl_cmd, timeout=acl_timeout, verbose=True)
250        return True
251    with open(os.path.join(result_dir, 'tracing'), 'w') as ftrace:
252        ftrace.write('Preamble\n')
253        base_utils.run(cmd, timeout=transfer_timeout, verbose=True,
254                       stdout_tee=ftrace, stderr_tee=ftrace)
255        if acl_cmd:
256            ftrace.write('\nACL setting\n')
257            # Apply the passed in ACL xml file to the uploaded object.
258            base_utils.run(acl_cmd, timeout=acl_timeout, verbose=True,
259                           stdout_tee=ftrace, stderr_tee=ftrace)
260        ftrace.write('Postamble\n')
261        return True
262
263
264def gs_ls(uri_pattern):
265    """Returns a list of URIs that match a given pattern.
266
267    @param uri_pattern: a GS URI pattern, may contain wildcards
268
269    @return A list of URIs matching the given pattern.
270
271    @raise CmdError: the gsutil command failed.
272
273    """
274    gs_cmd = ' '.join(['gsutil', 'ls', uri_pattern])
275    result = base_utils.system_output(gs_cmd).splitlines()
276    return [path.rstrip() for path in result if path]
277
278
279def nuke_pids(pid_list, signal_queue=[signal.SIGTERM, signal.SIGKILL]):
280    """
281    Given a list of pid's, kill them via an esclating series of signals.
282
283    @param pid_list: List of PID's to kill.
284    @param signal_queue: Queue of signals to send the PID's to terminate them.
285
286    @return: A mapping of the signal name to the number of processes it
287        was sent to.
288    """
289    sig_count = {}
290    # Though this is slightly hacky it beats hardcoding names anyday.
291    sig_names = dict((k, v) for v, k in signal.__dict__.iteritems()
292                     if v.startswith('SIG'))
293    for sig in signal_queue:
294        logging.debug('Sending signal %s to the following pids:', sig)
295        sig_count[sig_names.get(sig, 'unknown_signal')] = len(pid_list)
296        for pid in pid_list:
297            logging.debug('Pid %d', pid)
298            try:
299                os.kill(pid, sig)
300            except OSError:
301                # The process may have died from a previous signal before we
302                # could kill it.
303                pass
304        if sig == signal.SIGKILL:
305            return sig_count
306        pid_list = [pid for pid in pid_list if base_utils.pid_is_alive(pid)]
307        if not pid_list:
308            break
309        time.sleep(CHECK_PID_IS_ALIVE_TIMEOUT)
310    failed_list = []
311    for pid in pid_list:
312        if base_utils.pid_is_alive(pid):
313            failed_list.append('Could not kill %d for process name: %s.' % pid,
314                               base_utils.get_process_name(pid))
315    if failed_list:
316        raise error.AutoservRunError('Following errors occured: %s' %
317                                     failed_list, None)
318    return sig_count
319
320
321def externalize_host(host):
322    """Returns an externally accessible host name.
323
324    @param host: a host name or address (string)
325
326    @return An externally visible host name or address
327
328    """
329    return socket.gethostname() if host in _LOCAL_HOST_LIST else host
330
331
332def urlopen_socket_timeout(url, data=None, timeout=5):
333    """
334    Wrapper to urllib2.urlopen with a socket timeout.
335
336    This method will convert all socket timeouts to
337    TimeoutExceptions, so we can use it in conjunction
338    with the rpc retry decorator and continue to handle
339    other URLErrors as we see fit.
340
341    @param url: The url to open.
342    @param data: The data to send to the url (eg: the urlencoded dictionary
343                 used with a POST call).
344    @param timeout: The timeout for this urlopen call.
345
346    @return: The response of the urlopen call.
347
348    @raises: error.TimeoutException when a socket timeout occurs.
349             urllib2.URLError for errors that not caused by timeout.
350             urllib2.HTTPError for errors like 404 url not found.
351    """
352    old_timeout = socket.getdefaulttimeout()
353    socket.setdefaulttimeout(timeout)
354    try:
355        return urllib2.urlopen(url, data=data)
356    except urllib2.URLError as e:
357        if type(e.reason) is socket.timeout:
358            raise error.TimeoutException(str(e))
359        raise
360    finally:
361        socket.setdefaulttimeout(old_timeout)
362
363
364def parse_chrome_version(version_string):
365    """
366    Parse a chrome version string and return version and milestone.
367
368    Given a chrome version of the form "W.X.Y.Z", return "W.X.Y.Z" as
369    the version and "W" as the milestone.
370
371    @param version_string: Chrome version string.
372    @return: a tuple (chrome_version, milestone). If the incoming version
373             string is not of the form "W.X.Y.Z", chrome_version will
374             be set to the incoming "version_string" argument and the
375             milestone will be set to the empty string.
376    """
377    match = re.search('(\d+)\.\d+\.\d+\.\d+', version_string)
378    ver = match.group(0) if match else version_string
379    milestone = match.group(1) if match else ''
380    return ver, milestone
381
382
383def is_localhost(server):
384    """Check if server is equivalent to localhost.
385
386    @param server: Name of the server to check.
387
388    @return: True if given server is equivalent to localhost.
389
390    @raise socket.gaierror: If server name failed to be resolved.
391    """
392    if server in _LOCAL_HOST_LIST:
393        return True
394    try:
395        return (socket.gethostbyname(socket.gethostname()) ==
396                socket.gethostbyname(server))
397    except socket.gaierror:
398        logging.error('Failed to resolve server name %s.', server)
399        return False
400
401
402def is_puppylab_vm(server):
403    """Check if server is a virtual machine in puppylab.
404
405    In the virtual machine testing environment (i.e., puppylab), each
406    shard VM has a hostname like localhost:<port>.
407
408    @param server: Server name to check.
409
410    @return True if given server is a virtual machine in puppylab.
411
412    """
413    # TODO(mkryu): This is a puppylab specific hack. Please update
414    # this method if you have a better solution.
415    regex = re.compile(r'(.+):\d+')
416    m = regex.match(server)
417    if m:
418        return m.group(1) in _LOCAL_HOST_LIST
419    return False
420
421
422def get_function_arg_value(func, arg_name, args, kwargs):
423    """Get the value of the given argument for the function.
424
425    @param func: Function being called with given arguments.
426    @param arg_name: Name of the argument to look for value.
427    @param args: arguments for function to be called.
428    @param kwargs: keyword arguments for function to be called.
429
430    @return: The value of the given argument for the function.
431
432    @raise ValueError: If the argument is not listed function arguemnts.
433    @raise KeyError: If no value is found for the given argument.
434    """
435    if arg_name in kwargs:
436        return kwargs[arg_name]
437
438    argspec = inspect.getargspec(func)
439    index = argspec.args.index(arg_name)
440    try:
441        return args[index]
442    except IndexError:
443        try:
444            # The argument can use a default value. Reverse the default value
445            # so argument with default value can be counted from the last to
446            # the first.
447            return argspec.defaults[::-1][len(argspec.args) - index - 1]
448        except IndexError:
449            raise KeyError('Argument %s is not given a value. argspec: %s, '
450                           'args:%s, kwargs:%s' %
451                           (arg_name, argspec, args, kwargs))
452
453
454def version_match(build_version, release_version, update_url=''):
455    """Compare release versino from lsb-release with cros-version label.
456
457    build_version is a string based on build name. It is prefixed with builder
458    info and branch ID, e.g., lumpy-release/R43-6809.0.0. It may not include
459    builder info, e.g., lumpy-release, in which case, update_url shall be passed
460    in to determine if the build is a trybot or pgo-generate build.
461    release_version is retrieved from lsb-release.
462    These two values might not match exactly.
463
464    The method is designed to compare version for following 6 scenarios with
465    samples of build version and expected release version:
466    1. trybot non-release build (paladin, pre-cq or test-ap build).
467    build version:   trybot-lumpy-paladin/R27-3837.0.0-b123
468    release version: 3837.0.2013_03_21_1340
469
470    2. trybot release build.
471    build version:   trybot-lumpy-release/R27-3837.0.0-b456
472    release version: 3837.0.0
473
474    3. buildbot official release build.
475    build version:   lumpy-release/R27-3837.0.0
476    release version: 3837.0.0
477
478    4. non-official paladin rc build.
479    build version:   lumpy-paladin/R27-3878.0.0-rc7
480    release version: 3837.0.0-rc7
481
482    5. chrome-perf build.
483    build version:   lumpy-chrome-perf/R28-3837.0.0-b2996
484    release version: 3837.0.0
485
486    6. pgo-generate build.
487    build version:   lumpy-release-pgo-generate/R28-3837.0.0-b2996
488    release version: 3837.0.0-pgo-generate
489
490    TODO: This logic has a bug if a trybot paladin build failed to be
491    installed in a DUT running an older trybot paladin build with same
492    platform number, but different build number (-b###). So to conclusively
493    determine if a tryjob paladin build is imaged successfully, we may need
494    to find out the date string from update url.
495
496    @param build_version: Build name for cros version, e.g.
497                          peppy-release/R43-6809.0.0 or R43-6809.0.0
498    @param release_version: Release version retrieved from lsb-release,
499                            e.g., 6809.0.0
500    @param update_url: Update url which include the full builder information.
501                       Default is set to empty string.
502
503    @return: True if the values match, otherwise returns False.
504    """
505    # If the build is from release, CQ or PFQ builder, cros-version label must
506    # be ended with release version in lsb-release.
507    if build_version.endswith(release_version):
508        return True
509
510    # Remove R#- and -b# at the end of build version
511    stripped_version = re.sub(r'(R\d+-|-b\d+)', '', build_version)
512    # Trim the builder info, e.g., trybot-lumpy-paladin/
513    stripped_version = stripped_version.split('/')[-1]
514
515    is_trybot_non_release_build = (
516            re.match(r'.*trybot-.+-(paladin|pre-cq|test-ap)', build_version) or
517            re.match(r'.*trybot-.+-(paladin|pre-cq|test-ap)', update_url))
518
519    # Replace date string with 0 in release_version
520    release_version_no_date = re.sub(r'\d{4}_\d{2}_\d{2}_\d+', '0',
521                                    release_version)
522    has_date_string = release_version != release_version_no_date
523
524    is_pgo_generate_build = (
525            re.match(r'.+-pgo-generate', build_version) or
526            re.match(r'.+-pgo-generate', update_url))
527
528    # Remove |-pgo-generate| in release_version
529    release_version_no_pgo = release_version.replace('-pgo-generate', '')
530    has_pgo_generate = release_version != release_version_no_pgo
531
532    if is_trybot_non_release_build:
533        if not has_date_string:
534            logging.error('A trybot paladin or pre-cq build is expected. '
535                          'Version "%s" is not a paladin or pre-cq  build.',
536                          release_version)
537            return False
538        return stripped_version == release_version_no_date
539    elif is_pgo_generate_build:
540        if not has_pgo_generate:
541            logging.error('A pgo-generate build is expected. Version '
542                          '"%s" is not a pgo-generate build.',
543                          release_version)
544            return False
545        return stripped_version == release_version_no_pgo
546    else:
547        if has_date_string:
548            logging.error('Unexpected date found in a non trybot paladin or '
549                          'pre-cq build.')
550            return False
551        # Versioned build, i.e., rc or release build.
552        return stripped_version == release_version
553
554
555def get_real_user():
556    """Get the real user that runs the script.
557
558    The function check environment variable SUDO_USER for the user if the
559    script is run with sudo. Otherwise, it returns the value of environment
560    variable USER.
561
562    @return: The user name that runs the script.
563
564    """
565    user = os.environ.get('SUDO_USER')
566    if not user:
567        user = os.environ.get('USER')
568    return user
569
570
571def sudo_require_password():
572    """Test if the process can run sudo command without using password.
573
574    @return: True if the process needs password to run sudo command.
575
576    """
577    try:
578        base_utils.run('sudo -n true')
579        return False
580    except error.CmdError:
581        logging.warn('sudo command requires password.')
582        return True
583
584
585def is_in_container():
586    """Check if the process is running inside a container.
587
588    @return: True if the process is running inside a container, otherwise False.
589    """
590    result = base_utils.run('grep -q "/lxc/" /proc/1/cgroup',
591                            verbose=False, ignore_status=True)
592    return result.exit_status == 0
593
594
595def is_flash_installed():
596    """
597    The Adobe Flash binary is only distributed with internal builds.
598    """
599    return (os.path.exists('/opt/google/chrome/pepper/libpepflashplayer.so')
600        and os.path.exists('/opt/google/chrome/pepper/pepper-flash.info'))
601
602
603def verify_flash_installed():
604    """
605    The Adobe Flash binary is only distributed with internal builds.
606    Warn users of public builds of the extra dependency.
607    """
608    if not is_flash_installed():
609        raise error.TestNAError('No Adobe Flash binary installed.')
610
611
612def is_in_same_subnet(ip_1, ip_2, mask_bits=24):
613    """Check if two IP addresses are in the same subnet with given mask bits.
614
615    The two IP addresses are string of IPv4, e.g., '192.168.0.3'.
616
617    @param ip_1: First IP address to compare.
618    @param ip_2: Second IP address to compare.
619    @param mask_bits: Number of mask bits for subnet comparison. Default to 24.
620
621    @return: True if the two IP addresses are in the same subnet.
622
623    """
624    mask = ((2L<<mask_bits-1) -1)<<(32-mask_bits)
625    ip_1_num = struct.unpack('!I', socket.inet_aton(ip_1))[0]
626    ip_2_num = struct.unpack('!I', socket.inet_aton(ip_2))[0]
627    return ip_1_num & mask == ip_2_num & mask
628
629
630def get_ip_address(hostname):
631    """Get the IP address of given hostname.
632
633    @param hostname: Hostname of a DUT.
634
635    @return: The IP address of given hostname. None if failed to resolve
636             hostname.
637    """
638    try:
639        if hostname:
640            return socket.gethostbyname(hostname)
641    except socket.gaierror as e:
642        logging.error('Failed to get IP address of %s, error: %s.', hostname, e)
643
644
645def get_servers_in_same_subnet(host_ip, mask_bits, servers=None,
646                               server_ip_map=None):
647    """Get the servers in the same subnet of the given host ip.
648
649    @param host_ip: The IP address of a dut to look for devserver.
650    @param mask_bits: Number of mask bits.
651    @param servers: A list of servers to be filtered by subnet specified by
652                    host_ip and mask_bits.
653    @param server_ip_map: A map between the server name and its IP address.
654            The map can be pre-built for better performance, e.g., when
655            allocating a drone for an agent task.
656
657    @return: A list of servers in the same subnet of the given host ip.
658
659    """
660    matched_servers = []
661    if not servers and not server_ip_map:
662        raise ValueError('Either `servers` or `server_ip_map` must be given.')
663    if not servers:
664        servers = server_ip_map.keys()
665    # Make sure server_ip_map is an empty dict if it's not set.
666    if not server_ip_map:
667        server_ip_map = {}
668    for server in servers:
669        server_ip = server_ip_map.get(server, get_ip_address(server))
670        if server_ip and is_in_same_subnet(server_ip, host_ip, mask_bits):
671            matched_servers.append(server)
672    return matched_servers
673
674
675def get_restricted_subnet(hostname, restricted_subnets=RESTRICTED_SUBNETS):
676    """Get the restricted subnet of given hostname.
677
678    @param hostname: Name of the host to look for matched restricted subnet.
679    @param restricted_subnets: A list of restricted subnets, default is set to
680            RESTRICTED_SUBNETS.
681
682    @return: A tuple of (subnet_ip, mask_bits), which defines a restricted
683             subnet.
684    """
685    host_ip = get_ip_address(hostname)
686    if not host_ip:
687        return
688    for subnet_ip, mask_bits in restricted_subnets:
689        if is_in_same_subnet(subnet_ip, host_ip, mask_bits):
690            return subnet_ip, mask_bits
691
692
693def get_wireless_ssid(hostname):
694    """Get the wireless ssid based on given hostname.
695
696    The method tries to locate the wireless ssid in the same subnet of given
697    hostname first. If none is found, it returns the default setting in
698    CLIENT/wireless_ssid.
699
700    @param hostname: Hostname of the test device.
701
702    @return: wireless ssid for the test device.
703    """
704    default_ssid = CONFIG.get_config_value('CLIENT', 'wireless_ssid',
705                                           default=None)
706    host_ip = get_ip_address(hostname)
707    if not host_ip:
708        return default_ssid
709
710    # Get all wireless ssid in the global config.
711    ssids = CONFIG.get_config_value_regex('CLIENT', WIRELESS_SSID_PATTERN)
712
713    # There could be multiple subnet matches, pick the one with most strict
714    # match, i.e., the one with highest maskbit.
715    matched_ssid = default_ssid
716    matched_maskbit = -1
717    for key, value in ssids.items():
718        # The config key filtered by regex WIRELESS_SSID_PATTERN has a format of
719        # wireless_ssid_[subnet_ip]/[maskbit], for example:
720        # wireless_ssid_192.168.0.1/24
721        # Following line extract the subnet ip and mask bit from the key name.
722        match = re.match(WIRELESS_SSID_PATTERN, key)
723        subnet_ip, maskbit = match.groups()
724        maskbit = int(maskbit)
725        if (is_in_same_subnet(subnet_ip, host_ip, maskbit) and
726            maskbit > matched_maskbit):
727            matched_ssid = value
728            matched_maskbit = maskbit
729    return matched_ssid
730
731
732def parse_launch_control_build(build_name):
733    """Get branch, target, build_id from the given Launch Control build_name.
734
735    @param build_name: Name of a Launch Control build, should be formated as
736                       branch/target/build_id
737
738    @return: Tuple of branch, target, build_id
739    @raise ValueError: If the build_name is not correctly formated.
740    """
741    branch, target, build_id = build_name.split('/')
742    return branch, target, build_id
743
744
745def parse_android_target(target):
746    """Get board and build type from the given target.
747
748    @param target: Name of an Android build target, e.g., shamu-eng.
749
750    @return: Tuple of board, build_type
751    @raise ValueError: If the target is not correctly formated.
752    """
753    board, build_type = target.split('-')
754    return board, build_type
755
756
757def parse_launch_control_target(target):
758    """Parse the build target and type from a Launch Control target.
759
760    The Launch Control target has the format of build_target-build_type, e.g.,
761    shamu-eng or dragonboard-userdebug. This method extracts the build target
762    and type from the target name.
763
764    @param target: Name of a Launch Control target, e.g., shamu-eng.
765
766    @return: (build_target, build_type), e.g., ('shamu', 'userdebug')
767    """
768    match = re.match('(?P<build_target>.+)-(?P<build_type>[^-]+)', target)
769    if match:
770        return match.group('build_target'), match.group('build_type')
771    else:
772        return None, None
773
774
775def is_launch_control_build(build):
776    """Check if a given build is a Launch Control build.
777
778    @param build: Name of a build, e.g.,
779                  ChromeOS build: daisy-release/R50-1234.0.0
780                  Launch Control build: git_mnc_release/shamu-eng
781
782    @return: True if the build name matches the pattern of a Launch Control
783             build, False otherwise.
784    """
785    try:
786        _, target, _ = parse_launch_control_build(build)
787        build_target, _ = parse_launch_control_target(target)
788        if build_target:
789            return True
790    except ValueError:
791        # parse_launch_control_build or parse_launch_control_target failed.
792        pass
793    return False
794
795
796def which(exec_file):
797    """Finds an executable file.
798
799    If the file name contains a path component, it is checked as-is.
800    Otherwise, we check with each of the path components found in the system
801    PATH prepended. This behavior is similar to the 'which' command-line tool.
802
803    @param exec_file: Name or path to desired executable.
804
805    @return: An actual path to the executable, or None if not found.
806    """
807    if os.path.dirname(exec_file):
808        return exec_file if os.access(exec_file, os.X_OK) else None
809    sys_path = os.environ.get('PATH')
810    prefix_list = sys_path.split(os.pathsep) if sys_path else []
811    for prefix in prefix_list:
812        path = os.path.join(prefix, exec_file)
813        if os.access(path, os.X_OK):
814            return path
815
816
817class TimeoutError(error.TestError):
818    """Error raised when we time out when waiting on a condition."""
819    pass
820
821
822def poll_for_condition(condition,
823                       exception=None,
824                       timeout=10,
825                       sleep_interval=0.1,
826                       desc=None):
827    """Polls until a condition becomes true.
828
829    @param condition: function taking no args and returning bool
830    @param exception: exception to throw if condition doesn't become true
831    @param timeout: maximum number of seconds to wait
832    @param sleep_interval: time to sleep between polls
833    @param desc: description of default TimeoutError used if 'exception' is
834                 None
835
836    @return The true value that caused the poll loop to terminate.
837
838    @raise 'exception' arg if supplied; TimeoutError otherwise
839    """
840    start_time = time.time()
841    while True:
842        value = condition()
843        if value:
844            return value
845        if time.time() + sleep_interval - start_time > timeout:
846            if exception:
847                logging.error(exception)
848                raise exception
849
850            if desc:
851                desc = 'Timed out waiting for condition: ' + desc
852            else:
853                desc = 'Timed out waiting for unnamed condition'
854            logging.error(desc)
855            raise TimeoutError(desc)
856
857        time.sleep(sleep_interval)
858