site_utils.py revision 7836d25b375a791ebc7b3d0d255c8732c4f35be2
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 time
12import urllib2
13import uuid
14
15from autotest_lib.client.common_lib import base_utils
16from autotest_lib.client.common_lib import error
17from autotest_lib.client.common_lib import global_config
18from autotest_lib.client.common_lib import lsbrelease_utils
19from autotest_lib.client.cros import constants
20
21
22# Keep checking if the pid is alive every second until the timeout (in seconds)
23CHECK_PID_IS_ALIVE_TIMEOUT = 6
24
25_LOCAL_HOST_LIST = ('localhost', '127.0.0.1')
26
27# The default address of a vm gateway.
28DEFAULT_VM_GATEWAY = '10.0.2.2'
29
30# Google Storage bucket URI to store results in.
31DEFAULT_OFFLOAD_GSURI = global_config.global_config.get_config_value(
32        'CROS', 'results_storage_server', default=None)
33
34# Default Moblab Ethernet Interface.
35MOBLAB_ETH = 'eth0'
36
37def ping(host, deadline=None, tries=None, timeout=60):
38    """Attempt to ping |host|.
39
40    Shell out to 'ping' to try to reach |host| for |timeout| seconds.
41    Returns exit code of ping.
42
43    Per 'man ping', if you specify BOTH |deadline| and |tries|, ping only
44    returns 0 if we get responses to |tries| pings within |deadline| seconds.
45
46    Specifying |deadline| or |count| alone should return 0 as long as
47    some packets receive responses.
48
49    @param host: the host to ping.
50    @param deadline: seconds within which |tries| pings must succeed.
51    @param tries: number of pings to send.
52    @param timeout: number of seconds after which to kill 'ping' command.
53    @return exit code of ping command.
54    """
55    args = [host]
56    if deadline:
57        args.append('-w%d' % deadline)
58    if tries:
59        args.append('-c%d' % tries)
60    return base_utils.run('ping', args=args,
61                          ignore_status=True, timeout=timeout,
62                          stdout_tee=base_utils.TEE_TO_LOGS,
63                          stderr_tee=base_utils.TEE_TO_LOGS).exit_status
64
65
66def host_is_in_lab_zone(hostname):
67    """Check if the host is in the CLIENT.dns_zone.
68
69    @param hostname: The hostname to check.
70    @returns True if hostname.dns_zone resolves, otherwise False.
71    """
72    host_parts = hostname.split('.')
73    dns_zone = global_config.global_config.get_config_value('CLIENT', 'dns_zone',
74                                                            default=None)
75    fqdn = '%s.%s' % (host_parts[0], dns_zone)
76    try:
77        socket.gethostbyname(fqdn)
78        return True
79    except socket.gaierror:
80        return False
81
82
83def get_chrome_version(job_views):
84    """
85    Retrieves the version of the chrome binary associated with a job.
86
87    When a test runs we query the chrome binary for it's version and drop
88    that value into a client keyval. To retrieve the chrome version we get all
89    the views associated with a test from the db, including those of the
90    server and client jobs, and parse the version out of the first test view
91    that has it. If we never ran a single test in the suite the job_views
92    dictionary will not contain a chrome version.
93
94    This method cannot retrieve the chrome version from a dictionary that
95    does not conform to the structure of an autotest tko view.
96
97    @param job_views: a list of a job's result views, as returned by
98                      the get_detailed_test_views method in rpc_interface.
99    @return: The chrome version string, or None if one can't be found.
100    """
101
102    # Aborted jobs have no views.
103    if not job_views:
104        return None
105
106    for view in job_views:
107        if (view.get('attributes')
108            and constants.CHROME_VERSION in view['attributes'].keys()):
109
110            return view['attributes'].get(constants.CHROME_VERSION)
111
112    logging.warning('Could not find chrome version for failure.')
113    return None
114
115
116def get_interface_mac_address(interface):
117    """Return the MAC address of a given interface.
118
119    @param interface: Interface to look up the MAC address of.
120    """
121    interface_link = base_utils.run(
122            'ip addr show %s | grep link/ether' % interface).stdout
123    # The output will be in the format of:
124    # 'link/ether <mac> brd ff:ff:ff:ff:ff:ff'
125    return interface_link.split()[1]
126
127
128def get_offload_gsuri():
129    """Return the GSURI to offload test results to.
130
131    For the normal use case this is the results_storage_server in the
132    global_config.
133
134    However partners using Moblab will be offloading their results to a
135    subdirectory of their image storage buckets. The subdirectory is
136    determined by the MAC Address of the Moblab device.
137
138    @returns gsuri to offload test results to.
139    """
140    if not lsbrelease_utils.is_moblab():
141        return DEFAULT_OFFLOAD_GSURI
142    moblab_id_filepath = '/home/moblab/.moblab_id'
143    if os.path.exists(moblab_id_filepath):
144        with open(moblab_id_filepath, 'r') as moblab_id_file:
145            random_id = moblab_id_file.read()
146    else:
147        random_id = uuid.uuid1()
148        with open(moblab_id_filepath, 'w') as moblab_id_file:
149            moblab_id_file.write('%s' % random_id)
150    return '%sresults/%s/%s/' % (
151            global_config.global_config.get_config_value(
152                    'CROS', 'image_storage_server'),
153            get_interface_mac_address(MOBLAB_ETH), random_id)
154
155
156# TODO(petermayo): crosbug.com/31826 Share this with _GsUpload in
157# //chromite.git/buildbot/prebuilt.py somewhere/somehow
158def gs_upload(local_file, remote_file, acl, result_dir=None,
159              transfer_timeout=300, acl_timeout=300):
160    """Upload to GS bucket.
161
162    @param local_file: Local file to upload
163    @param remote_file: Remote location to upload the local_file to.
164    @param acl: name or file used for controlling access to the uploaded
165                file.
166    @param result_dir: Result directory if you want to add tracing to the
167                       upload.
168    @param transfer_timeout: Timeout for this upload call.
169    @param acl_timeout: Timeout for the acl call needed to confirm that
170                        the uploader has permissions to execute the upload.
171
172    @raise CmdError: the exit code of the gsutil call was not 0.
173
174    @returns True/False - depending on if the upload succeeded or failed.
175    """
176    # https://developers.google.com/storage/docs/accesscontrol#extension
177    CANNED_ACLS = ['project-private', 'private', 'public-read',
178                   'public-read-write', 'authenticated-read',
179                   'bucket-owner-read', 'bucket-owner-full-control']
180    _GSUTIL_BIN = 'gsutil'
181    acl_cmd = None
182    if acl in CANNED_ACLS:
183        cmd = '%s cp -a %s %s %s' % (_GSUTIL_BIN, acl, local_file, remote_file)
184    else:
185        # For private uploads we assume that the overlay board is set up
186        # properly and a googlestore_acl.xml is present, if not this script
187        # errors
188        cmd = '%s cp -a private %s %s' % (_GSUTIL_BIN, local_file, remote_file)
189        if not os.path.exists(acl):
190            logging.error('Unable to find ACL File %s.', acl)
191            return False
192        acl_cmd = '%s setacl %s %s' % (_GSUTIL_BIN, acl, remote_file)
193    if not result_dir:
194        base_utils.run(cmd, timeout=transfer_timeout, verbose=True)
195        if acl_cmd:
196            base_utils.run(acl_cmd, timeout=acl_timeout, verbose=True)
197        return True
198    with open(os.path.join(result_dir, 'tracing'), 'w') as ftrace:
199        ftrace.write('Preamble\n')
200        base_utils.run(cmd, timeout=transfer_timeout, verbose=True,
201                       stdout_tee=ftrace, stderr_tee=ftrace)
202        if acl_cmd:
203            ftrace.write('\nACL setting\n')
204            # Apply the passed in ACL xml file to the uploaded object.
205            base_utils.run(acl_cmd, timeout=acl_timeout, verbose=True,
206                           stdout_tee=ftrace, stderr_tee=ftrace)
207        ftrace.write('Postamble\n')
208        return True
209
210
211def gs_ls(uri_pattern):
212    """Returns a list of URIs that match a given pattern.
213
214    @param uri_pattern: a GS URI pattern, may contain wildcards
215
216    @return A list of URIs matching the given pattern.
217
218    @raise CmdError: the gsutil command failed.
219
220    """
221    gs_cmd = ' '.join(['gsutil', 'ls', uri_pattern])
222    result = base_utils.system_output(gs_cmd).splitlines()
223    return [path.rstrip() for path in result if path]
224
225
226def nuke_pids(pid_list, signal_queue=[signal.SIGTERM, signal.SIGKILL]):
227    """
228    Given a list of pid's, kill them via an esclating series of signals.
229
230    @param pid_list: List of PID's to kill.
231    @param signal_queue: Queue of signals to send the PID's to terminate them.
232
233    @return: A mapping of the signal name to the number of processes it
234        was sent to.
235    """
236    sig_count = {}
237    # Though this is slightly hacky it beats hardcoding names anyday.
238    sig_names = dict((k, v) for v, k in signal.__dict__.iteritems()
239                     if v.startswith('SIG'))
240    for sig in signal_queue:
241        logging.debug('Sending signal %s to the following pids:', sig)
242        sig_count[sig_names.get(sig, 'unknown_signal')] = len(pid_list)
243        for pid in pid_list:
244            logging.debug('Pid %d', pid)
245            try:
246                os.kill(pid, sig)
247            except OSError:
248                # The process may have died from a previous signal before we
249                # could kill it.
250                pass
251        if sig == signal.SIGKILL:
252            return sig_count
253        pid_list = [pid for pid in pid_list if base_utils.pid_is_alive(pid)]
254        if not pid_list:
255            break
256        time.sleep(CHECK_PID_IS_ALIVE_TIMEOUT)
257    failed_list = []
258    for pid in pid_list:
259        if base_utils.pid_is_alive(pid):
260            failed_list.append('Could not kill %d for process name: %s.' % pid,
261                               base_utils.get_process_name(pid))
262    if failed_list:
263        raise error.AutoservRunError('Following errors occured: %s' %
264                                     failed_list, None)
265    return sig_count
266
267
268def externalize_host(host):
269    """Returns an externally accessible host name.
270
271    @param host: a host name or address (string)
272
273    @return An externally visible host name or address
274
275    """
276    return socket.gethostname() if host in _LOCAL_HOST_LIST else host
277
278
279def urlopen_socket_timeout(url, data=None, timeout=5):
280    """
281    Wrapper to urllib2.urlopen with a socket timeout.
282
283    This method will convert all socket timeouts to
284    TimeoutExceptions, so we can use it in conjunction
285    with the rpc retry decorator and continue to handle
286    other URLErrors as we see fit.
287
288    @param url: The url to open.
289    @param data: The data to send to the url (eg: the urlencoded dictionary
290                 used with a POST call).
291    @param timeout: The timeout for this urlopen call.
292
293    @return: The response of the urlopen call.
294
295    @raises: error.TimeoutException when a socket timeout occurs.
296             urllib2.URLError for errors that not caused by timeout.
297             urllib2.HTTPError for errors like 404 url not found.
298    """
299    old_timeout = socket.getdefaulttimeout()
300    socket.setdefaulttimeout(timeout)
301    try:
302        return urllib2.urlopen(url, data=data)
303    except urllib2.URLError as e:
304        if type(e.reason) is socket.timeout:
305            raise error.TimeoutException(str(e))
306        raise
307    finally:
308        socket.setdefaulttimeout(old_timeout)
309
310
311def parse_chrome_version(version_string):
312    """
313    Parse a chrome version string and return version and milestone.
314
315    Given a chrome version of the form "W.X.Y.Z", return "W.X.Y.Z" as
316    the version and "W" as the milestone.
317
318    @param version_string: Chrome version string.
319    @return: a tuple (chrome_version, milestone). If the incoming version
320             string is not of the form "W.X.Y.Z", chrome_version will
321             be set to the incoming "version_string" argument and the
322             milestone will be set to the empty string.
323    """
324    match = re.search('(\d+)\.\d+\.\d+\.\d+', version_string)
325    ver = match.group(0) if match else version_string
326    milestone = match.group(1) if match else ''
327    return ver, milestone
328
329
330def is_localhost(server):
331    """Check if server is equivalent to localhost.
332
333    @param server: Name of the server to check.
334
335    @return: True if given server is equivalent to localhost.
336
337    @raise socket.gaierror: If server name failed to be resolved.
338    """
339    if server in _LOCAL_HOST_LIST:
340        return True
341    try:
342        return (socket.gethostbyname(socket.gethostname()) ==
343                socket.gethostbyname(server))
344    except socket.gaierror:
345        logging.error('Failed to resolve server name %s.', server)
346        return False
347
348
349def get_function_arg_value(func, arg_name, args, kwargs):
350    """Get the value of the given argument for the function.
351
352    @param func: Function being called with given arguments.
353    @param arg_name: Name of the argument to look for value.
354    @param args: arguments for function to be called.
355    @param kwargs: keyword arguments for function to be called.
356
357    @return: The value of the given argument for the function.
358
359    @raise ValueError: If the argument is not listed function arguemnts.
360    @raise KeyError: If no value is found for the given argument.
361    """
362    if arg_name in kwargs:
363        return kwargs[arg_name]
364
365    argspec = inspect.getargspec(func)
366    index = argspec.args.index(arg_name)
367    try:
368        return args[index]
369    except IndexError:
370        try:
371            # The argument can use a default value. Reverse the default value
372            # so argument with default value can be counted from the last to
373            # the first.
374            return argspec.defaults[::-1][len(argspec.args) - index - 1]
375        except IndexError:
376            raise KeyError('Argument %s is not given a value. argspec: %s, '
377                           'args:%s, kwargs:%s' %
378                           (arg_name, argspec, args, kwargs))
379
380
381def version_match(build_version, release_version, update_url=''):
382    """Compare release versino from lsb-release with cros-version label.
383
384    build_version is a string based on build name. It is prefixed with builder
385    info and branch ID, e.g., lumpy-release/R43-6809.0.0. It may not include
386    builder info, e.g., lumpy-release, in which case, update_url shall be passed
387    in to determine if the build is a trybot or pgo-generate build.
388    release_version is retrieved from lsb-release.
389    These two values might not match exactly.
390
391    The method is designed to compare version for following 6 scenarios with
392    samples of build version and expected release version:
393    1. trybot paladin build.
394    build version:   trybot-lumpy-paladin/R27-3837.0.0-b123
395    release version: 3837.0.2013_03_21_1340
396
397    2. trybot release build.
398    build version:   trybot-lumpy-release/R27-3837.0.0-b456
399    release version: 3837.0.0
400
401    3. buildbot official release build.
402    build version:   lumpy-release/R27-3837.0.0
403    release version: 3837.0.0
404
405    4. non-official paladin rc build.
406    build version:   lumpy-paladin/R27-3878.0.0-rc7
407    release version: 3837.0.0-rc7
408
409    5. chrome-perf build.
410    build version:   lumpy-chrome-perf/R28-3837.0.0-b2996
411    release version: 3837.0.0
412
413    6. pgo-generate build.
414    build version:   lumpy-release-pgo-generate/R28-3837.0.0-b2996
415    release version: 3837.0.0-pgo-generate
416
417    TODO: This logic has a bug if a trybot paladin build failed to be
418    installed in a DUT running an older trybot paladin build with same
419    platform number, but different build number (-b###). So to conclusively
420    determine if a tryjob paladin build is imaged successfully, we may need
421    to find out the date string from update url.
422
423    @param build_version: Build name for cros version, e.g.
424                          peppy-release/R43-6809.0.0 or R43-6809.0.0
425    @param release_version: Release version retrieved from lsb-release,
426                            e.g., 6809.0.0
427    @param update_url: Update url which include the full builder information.
428                       Default is set to empty string.
429
430    @return: True if the values match, otherwise returns False.
431    """
432    # If the build is from release, CQ or PFQ builder, cros-version label must
433    # be ended with release version in lsb-release.
434    if build_version.endswith(release_version):
435        return True
436
437    # Remove R#- and -b# at the end of build version
438    stripped_version = re.sub(r'(R\d+-|-b\d+)', '', build_version)
439    # Trim the builder info, e.g., trybot-lumpy-paladin/
440    stripped_version = stripped_version.split('/')[-1]
441
442    is_trybot_paladin_build = (
443            re.match(r'.*trybot-.+-paladin', build_version) or
444            re.match(r'.*trybot-.+-paladin', update_url))
445
446    # Replace date string with 0 in release_version
447    release_version_no_date = re.sub(r'\d{4}_\d{2}_\d{2}_\d+', '0',
448                                    release_version)
449    has_date_string = release_version != release_version_no_date
450
451    is_pgo_generate_build = (
452            re.match(r'.+-pgo-generate', build_version) or
453            re.match(r'.+-pgo-generate', update_url))
454
455    # Remove |-pgo-generate| in release_version
456    release_version_no_pgo = release_version.replace('-pgo-generate', '')
457    has_pgo_generate = release_version != release_version_no_pgo
458
459    if is_trybot_paladin_build:
460        if not has_date_string:
461            logging.error('A trybot paladin build is expected. Version '
462                          '"%s" is not a paladin build.', release_version)
463            return False
464        return stripped_version == release_version_no_date
465    elif is_pgo_generate_build:
466        if not has_pgo_generate:
467            logging.error('A pgo-generate build is expected. Version '
468                          '"%s" is not a pgo-generate build.',
469                          release_version)
470            return False
471        return stripped_version == release_version_no_pgo
472    else:
473        if has_date_string:
474            logging.error('Unexpected date found in a non trybot paladin '
475                          'build.')
476            return False
477        # Versioned build, i.e., rc or release build.
478        return stripped_version == release_version
479
480
481def get_real_user():
482    """Get the real user that runs the script.
483
484    The function check environment variable SUDO_USER for the user if the
485    script is run with sudo. Otherwise, it returns the value of environment
486    variable USER.
487
488    @return: The user name that runs the script.
489
490    """
491    user = os.environ.get('SUDO_USER')
492    if not user:
493        user = os.environ.get('USER')
494    return user
495