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