blob: a853032525a48d5c9a53ae66e18c58e0023ae2c4 [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
beepsbff9f9d2013-12-06 11:14:08 -08005import glob
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
beepsbff9f9d2013-12-06 11:14:08 -080011import sys
Simran Basiaf9b8e72012-10-12 15:02:36 -070012import time
beeps60aec242013-06-26 14:47:48 -070013import urllib2
Simran Basidd129972014-09-11 14:34:49 -070014import uuid
Chris Masone6a0680f2012-03-02 08:40:00 -080015
Simran Basiaf9b8e72012-10-12 15:02:36 -070016from autotest_lib.client.common_lib import base_utils, error, global_config
beepsc4fb1472013-05-08 21:49:48 -070017from autotest_lib.client.cros import constants
Simran Basiaf9b8e72012-10-12 15:02:36 -070018
19
20# Keep checking if the pid is alive every second until the timeout (in seconds)
21CHECK_PID_IS_ALIVE_TIMEOUT = 6
22
Simran Basi22aa9fe2012-12-07 16:37:09 -080023_LOCAL_HOST_LIST = ('localhost', '127.0.0.1')
24
Simran Basidd129972014-09-11 14:34:49 -070025# Google Storage bucket URI to store results in.
26DEFAULT_OFFLOAD_GSURI = global_config.global_config.get_config_value(
27 'CROS', 'results_storage_server', default=None)
28
29# Default Moblab Ethernet Interface.
30MOBLAB_ETH = 'eth0'
Fang Deng3197b392013-06-26 11:42:02 -070031
Chris Masone6a0680f2012-03-02 08:40:00 -080032def ping(host, deadline=None, tries=None, timeout=60):
33 """Attempt to ping |host|.
34
35 Shell out to 'ping' to try to reach |host| for |timeout| seconds.
36 Returns exit code of ping.
37
38 Per 'man ping', if you specify BOTH |deadline| and |tries|, ping only
39 returns 0 if we get responses to |tries| pings within |deadline| seconds.
40
41 Specifying |deadline| or |count| alone should return 0 as long as
42 some packets receive responses.
43
beepsfda8f412013-05-02 19:08:20 -070044 @param host: the host to ping.
Chris Masone6a0680f2012-03-02 08:40:00 -080045 @param deadline: seconds within which |tries| pings must succeed.
46 @param tries: number of pings to send.
47 @param timeout: number of seconds after which to kill 'ping' command.
48 @return exit code of ping command.
49 """
50 args = [host]
51 if deadline:
52 args.append('-w%d' % deadline)
53 if tries:
54 args.append('-c%d' % tries)
55 return base_utils.run('ping', args=args,
56 ignore_status=True, timeout=timeout,
Scott Zawalskiae843542012-03-20 09:51:29 -040057 stdout_tee=base_utils.TEE_TO_LOGS,
58 stderr_tee=base_utils.TEE_TO_LOGS).exit_status
Scott Zawalski347a0b82012-03-30 16:39:21 -040059
60
61def host_is_in_lab_zone(hostname):
Hung-ying Tyancbdd1982014-09-03 16:54:08 +080062 """Check if the host is in the CLIENT.dns_zone.
Scott Zawalski347a0b82012-03-30 16:39:21 -040063
64 @param hostname: The hostname to check.
65 @returns True if hostname.dns_zone resolves, otherwise False.
66 """
67 host_parts = hostname.split('.')
Hung-ying Tyancbdd1982014-09-03 16:54:08 +080068 dns_zone = global_config.global_config.get_config_value('CLIENT', 'dns_zone',
Scott Zawalski347a0b82012-03-30 16:39:21 -040069 default=None)
70 fqdn = '%s.%s' % (host_parts[0], dns_zone)
71 try:
72 socket.gethostbyname(fqdn)
73 return True
74 except socket.gaierror:
75 return False
Fang Deng7c2be102012-08-27 16:20:25 -070076
77
beepsc4fb1472013-05-08 21:49:48 -070078def get_chrome_version(job_views):
79 """
80 Retrieves the version of the chrome binary associated with a job.
81
82 When a test runs we query the chrome binary for it's version and drop
83 that value into a client keyval. To retrieve the chrome version we get all
84 the views associated with a test from the db, including those of the
85 server and client jobs, and parse the version out of the first test view
86 that has it. If we never ran a single test in the suite the job_views
87 dictionary will not contain a chrome version.
88
89 This method cannot retrieve the chrome version from a dictionary that
90 does not conform to the structure of an autotest tko view.
91
92 @param job_views: a list of a job's result views, as returned by
93 the get_detailed_test_views method in rpc_interface.
94 @return: The chrome version string, or None if one can't be found.
95 """
96
97 # Aborted jobs have no views.
98 if not job_views:
99 return None
100
101 for view in job_views:
102 if (view.get('attributes')
103 and constants.CHROME_VERSION in view['attributes'].keys()):
104
105 return view['attributes'].get(constants.CHROME_VERSION)
106
107 logging.warning('Could not find chrome version for failure.')
108 return None
109
110
Simran Basi85f4c362014-04-08 13:40:57 -0700111def _lsbrelease_search(regex, group_id=0):
112 """Searches /etc/lsb-release for a regex match.
113
114 @param regex: Regex to match.
115 @param group_id: The group in the regex we are searching for.
116 Default is group 0.
117
118 @returns the string in the specified group if there is a match or None if
119 not found.
120
121 @raises IOError if /etc/lsb-release can not be accessed.
122 """
123 with open(constants.LSB_RELEASE) as lsb_release_file:
124 for line in lsb_release_file:
125 m = re.match(regex, line)
126 if m:
127 return m.group(group_id)
128 return None
129
130
Fang Deng7c2be102012-08-27 16:20:25 -0700131def get_current_board():
132 """Return the current board name.
133
134 @return current board name, e.g "lumpy", None on fail.
135 """
Simran Basi85f4c362014-04-08 13:40:57 -0700136 return _lsbrelease_search(r'^CHROMEOS_RELEASE_BOARD=(.+)$', group_id=1)
Simran Basi87d7a212012-09-27 10:41:05 -0700137
138
mussa9f6a0ae2014-06-05 14:54:05 -0700139def get_chromeos_release_version():
140 """
141 @return chromeos version in device under test as string. None on fail.
142 """
143 return _lsbrelease_search(r'^CHROMEOS_RELEASE_VERSION=(.+)$', group_id=1)
144
145
Simran Basi85f4c362014-04-08 13:40:57 -0700146def is_moblab():
147 """Return if we are running on a Moblab system or not.
148
149 @return the board string if this is a Moblab device or None if it is not.
150 """
151 try:
152 return _lsbrelease_search(r'.*moblab')
153 except IOError as e:
154 logging.error('Unable to determine if this is a moblab system: %s', e)
155
Simran Basidd129972014-09-11 14:34:49 -0700156
157def get_interface_mac_address(interface):
158 """Return the MAC address of a given interface.
159
160 @param interface: Interface to look up the MAC address of.
161 """
162 interface_link = base_utils.run(
163 'ip addr show %s | grep link/ether' % interface).stdout
164 # The output will be in the format of:
165 # 'link/ether <mac> brd ff:ff:ff:ff:ff:ff'
166 return interface_link.split()[1]
167
168
169def get_offload_gsuri():
170 """Return the GSURI to offload test results to.
171
172 For the normal use case this is the results_storage_server in the
173 global_config.
174
175 However partners using Moblab will be offloading their results to a
176 subdirectory of their image storage buckets. The subdirectory is
177 determined by the MAC Address of the Moblab device.
178
179 @returns gsuri to offload test results to.
180 """
181 if not is_moblab():
182 return DEFAULT_OFFLOAD_GSURI
183 moblab_id_filepath = '/home/moblab/.moblab_id'
184 if os.path.exists(moblab_id_filepath):
185 with open(moblab_id_filepath, 'r') as moblab_id_file:
186 random_id = moblab_id_file.read()
187 else:
188 random_id = uuid.uuid1()
189 with open(moblab_id_filepath, 'w') as moblab_id_file:
190 moblab_id_file.write('%s' % random_id)
191 return '%sresults/%s/%s/' % (
192 global_config.global_config.get_config_value(
193 'CROS', 'image_storage_server'),
194 get_interface_mac_address(MOBLAB_ETH), random_id)
195
196
Simran Basi87d7a212012-09-27 10:41:05 -0700197# TODO(petermayo): crosbug.com/31826 Share this with _GsUpload in
198# //chromite.git/buildbot/prebuilt.py somewhere/somehow
199def gs_upload(local_file, remote_file, acl, result_dir=None,
200 transfer_timeout=300, acl_timeout=300):
201 """Upload to GS bucket.
202
203 @param local_file: Local file to upload
204 @param remote_file: Remote location to upload the local_file to.
205 @param acl: name or file used for controlling access to the uploaded
206 file.
207 @param result_dir: Result directory if you want to add tracing to the
208 upload.
beepsfda8f412013-05-02 19:08:20 -0700209 @param transfer_timeout: Timeout for this upload call.
210 @param acl_timeout: Timeout for the acl call needed to confirm that
211 the uploader has permissions to execute the upload.
Simran Basi87d7a212012-09-27 10:41:05 -0700212
213 @raise CmdError: the exit code of the gsutil call was not 0.
214
215 @returns True/False - depending on if the upload succeeded or failed.
216 """
217 # https://developers.google.com/storage/docs/accesscontrol#extension
218 CANNED_ACLS = ['project-private', 'private', 'public-read',
219 'public-read-write', 'authenticated-read',
220 'bucket-owner-read', 'bucket-owner-full-control']
221 _GSUTIL_BIN = 'gsutil'
222 acl_cmd = None
223 if acl in CANNED_ACLS:
224 cmd = '%s cp -a %s %s %s' % (_GSUTIL_BIN, acl, local_file, remote_file)
225 else:
226 # For private uploads we assume that the overlay board is set up
227 # properly and a googlestore_acl.xml is present, if not this script
228 # errors
229 cmd = '%s cp -a private %s %s' % (_GSUTIL_BIN, local_file, remote_file)
230 if not os.path.exists(acl):
231 logging.error('Unable to find ACL File %s.', acl)
232 return False
233 acl_cmd = '%s setacl %s %s' % (_GSUTIL_BIN, acl, remote_file)
234 if not result_dir:
235 base_utils.run(cmd, timeout=transfer_timeout, verbose=True)
236 if acl_cmd:
237 base_utils.run(acl_cmd, timeout=acl_timeout, verbose=True)
238 return True
239 with open(os.path.join(result_dir, 'tracing'), 'w') as ftrace:
240 ftrace.write('Preamble\n')
241 base_utils.run(cmd, timeout=transfer_timeout, verbose=True,
242 stdout_tee=ftrace, stderr_tee=ftrace)
243 if acl_cmd:
244 ftrace.write('\nACL setting\n')
245 # Apply the passed in ACL xml file to the uploaded object.
246 base_utils.run(acl_cmd, timeout=acl_timeout, verbose=True,
247 stdout_tee=ftrace, stderr_tee=ftrace)
248 ftrace.write('Postamble\n')
249 return True
Simran Basiaf9b8e72012-10-12 15:02:36 -0700250
251
Gilad Arnold0ed760c2012-11-05 23:42:53 -0800252def gs_ls(uri_pattern):
253 """Returns a list of URIs that match a given pattern.
254
255 @param uri_pattern: a GS URI pattern, may contain wildcards
256
257 @return A list of URIs matching the given pattern.
258
259 @raise CmdError: the gsutil command failed.
260
261 """
262 gs_cmd = ' '.join(['gsutil', 'ls', uri_pattern])
263 result = base_utils.system_output(gs_cmd).splitlines()
264 return [path.rstrip() for path in result if path]
265
266
Simran Basiaf9b8e72012-10-12 15:02:36 -0700267def nuke_pids(pid_list, signal_queue=[signal.SIGTERM, signal.SIGKILL]):
268 """
269 Given a list of pid's, kill them via an esclating series of signals.
270
271 @param pid_list: List of PID's to kill.
272 @param signal_queue: Queue of signals to send the PID's to terminate them.
Prashanth Bcf731e32014-08-10 18:03:57 -0700273
274 @return: A mapping of the signal name to the number of processes it
275 was sent to.
Simran Basiaf9b8e72012-10-12 15:02:36 -0700276 """
Prashanth Bcf731e32014-08-10 18:03:57 -0700277 sig_count = {}
278 # Though this is slightly hacky it beats hardcoding names anyday.
279 sig_names = dict((k, v) for v, k in signal.__dict__.iteritems()
280 if v.startswith('SIG'))
Simran Basiaf9b8e72012-10-12 15:02:36 -0700281 for sig in signal_queue:
282 logging.debug('Sending signal %s to the following pids:', sig)
Prashanth Bcf731e32014-08-10 18:03:57 -0700283 sig_count[sig_names.get(sig, 'unknown_signal')] = len(pid_list)
Simran Basiaf9b8e72012-10-12 15:02:36 -0700284 for pid in pid_list:
285 logging.debug('Pid %d', pid)
286 try:
287 os.kill(pid, sig)
288 except OSError:
289 # The process may have died from a previous signal before we
290 # could kill it.
291 pass
Prashanth Bcf731e32014-08-10 18:03:57 -0700292 pid_list = [pid for pid in pid_list if base_utils.pid_is_alive(pid)]
293 if not pid_list:
294 break
Simran Basiaf9b8e72012-10-12 15:02:36 -0700295 time.sleep(CHECK_PID_IS_ALIVE_TIMEOUT)
296 failed_list = []
297 if signal.SIGKILL in signal_queue:
Prashanth Bcf731e32014-08-10 18:03:57 -0700298 return sig_count
Simran Basiaf9b8e72012-10-12 15:02:36 -0700299 for pid in pid_list:
300 if base_utils.pid_is_alive(pid):
301 failed_list.append('Could not kill %d for process name: %s.' % pid,
Simran Basi62723202013-01-22 15:24:49 -0800302 base_utils.get_process_name(pid))
Simran Basiaf9b8e72012-10-12 15:02:36 -0700303 if failed_list:
304 raise error.AutoservRunError('Following errors occured: %s' %
305 failed_list, None)
Prashanth Bcf731e32014-08-10 18:03:57 -0700306 return sig_count
Gilad Arnold0ed760c2012-11-05 23:42:53 -0800307
308
309def externalize_host(host):
310 """Returns an externally accessible host name.
311
312 @param host: a host name or address (string)
313
314 @return An externally visible host name or address
315
316 """
317 return socket.gethostname() if host in _LOCAL_HOST_LIST else host
Simran Basi22aa9fe2012-12-07 16:37:09 -0800318
319
beeps60aec242013-06-26 14:47:48 -0700320def urlopen_socket_timeout(url, data=None, timeout=5):
321 """
322 Wrapper to urllib2.urlopen with a socket timeout.
323
324 This method will convert all socket timeouts to
325 TimeoutExceptions, so we can use it in conjunction
326 with the rpc retry decorator and continue to handle
327 other URLErrors as we see fit.
328
329 @param url: The url to open.
330 @param data: The data to send to the url (eg: the urlencoded dictionary
331 used with a POST call).
332 @param timeout: The timeout for this urlopen call.
333
334 @return: The response of the urlopen call.
335
336 @raises: error.TimeoutException when a socket timeout occurs.
Dan Shi6c00dde2013-07-29 17:47:29 -0700337 urllib2.URLError for errors that not caused by timeout.
338 urllib2.HTTPError for errors like 404 url not found.
beeps60aec242013-06-26 14:47:48 -0700339 """
340 old_timeout = socket.getdefaulttimeout()
341 socket.setdefaulttimeout(timeout)
342 try:
343 return urllib2.urlopen(url, data=data)
344 except urllib2.URLError as e:
345 if type(e.reason) is socket.timeout:
346 raise error.TimeoutException(str(e))
Dan Shi6c00dde2013-07-29 17:47:29 -0700347 raise
beeps60aec242013-06-26 14:47:48 -0700348 finally:
349 socket.setdefaulttimeout(old_timeout)
Luis Lozano40b7d0d2014-01-17 15:12:06 -0800350
351
352def parse_chrome_version(version_string):
353 """
354 Parse a chrome version string and return version and milestone.
355
356 Given a chrome version of the form "W.X.Y.Z", return "W.X.Y.Z" as
357 the version and "W" as the milestone.
358
359 @param version_string: Chrome version string.
360 @return: a tuple (chrome_version, milestone). If the incoming version
361 string is not of the form "W.X.Y.Z", chrome_version will
362 be set to the incoming "version_string" argument and the
363 milestone will be set to the empty string.
364 """
365 match = re.search('(\d+)\.\d+\.\d+\.\d+', version_string)
366 ver = match.group(0) if match else version_string
367 milestone = match.group(1) if match else ''
368 return ver, milestone
beepsbff9f9d2013-12-06 11:14:08 -0800369
370
371def take_screenshot(dest_dir, fname_prefix, format='png'):
mussafd5b8052014-05-06 10:04:54 -0700372 """
373 Take screenshot and save to a new file in the dest_dir.
beepsbff9f9d2013-12-06 11:14:08 -0800374
mussafd5b8052014-05-06 10:04:54 -0700375 @param dest_dir: path, destination directory to save the screenshot.
376 @param fname_prefix: string, prefix for output filename.
377 @param format: string, file format ('png', 'jpg', etc) to use.
beepsbff9f9d2013-12-06 11:14:08 -0800378
mussafd5b8052014-05-06 10:04:54 -0700379 @returns complete path to saved screenshot file.
380
beepsbff9f9d2013-12-06 11:14:08 -0800381 """
Dan Shi1d8803b2014-06-19 14:32:00 -0700382 if not _is_x_running():
383 return
384
beepsbff9f9d2013-12-06 11:14:08 -0800385 next_index = len(glob.glob(
386 os.path.join(dest_dir, '%s-*.%s' % (fname_prefix, format))))
387 screenshot_file = os.path.join(
388 dest_dir, '%s-%d.%s' % (fname_prefix, next_index, format))
389 logging.info('Saving screenshot to %s.', screenshot_file)
390
mussafd5b8052014-05-06 10:04:54 -0700391 import_cmd = ('/usr/local/bin/import -window root -depth 8 %s' %
392 screenshot_file)
393
394 _execute_screenshot_capture_command(import_cmd)
395
396 return screenshot_file
397
398
399def take_screen_shot_crop_by_height(fullpath, final_height, x_offset_pixels,
400 y_offset_pixels):
401 """
402 Take a screenshot, crop to final height starting at given (x, y) coordinate.
403
404 Image width will be adjusted to maintain original aspect ratio).
405
406 @param fullpath: path, fullpath of the file that will become the image file.
407 @param final_height: integer, height in pixels of resulting image.
408 @param x_offset_pixels: integer, number of pixels from left margin
409 to begin cropping.
410 @param y_offset_pixels: integer, number of pixels from top margin
411 to begin cropping.
412
413 """
414
415 params = {'height': final_height, 'x_offset': x_offset_pixels,
416 'y_offset': y_offset_pixels, 'path': fullpath}
417
Mussa56b94df2014-08-13 16:50:17 -0700418 import_cmd = ('/usr/local/bin/import -window root -depth 8 -crop '
mussafd5b8052014-05-06 10:04:54 -0700419 'x%(height)d+%(x_offset)d+%(y_offset)d %(path)s' % params)
420
421 _execute_screenshot_capture_command(import_cmd)
422
423 return fullpath
424
425
Mussa45745d82014-08-26 16:06:29 -0700426def take_screenshot_crop(fullpath, box=None):
427 """
428 Take a screenshot using import tool, crop according to dim given by the box.
429
430 @param fullpath: path, full path to save the image to.
431 @param box: 4-tuple giving the upper left and lower right pixel coordinates.
432
433 """
434
435 if box:
436 upperx, uppery, lowerx, lowery = box
437
438 img_w = lowerx - upperx
439 img_h = lowery - uppery
440
441 import_cmd = ('/usr/local/bin/import -window root -depth 8 -crop '
442 '%dx%d+%d+%d' % (img_w, img_h, upperx, uppery))
443 else:
444 import_cmd = ('/usr/local/bin/import -window root -depth 8')
445
446 _execute_screenshot_capture_command('%s %s' % (import_cmd, fullpath))
447
448
Mussa3eeab762014-07-23 17:25:27 -0700449def get_dut_display_resolution():
450 """
451 Parses output of xrandr to determine the display resolution of the dut.
452
453 @return: tuple, (w,h) resolution of device under test.
454 """
455
456 env_vars = 'DISPLAY=:0.0 XAUTHORITY=/home/chronos/.Xauthority'
457 cmd = '%s xrandr | egrep -o "current [0-9]* x [0-9]*"' % env_vars
458 output = base_utils.system_output(cmd)
459
460 m = re.search('(\d+) x (\d+)', output)
461
462 if len(m.groups()) == 2:
463 return int(m.group(1)), int(m.group(2))
464 else:
465 return None
466
467
mussafd5b8052014-05-06 10:04:54 -0700468def _execute_screenshot_capture_command(import_cmd_string):
469 """
470 Executes command to capture a screenshot.
471
472 Provides safe execution of command to capture screenshot by wrapping
473 the command around a try-catch construct.
474
475 @param import_cmd_string: string, screenshot capture command.
476
477 """
478
beepsbff9f9d2013-12-06 11:14:08 -0800479 old_exc_type = sys.exc_info()[0]
mussafd5b8052014-05-06 10:04:54 -0700480 full_cmd = ('DISPLAY=:0.0 XAUTHORITY=/home/chronos/.Xauthority %s' %
481 import_cmd_string)
beepsbff9f9d2013-12-06 11:14:08 -0800482 try:
mussafd5b8052014-05-06 10:04:54 -0700483 base_utils.system(full_cmd)
beepsbff9f9d2013-12-06 11:14:08 -0800484 except Exception as err:
485 # Do not raise an exception if the screenshot fails while processing
486 # another exception.
487 if old_exc_type is None:
488 raise
Dan Shi1d8803b2014-06-19 14:32:00 -0700489 logging.error(err)
490
491
492def _is_x_running():
493 try:
494 return int(base_utils.system_output('pgrep -o ^X$')) > 0
495 except Exception:
Prashanth Bcf731e32014-08-10 18:03:57 -0700496 return False
Dan Shif6c65bd2014-08-29 16:15:07 -0700497
498
499def is_localhost(server):
500 """Check if server is equivalent to localhost.
501
502 @param server: Name of the server to check.
503
504 @return: True if given server is equivalent to localhost.
505 @raise socket.gaierror: If server name failed to be resolved.
506 """
507 if server in _LOCAL_HOST_LIST:
508 return True
509 try:
510 return (socket.gethostbyname(socket.gethostname()) ==
511 socket.gethostbyname(server))
512 except socket.gaierror:
513 logging.error('Failed to resolve server name %s.', server)
514 return False