site_utils.py revision aa401d354435497a4195efad12c89912d5028187
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 has_systemd():
455    """Check if the host is running systemd.
456
457    @return: True if the host uses systemd, otherwise returns False.
458    """
459    return os.path.basename(os.readlink('/proc/1/exe')) == 'systemd'
460
461
462def version_match(build_version, release_version, update_url=''):
463    """Compare release versino from lsb-release with cros-version label.
464
465    build_version is a string based on build name. It is prefixed with builder
466    info and branch ID, e.g., lumpy-release/R43-6809.0.0. It may not include
467    builder info, e.g., lumpy-release, in which case, update_url shall be passed
468    in to determine if the build is a trybot or pgo-generate build.
469    release_version is retrieved from lsb-release.
470    These two values might not match exactly.
471
472    The method is designed to compare version for following 6 scenarios with
473    samples of build version and expected release version:
474    1. trybot non-release build (paladin, pre-cq or test-ap build).
475    build version:   trybot-lumpy-paladin/R27-3837.0.0-b123
476    release version: 3837.0.2013_03_21_1340
477
478    2. trybot release build.
479    build version:   trybot-lumpy-release/R27-3837.0.0-b456
480    release version: 3837.0.0
481
482    3. buildbot official release build.
483    build version:   lumpy-release/R27-3837.0.0
484    release version: 3837.0.0
485
486    4. non-official paladin rc build.
487    build version:   lumpy-paladin/R27-3878.0.0-rc7
488    release version: 3837.0.0-rc7
489
490    5. chrome-perf build.
491    build version:   lumpy-chrome-perf/R28-3837.0.0-b2996
492    release version: 3837.0.0
493
494    6. pgo-generate build.
495    build version:   lumpy-release-pgo-generate/R28-3837.0.0-b2996
496    release version: 3837.0.0-pgo-generate
497
498    TODO: This logic has a bug if a trybot paladin build failed to be
499    installed in a DUT running an older trybot paladin build with same
500    platform number, but different build number (-b###). So to conclusively
501    determine if a tryjob paladin build is imaged successfully, we may need
502    to find out the date string from update url.
503
504    @param build_version: Build name for cros version, e.g.
505                          peppy-release/R43-6809.0.0 or R43-6809.0.0
506    @param release_version: Release version retrieved from lsb-release,
507                            e.g., 6809.0.0
508    @param update_url: Update url which include the full builder information.
509                       Default is set to empty string.
510
511    @return: True if the values match, otherwise returns False.
512    """
513    # If the build is from release, CQ or PFQ builder, cros-version label must
514    # be ended with release version in lsb-release.
515    if build_version.endswith(release_version):
516        return True
517
518    # Remove R#- and -b# at the end of build version
519    stripped_version = re.sub(r'(R\d+-|-b\d+)', '', build_version)
520    # Trim the builder info, e.g., trybot-lumpy-paladin/
521    stripped_version = stripped_version.split('/')[-1]
522
523    is_trybot_non_release_build = (
524            re.match(r'.*trybot-.+-(paladin|pre-cq|test-ap)', build_version) or
525            re.match(r'.*trybot-.+-(paladin|pre-cq|test-ap)', update_url))
526
527    # Replace date string with 0 in release_version
528    release_version_no_date = re.sub(r'\d{4}_\d{2}_\d{2}_\d+', '0',
529                                    release_version)
530    has_date_string = release_version != release_version_no_date
531
532    is_pgo_generate_build = (
533            re.match(r'.+-pgo-generate', build_version) or
534            re.match(r'.+-pgo-generate', update_url))
535
536    # Remove |-pgo-generate| in release_version
537    release_version_no_pgo = release_version.replace('-pgo-generate', '')
538    has_pgo_generate = release_version != release_version_no_pgo
539
540    if is_trybot_non_release_build:
541        if not has_date_string:
542            logging.error('A trybot paladin or pre-cq build is expected. '
543                          'Version "%s" is not a paladin or pre-cq  build.',
544                          release_version)
545            return False
546        return stripped_version == release_version_no_date
547    elif is_pgo_generate_build:
548        if not has_pgo_generate:
549            logging.error('A pgo-generate build is expected. Version '
550                          '"%s" is not a pgo-generate build.',
551                          release_version)
552            return False
553        return stripped_version == release_version_no_pgo
554    else:
555        if has_date_string:
556            logging.error('Unexpected date found in a non trybot paladin or '
557                          'pre-cq build.')
558            return False
559        # Versioned build, i.e., rc or release build.
560        return stripped_version == release_version
561
562
563def get_real_user():
564    """Get the real user that runs the script.
565
566    The function check environment variable SUDO_USER for the user if the
567    script is run with sudo. Otherwise, it returns the value of environment
568    variable USER.
569
570    @return: The user name that runs the script.
571
572    """
573    user = os.environ.get('SUDO_USER')
574    if not user:
575        user = os.environ.get('USER')
576    return user
577
578
579def get_service_pid(service_name):
580    """Return pid of service.
581
582    @param service_name: string name of service.
583
584    @return: pid or 0 if service is not running.
585    """
586    if has_systemd():
587        # systemctl show prints 'MainPID=0' if the service is not running.
588        cmd_result = base_utils.run('systemctl show -p MainPID %s' %
589                                    service_name, ignore_status=True)
590        return int(cmd_result.stdout.split('=')[1])
591    else:
592        cmd_result = base_utils.run('status %s' % service_name,
593                                        ignore_status=True)
594        if 'start/running' in cmd_result.stdout:
595            return int(cmd_result.stdout.split()[3])
596        return 0
597
598
599def control_service(service_name, action='start', ignore_status=True):
600    """Controls a service. It can be used to start, stop or restart
601    a service.
602
603    @param service_name: string service to be restarted.
604
605    @param action: string choice of action to control command.
606
607    @param ignore_status: boolean ignore if system command fails.
608
609    @return: status code of the executed command.
610    """
611    if action not in ('start', 'stop', 'restart'):
612        raise ValueError('Unknown action supplied as parameter.')
613
614    control_cmd = action + ' ' + service_name
615    if has_systemd():
616        control_cmd = 'systemctl ' + control_cmd
617    return base_utils.system(control_cmd, ignore_status=ignore_status)
618
619
620def restart_service(service_name, ignore_status=True):
621    """Restarts a service
622
623    @param service_name: string service to be restarted.
624
625    @param ignore_status: boolean ignore if system command fails.
626
627    @return: status code of the executed command.
628    """
629    return control_service(service_name, action='restart', ignore_status=ignore_status)
630
631
632def start_service(service_name, ignore_status=True):
633    """Starts a service
634
635    @param service_name: string service to be started.
636
637    @param ignore_status: boolean ignore if system command fails.
638
639    @return: status code of the executed command.
640    """
641    return control_service(service_name, action='start', ignore_status=ignore_status)
642
643
644def stop_service(service_name, ignore_status=True):
645    """Stops a service
646
647    @param service_name: string service to be stopped.
648
649    @param ignore_status: boolean ignore if system command fails.
650
651    @return: status code of the executed command.
652    """
653    return control_service(service_name, action='stop', ignore_status=ignore_status)
654
655
656def sudo_require_password():
657    """Test if the process can run sudo command without using password.
658
659    @return: True if the process needs password to run sudo command.
660
661    """
662    try:
663        base_utils.run('sudo -n true')
664        return False
665    except error.CmdError:
666        logging.warn('sudo command requires password.')
667        return True
668
669
670def is_in_container():
671    """Check if the process is running inside a container.
672
673    @return: True if the process is running inside a container, otherwise False.
674    """
675    result = base_utils.run('grep -q "/lxc/" /proc/1/cgroup',
676                            verbose=False, ignore_status=True)
677    return result.exit_status == 0
678
679
680def is_flash_installed():
681    """
682    The Adobe Flash binary is only distributed with internal builds.
683    """
684    return (os.path.exists('/opt/google/chrome/pepper/libpepflashplayer.so')
685        and os.path.exists('/opt/google/chrome/pepper/pepper-flash.info'))
686
687
688def verify_flash_installed():
689    """
690    The Adobe Flash binary is only distributed with internal builds.
691    Warn users of public builds of the extra dependency.
692    """
693    if not is_flash_installed():
694        raise error.TestNAError('No Adobe Flash binary installed.')
695
696
697def is_in_same_subnet(ip_1, ip_2, mask_bits=24):
698    """Check if two IP addresses are in the same subnet with given mask bits.
699
700    The two IP addresses are string of IPv4, e.g., '192.168.0.3'.
701
702    @param ip_1: First IP address to compare.
703    @param ip_2: Second IP address to compare.
704    @param mask_bits: Number of mask bits for subnet comparison. Default to 24.
705
706    @return: True if the two IP addresses are in the same subnet.
707
708    """
709    mask = ((2L<<mask_bits-1) -1)<<(32-mask_bits)
710    ip_1_num = struct.unpack('!I', socket.inet_aton(ip_1))[0]
711    ip_2_num = struct.unpack('!I', socket.inet_aton(ip_2))[0]
712    return ip_1_num & mask == ip_2_num & mask
713
714
715def get_ip_address(hostname):
716    """Get the IP address of given hostname.
717
718    @param hostname: Hostname of a DUT.
719
720    @return: The IP address of given hostname. None if failed to resolve
721             hostname.
722    """
723    try:
724        if hostname:
725            return socket.gethostbyname(hostname)
726    except socket.gaierror as e:
727        logging.error('Failed to get IP address of %s, error: %s.', hostname, e)
728
729
730def get_servers_in_same_subnet(host_ip, mask_bits, servers=None,
731                               server_ip_map=None):
732    """Get the servers in the same subnet of the given host ip.
733
734    @param host_ip: The IP address of a dut to look for devserver.
735    @param mask_bits: Number of mask bits.
736    @param servers: A list of servers to be filtered by subnet specified by
737                    host_ip and mask_bits.
738    @param server_ip_map: A map between the server name and its IP address.
739            The map can be pre-built for better performance, e.g., when
740            allocating a drone for an agent task.
741
742    @return: A list of servers in the same subnet of the given host ip.
743
744    """
745    matched_servers = []
746    if not servers and not server_ip_map:
747        raise ValueError('Either `servers` or `server_ip_map` must be given.')
748    if not servers:
749        servers = server_ip_map.keys()
750    # Make sure server_ip_map is an empty dict if it's not set.
751    if not server_ip_map:
752        server_ip_map = {}
753    for server in servers:
754        server_ip = server_ip_map.get(server, get_ip_address(server))
755        if server_ip and is_in_same_subnet(server_ip, host_ip, mask_bits):
756            matched_servers.append(server)
757    return matched_servers
758
759
760def get_restricted_subnet(hostname, restricted_subnets=RESTRICTED_SUBNETS):
761    """Get the restricted subnet of given hostname.
762
763    @param hostname: Name of the host to look for matched restricted subnet.
764    @param restricted_subnets: A list of restricted subnets, default is set to
765            RESTRICTED_SUBNETS.
766
767    @return: A tuple of (subnet_ip, mask_bits), which defines a restricted
768             subnet.
769    """
770    host_ip = get_ip_address(hostname)
771    if not host_ip:
772        return
773    for subnet_ip, mask_bits in restricted_subnets:
774        if is_in_same_subnet(subnet_ip, host_ip, mask_bits):
775            return subnet_ip, mask_bits
776
777
778def get_wireless_ssid(hostname):
779    """Get the wireless ssid based on given hostname.
780
781    The method tries to locate the wireless ssid in the same subnet of given
782    hostname first. If none is found, it returns the default setting in
783    CLIENT/wireless_ssid.
784
785    @param hostname: Hostname of the test device.
786
787    @return: wireless ssid for the test device.
788    """
789    default_ssid = CONFIG.get_config_value('CLIENT', 'wireless_ssid',
790                                           default=None)
791    host_ip = get_ip_address(hostname)
792    if not host_ip:
793        return default_ssid
794
795    # Get all wireless ssid in the global config.
796    ssids = CONFIG.get_config_value_regex('CLIENT', WIRELESS_SSID_PATTERN)
797
798    # There could be multiple subnet matches, pick the one with most strict
799    # match, i.e., the one with highest maskbit.
800    matched_ssid = default_ssid
801    matched_maskbit = -1
802    for key, value in ssids.items():
803        # The config key filtered by regex WIRELESS_SSID_PATTERN has a format of
804        # wireless_ssid_[subnet_ip]/[maskbit], for example:
805        # wireless_ssid_192.168.0.1/24
806        # Following line extract the subnet ip and mask bit from the key name.
807        match = re.match(WIRELESS_SSID_PATTERN, key)
808        subnet_ip, maskbit = match.groups()
809        maskbit = int(maskbit)
810        if (is_in_same_subnet(subnet_ip, host_ip, maskbit) and
811            maskbit > matched_maskbit):
812            matched_ssid = value
813            matched_maskbit = maskbit
814    return matched_ssid
815
816
817def parse_launch_control_build(build_name):
818    """Get branch, target, build_id from the given Launch Control build_name.
819
820    @param build_name: Name of a Launch Control build, should be formated as
821                       branch/target/build_id
822
823    @return: Tuple of branch, target, build_id
824    @raise ValueError: If the build_name is not correctly formated.
825    """
826    branch, target, build_id = build_name.split('/')
827    return branch, target, build_id
828
829
830def parse_android_target(target):
831    """Get board and build type from the given target.
832
833    @param target: Name of an Android build target, e.g., shamu-eng.
834
835    @return: Tuple of board, build_type
836    @raise ValueError: If the target is not correctly formated.
837    """
838    board, build_type = target.split('-')
839    return board, build_type
840
841
842def parse_launch_control_target(target):
843    """Parse the build target and type from a Launch Control target.
844
845    The Launch Control target has the format of build_target-build_type, e.g.,
846    shamu-eng or dragonboard-userdebug. This method extracts the build target
847    and type from the target name.
848
849    @param target: Name of a Launch Control target, e.g., shamu-eng.
850
851    @return: (build_target, build_type), e.g., ('shamu', 'userdebug')
852    """
853    match = re.match('(?P<build_target>.+)-(?P<build_type>[^-]+)', target)
854    if match:
855        return match.group('build_target'), match.group('build_type')
856    else:
857        return None, None
858
859
860def is_launch_control_build(build):
861    """Check if a given build is a Launch Control build.
862
863    @param build: Name of a build, e.g.,
864                  ChromeOS build: daisy-release/R50-1234.0.0
865                  Launch Control build: git_mnc_release/shamu-eng
866
867    @return: True if the build name matches the pattern of a Launch Control
868             build, False otherwise.
869    """
870    try:
871        _, target, _ = parse_launch_control_build(build)
872        build_target, _ = parse_launch_control_target(target)
873        if build_target:
874            return True
875    except ValueError:
876        # parse_launch_control_build or parse_launch_control_target failed.
877        pass
878    return False
879
880
881def which(exec_file):
882    """Finds an executable file.
883
884    If the file name contains a path component, it is checked as-is.
885    Otherwise, we check with each of the path components found in the system
886    PATH prepended. This behavior is similar to the 'which' command-line tool.
887
888    @param exec_file: Name or path to desired executable.
889
890    @return: An actual path to the executable, or None if not found.
891    """
892    if os.path.dirname(exec_file):
893        return exec_file if os.access(exec_file, os.X_OK) else None
894    sys_path = os.environ.get('PATH')
895    prefix_list = sys_path.split(os.pathsep) if sys_path else []
896    for prefix in prefix_list:
897        path = os.path.join(prefix, exec_file)
898        if os.access(path, os.X_OK):
899            return path
900
901
902class TimeoutError(error.TestError):
903    """Error raised when we time out when waiting on a condition."""
904    pass
905
906
907def poll_for_condition(condition,
908                       exception=None,
909                       timeout=10,
910                       sleep_interval=0.1,
911                       desc=None):
912    """Polls until a condition becomes true.
913
914    @param condition: function taking no args and returning bool
915    @param exception: exception to throw if condition doesn't become true
916    @param timeout: maximum number of seconds to wait
917    @param sleep_interval: time to sleep between polls
918    @param desc: description of default TimeoutError used if 'exception' is
919                 None
920
921    @return The true value that caused the poll loop to terminate.
922
923    @raise 'exception' arg if supplied; TimeoutError otherwise
924    """
925    start_time = time.time()
926    while True:
927        value = condition()
928        if value:
929            return value
930        if time.time() + sleep_interval - start_time > timeout:
931            if exception:
932                logging.error(exception)
933                raise exception
934
935            if desc:
936                desc = 'Timed out waiting for condition: ' + desc
937            else:
938                desc = 'Timed out waiting for unnamed condition'
939            logging.error(desc)
940            raise TimeoutError(desc)
941
942        time.sleep(sleep_interval)
943