blob: 4e21cb2f271efe6d3f8f679903575997c2d4d74e [file] [log] [blame]
Chris Masone6a0680f2012-03-02 08:40:00 -08001# 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.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -08004
Dan Shi767dced2015-02-01 00:21:07 -08005import inspect
Simran Basi87d7a212012-09-27 10:41:05 -07006import logging
Simran Basiaf9b8e72012-10-12 15:02:36 -07007import os
Fang Deng7c2be102012-08-27 16:20:25 -07008import re
Simran Basiaf9b8e72012-10-12 15:02:36 -07009import signal
Scott Zawalski347a0b82012-03-30 16:39:21 -040010import socket
Simran Basiaf9b8e72012-10-12 15:02:36 -070011import time
beeps60aec242013-06-26 14:47:48 -070012import urllib2
Simran Basidd129972014-09-11 14:34:49 -070013import uuid
Chris Masone6a0680f2012-03-02 08:40:00 -080014
Dan Shi60cf6a92015-01-29 17:22:49 -080015from 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
beepsc4fb1472013-05-08 21:49:48 -070019from autotest_lib.client.cros import constants
Simran Basiaf9b8e72012-10-12 15:02:36 -070020
21
22# Keep checking if the pid is alive every second until the timeout (in seconds)
23CHECK_PID_IS_ALIVE_TIMEOUT = 6
24
Simran Basi22aa9fe2012-12-07 16:37:09 -080025_LOCAL_HOST_LIST = ('localhost', '127.0.0.1')
26
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -080027# The default address of a vm gateway.
28DEFAULT_VM_GATEWAY = '10.0.2.2'
29
Simran Basidd129972014-09-11 14:34:49 -070030# 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'
Fang Deng3197b392013-06-26 11:42:02 -070036
Chris Masone6a0680f2012-03-02 08:40:00 -080037def 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
beepsfda8f412013-05-02 19:08:20 -070049 @param host: the host to ping.
Chris Masone6a0680f2012-03-02 08:40:00 -080050 @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,
Scott Zawalskiae843542012-03-20 09:51:29 -040062 stdout_tee=base_utils.TEE_TO_LOGS,
63 stderr_tee=base_utils.TEE_TO_LOGS).exit_status
Scott Zawalski347a0b82012-03-30 16:39:21 -040064
65
66def host_is_in_lab_zone(hostname):
Hung-ying Tyancbdd1982014-09-03 16:54:08 +080067 """Check if the host is in the CLIENT.dns_zone.
Scott Zawalski347a0b82012-03-30 16:39:21 -040068
69 @param hostname: The hostname to check.
70 @returns True if hostname.dns_zone resolves, otherwise False.
71 """
72 host_parts = hostname.split('.')
Hung-ying Tyancbdd1982014-09-03 16:54:08 +080073 dns_zone = global_config.global_config.get_config_value('CLIENT', 'dns_zone',
Scott Zawalski347a0b82012-03-30 16:39:21 -040074 default=None)
75 fqdn = '%s.%s' % (host_parts[0], dns_zone)
76 try:
77 socket.gethostbyname(fqdn)
78 return True
79 except socket.gaierror:
Ilja H. Friedel8753d832014-10-07 22:30:06 -070080 return False
Fang Deng7c2be102012-08-27 16:20:25 -070081
82
beepsc4fb1472013-05-08 21:49:48 -070083def 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
Simran Basidd129972014-09-11 14:34:49 -0700116def 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 """
Dan Shi60cf6a92015-01-29 17:22:49 -0800140 if not lsbrelease_utils.is_moblab():
Simran Basidd129972014-09-11 14:34:49 -0700141 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
Simran Basi87d7a212012-09-27 10:41:05 -0700156# 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.
beepsfda8f412013-05-02 19:08:20 -0700168 @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.
Simran Basi87d7a212012-09-27 10:41:05 -0700171
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
Simran Basiaf9b8e72012-10-12 15:02:36 -0700209
210
Gilad Arnold0ed760c2012-11-05 23:42:53 -0800211def 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
Simran Basiaf9b8e72012-10-12 15:02:36 -0700226def 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.
Prashanth Bcf731e32014-08-10 18:03:57 -0700232
233 @return: A mapping of the signal name to the number of processes it
234 was sent to.
Simran Basiaf9b8e72012-10-12 15:02:36 -0700235 """
Prashanth Bcf731e32014-08-10 18:03:57 -0700236 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'))
Simran Basiaf9b8e72012-10-12 15:02:36 -0700240 for sig in signal_queue:
241 logging.debug('Sending signal %s to the following pids:', sig)
Prashanth Bcf731e32014-08-10 18:03:57 -0700242 sig_count[sig_names.get(sig, 'unknown_signal')] = len(pid_list)
Simran Basiaf9b8e72012-10-12 15:02:36 -0700243 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
David Jamesf77198b2014-11-17 17:23:12 -0800251 if sig == signal.SIGKILL:
252 return sig_count
Prashanth Bcf731e32014-08-10 18:03:57 -0700253 pid_list = [pid for pid in pid_list if base_utils.pid_is_alive(pid)]
254 if not pid_list:
255 break
Simran Basiaf9b8e72012-10-12 15:02:36 -0700256 time.sleep(CHECK_PID_IS_ALIVE_TIMEOUT)
257 failed_list = []
Simran Basiaf9b8e72012-10-12 15:02:36 -0700258 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,
Simran Basi62723202013-01-22 15:24:49 -0800261 base_utils.get_process_name(pid))
Simran Basiaf9b8e72012-10-12 15:02:36 -0700262 if failed_list:
263 raise error.AutoservRunError('Following errors occured: %s' %
264 failed_list, None)
Prashanth Bcf731e32014-08-10 18:03:57 -0700265 return sig_count
Gilad Arnold0ed760c2012-11-05 23:42:53 -0800266
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
Simran Basi22aa9fe2012-12-07 16:37:09 -0800277
278
beeps60aec242013-06-26 14:47:48 -0700279def 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.
Dan Shi6c00dde2013-07-29 17:47:29 -0700296 urllib2.URLError for errors that not caused by timeout.
297 urllib2.HTTPError for errors like 404 url not found.
beeps60aec242013-06-26 14:47:48 -0700298 """
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))
Dan Shi6c00dde2013-07-29 17:47:29 -0700306 raise
beeps60aec242013-06-26 14:47:48 -0700307 finally:
308 socket.setdefaulttimeout(old_timeout)
Luis Lozano40b7d0d2014-01-17 15:12:06 -0800309
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
beepsbff9f9d2013-12-06 11:14:08 -0800328
329
Dan Shif6c65bd2014-08-29 16:15:07 -0700330def 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.
Dan Shi767dced2015-02-01 00:21:07 -0800336
Dan Shif6c65bd2014-08-29 16:15:07 -0700337 @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
Dan Shi767dced2015-02-01 00:21:07 -0800347
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))
Dan Shi549fb822015-03-24 18:01:11 -0700379
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