blob: 0ce9334530f2ef963fd6a94412303277432e16ba [file] [log] [blame]
Allen Li5ed7e632017-02-03 16:31:33 -08001# Copyright (c) 2017 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
Eric Li861b2d52011-02-04 14:50:35 -08005"""
6Convenience functions for use by tests or whomever.
mbligh63073c92008-03-31 16:49:32 +00007
Eric Li861b2d52011-02-04 14:50:35 -08008There's no really good way to do this, as this isn't a class we can do
9inheritance with, just a collection of static methods.
10"""
Allen Li5ed7e632017-02-03 16:31:33 -080011
12# pylint: disable=missing-docstring
13
14import StringIO
15import errno
16import inspect
17import itertools
18import logging
19import os
20import pickle
21import random
22import re
23import resource
24import select
25import shutil
26import signal
27import socket
28import string
29import struct
30import subprocess
31import textwrap
32import time
33import urllib2
34import urlparse
35import uuid
36import warnings
37
38try:
39 import hashlib
40except ImportError:
41 import md5
42 import sha
43
Shuqian Zhaoae2d0782016-11-15 16:58:47 -080044import common
Allen Li5ed7e632017-02-03 16:31:33 -080045
Aviv Keshet12aa4a22017-05-18 16:48:05 -070046from autotest_lib.client.common_lib import env
Allen Li5ed7e632017-02-03 16:31:33 -080047from autotest_lib.client.common_lib import error
48from autotest_lib.client.common_lib import global_config
49from autotest_lib.client.common_lib import logging_manager
50from autotest_lib.client.common_lib.cros.graphite import stats_es_mock
51from autotest_lib.client.cros import constants
52
Dan Shi60cf6a92015-01-29 17:22:49 -080053from autotest_lib.client.common_lib.lsbrelease_utils import *
Allen Li5ed7e632017-02-03 16:31:33 -080054
55
56def deprecated(func):
57 """This is a decorator which can be used to mark functions as deprecated.
58 It will result in a warning being emmitted when the function is used."""
59 def new_func(*args, **dargs):
60 warnings.warn("Call to deprecated function %s." % func.__name__,
61 category=DeprecationWarning)
62 return func(*args, **dargs)
63 new_func.__name__ = func.__name__
64 new_func.__doc__ = func.__doc__
65 new_func.__dict__.update(func.__dict__)
66 return new_func
67
68
69class _NullStream(object):
70 def write(self, data):
71 pass
72
73
74 def flush(self):
75 pass
76
77
78TEE_TO_LOGS = object()
79_the_null_stream = _NullStream()
80
Hidehiko Abed939d662017-06-13 20:25:03 +090081DEVNULL = object()
82
Allen Li5ed7e632017-02-03 16:31:33 -080083DEFAULT_STDOUT_LEVEL = logging.DEBUG
84DEFAULT_STDERR_LEVEL = logging.ERROR
85
86# prefixes for logging stdout/stderr of commands
87STDOUT_PREFIX = '[stdout] '
88STDERR_PREFIX = '[stderr] '
89
90# safe characters for the shell (do not need quoting)
91SHELL_QUOTING_WHITELIST = frozenset(string.ascii_letters +
92 string.digits +
93 '_-+=')
94
95def custom_warning_handler(message, category, filename, lineno, file=None,
96 line=None):
97 """Custom handler to log at the WARNING error level. Ignores |file|."""
98 logging.warning(warnings.formatwarning(message, category, filename, lineno,
99 line))
100
101warnings.showwarning = custom_warning_handler
102
103def get_stream_tee_file(stream, level, prefix=''):
104 if stream is None:
105 return _the_null_stream
Hidehiko Abed939d662017-06-13 20:25:03 +0900106 if stream is DEVNULL:
107 return None
Allen Li5ed7e632017-02-03 16:31:33 -0800108 if stream is TEE_TO_LOGS:
109 return logging_manager.LoggingFile(level=level, prefix=prefix)
110 return stream
111
112
113def _join_with_nickname(base_string, nickname):
114 if nickname:
115 return '%s BgJob "%s" ' % (base_string, nickname)
116 return base_string
117
118
Hidehiko Abed939d662017-06-13 20:25:03 +0900119# TODO: Cleanup and possibly eliminate |unjoinable|, which is only used in our
120# master-ssh connection process, while fixing underlying
Allen Li5ed7e632017-02-03 16:31:33 -0800121# semantics problem in BgJob. See crbug.com/279312
122class BgJob(object):
123 def __init__(self, command, stdout_tee=None, stderr_tee=None, verbose=True,
124 stdin=None, stderr_level=DEFAULT_STDERR_LEVEL, nickname=None,
Hidehiko Abed939d662017-06-13 20:25:03 +0900125 unjoinable=False, env=None, extra_paths=None):
Allen Li5ed7e632017-02-03 16:31:33 -0800126 """Create and start a new BgJob.
127
128 This constructor creates a new BgJob, and uses Popen to start a new
129 subprocess with given command. It returns without blocking on execution
130 of the subprocess.
131
132 After starting a new BgJob, use output_prepare to connect the process's
133 stdout and stderr pipes to the stream of your choice.
134
135 When the job is running, the jobs's output streams are only read from
136 when process_output is called.
137
138 @param command: command to be executed in new subprocess. May be either
139 a list, or a string (in which case Popen will be called
140 with shell=True)
Hidehiko Abed939d662017-06-13 20:25:03 +0900141 @param stdout_tee: (Optional) a file like object, TEE_TO_LOGS or
142 DEVNULL.
143 If not given, after finishing the process, the
144 stdout data from subprocess is available in
145 result.stdout.
146 If a file like object is given, in process_output(),
147 the stdout data from the subprocess will be handled
148 by the given file like object.
149 If TEE_TO_LOGS is given, in process_output(), the
150 stdout data from the subprocess will be handled by
Allen Li5ed7e632017-02-03 16:31:33 -0800151 the standard logging_manager.
Hidehiko Abed939d662017-06-13 20:25:03 +0900152 If DEVNULL is given, the stdout of the subprocess
153 will be just discarded. In addition, even after
154 cleanup(), result.stdout will be just an empty
155 string (unlike the case where stdout_tee is not
156 given).
Allen Li5ed7e632017-02-03 16:31:33 -0800157 @param stderr_tee: Same as stdout_tee, but for stderr.
158 @param verbose: Boolean, make BgJob logging more verbose.
159 @param stdin: Stream object, will be passed to Popen as the new
160 process's stdin.
161 @param stderr_level: A logging level value. If stderr_tee was set to
162 TEE_TO_LOGS, sets the level that tee'd
163 stderr output will be logged at. Ignored
164 otherwise.
165 @param nickname: Optional string, to be included in logging messages
Hidehiko Abed939d662017-06-13 20:25:03 +0900166 @param unjoinable: Optional bool, default False.
167 This should be True for BgJobs running in background
168 and will never be joined with join_bg_jobs(), such
169 as the master-ssh connection. Instead, it is
170 caller's responsibility to terminate the subprocess
171 correctly, e.g. by calling nuke_subprocess().
172 This will lead that, calling join_bg_jobs(),
173 process_output() or cleanup() will result in an
174 InvalidBgJobCall exception.
175 Also, |stdout_tee| and |stderr_tee| must be set to
176 DEVNULL, otherwise InvalidBgJobCall is raised.
Allen Li5ed7e632017-02-03 16:31:33 -0800177 @param env: Dict containing environment variables used in subprocess.
178 @param extra_paths: Optional string list, to be prepended to the PATH
179 env variable in env (or os.environ dict if env is
180 not specified).
181 """
182 self.command = command
Hidehiko Abed939d662017-06-13 20:25:03 +0900183 self.unjoinable = unjoinable
184 if (unjoinable and (stdout_tee != DEVNULL or stderr_tee != DEVNULL)):
185 raise error.InvalidBgJobCall(
186 'stdout_tee and stderr_tee must be DEVNULL for '
187 'unjoinable BgJob')
188 self._stdout_tee = get_stream_tee_file(
189 stdout_tee, DEFAULT_STDOUT_LEVEL,
Allen Li5ed7e632017-02-03 16:31:33 -0800190 prefix=_join_with_nickname(STDOUT_PREFIX, nickname))
Hidehiko Abed939d662017-06-13 20:25:03 +0900191 self._stderr_tee = get_stream_tee_file(
192 stderr_tee, stderr_level,
Allen Li5ed7e632017-02-03 16:31:33 -0800193 prefix=_join_with_nickname(STDERR_PREFIX, nickname))
194 self.result = CmdResult(command)
195
196 # allow for easy stdin input by string, we'll let subprocess create
197 # a pipe for stdin input and we'll write to it in the wait loop
198 if isinstance(stdin, basestring):
199 self.string_stdin = stdin
200 stdin = subprocess.PIPE
201 else:
202 self.string_stdin = None
203
Allen Li5ed7e632017-02-03 16:31:33 -0800204 # Prepend extra_paths to env['PATH'] if necessary.
205 if extra_paths:
206 env = (os.environ if env is None else env).copy()
207 oldpath = env.get('PATH')
208 env['PATH'] = os.pathsep.join(
209 extra_paths + ([oldpath] if oldpath else []))
210
211 if verbose:
212 logging.debug("Running '%s'", command)
Hidehiko Abed939d662017-06-13 20:25:03 +0900213
Allen Li5ed7e632017-02-03 16:31:33 -0800214 if type(command) == list:
Hidehiko Abed939d662017-06-13 20:25:03 +0900215 shell = False
216 executable = None
Allen Li5ed7e632017-02-03 16:31:33 -0800217 else:
Hidehiko Abed939d662017-06-13 20:25:03 +0900218 shell = True
219 executable = '/bin/bash'
Allen Li5ed7e632017-02-03 16:31:33 -0800220
Hidehiko Abed939d662017-06-13 20:25:03 +0900221 with open('/dev/null', 'w') as devnull:
222 self.sp = subprocess.Popen(
223 command,
224 stdin=stdin,
225 stdout=devnull if stdout_tee == DEVNULL else subprocess.PIPE,
226 stderr=devnull if stderr_tee == DEVNULL else subprocess.PIPE,
227 preexec_fn=self._reset_sigpipe,
228 shell=shell, executable=executable,
229 env=env, close_fds=True)
230
Allen Li5ed7e632017-02-03 16:31:33 -0800231 self._cleanup_called = False
Hidehiko Abed939d662017-06-13 20:25:03 +0900232 self._stdout_file = (
233 None if stdout_tee == DEVNULL else StringIO.StringIO())
234 self._stderr_file = (
235 None if stderr_tee == DEVNULL else StringIO.StringIO())
Allen Li5ed7e632017-02-03 16:31:33 -0800236
237 def process_output(self, stdout=True, final_read=False):
238 """Read from process's output stream, and write data to destinations.
239
240 This function reads up to 1024 bytes from the background job's
241 stdout or stderr stream, and writes the resulting data to the BgJob's
242 output tee and to the stream set up in output_prepare.
243
244 Warning: Calls to process_output will block on reads from the
245 subprocess stream, and will block on writes to the configured
246 destination stream.
247
248 @param stdout: True = read and process data from job's stdout.
249 False = from stderr.
250 Default: True
251 @param final_read: Do not read only 1024 bytes from stream. Instead,
252 read and process all data until end of the stream.
253
254 """
Hidehiko Abed939d662017-06-13 20:25:03 +0900255 if self.unjoinable:
Allen Li5ed7e632017-02-03 16:31:33 -0800256 raise error.InvalidBgJobCall('Cannot call process_output on '
Hidehiko Abed939d662017-06-13 20:25:03 +0900257 'a job with unjoinable BgJob')
Allen Li5ed7e632017-02-03 16:31:33 -0800258 if stdout:
Hidehiko Abed939d662017-06-13 20:25:03 +0900259 pipe, buf, tee = (
260 self.sp.stdout, self._stdout_file, self._stdout_tee)
Allen Li5ed7e632017-02-03 16:31:33 -0800261 else:
Hidehiko Abed939d662017-06-13 20:25:03 +0900262 pipe, buf, tee = (
263 self.sp.stderr, self._stderr_file, self._stderr_tee)
264
265 if not pipe:
266 return
Allen Li5ed7e632017-02-03 16:31:33 -0800267
268 if final_read:
269 # read in all the data we can from pipe and then stop
270 data = []
271 while select.select([pipe], [], [], 0)[0]:
272 data.append(os.read(pipe.fileno(), 1024))
273 if len(data[-1]) == 0:
274 break
275 data = "".join(data)
276 else:
277 # perform a single read
278 data = os.read(pipe.fileno(), 1024)
279 buf.write(data)
280 tee.write(data)
281
Allen Li5ed7e632017-02-03 16:31:33 -0800282 def cleanup(self):
283 """Clean up after BgJob.
284
285 Flush the stdout_tee and stderr_tee buffers, close the
286 subprocess stdout and stderr buffers, and saves data from
287 the configured stdout and stderr destination streams to
288 self.result. Duplicate calls ignored with a warning.
289 """
Hidehiko Abed939d662017-06-13 20:25:03 +0900290 if self.unjoinable:
Allen Li5ed7e632017-02-03 16:31:33 -0800291 raise error.InvalidBgJobCall('Cannot call cleanup on '
Hidehiko Abed939d662017-06-13 20:25:03 +0900292 'a job with a unjoinable BgJob')
Allen Li5ed7e632017-02-03 16:31:33 -0800293 if self._cleanup_called:
294 logging.warning('BgJob [%s] received a duplicate call to '
295 'cleanup. Ignoring.', self.command)
296 return
297 try:
Hidehiko Abed939d662017-06-13 20:25:03 +0900298 if self.sp.stdout:
299 self._stdout_tee.flush()
300 self.sp.stdout.close()
301 self.result.stdout = self._stdout_file.getvalue()
302
303 if self.sp.stderr:
304 self._stderr_tee.flush()
305 self.sp.stderr.close()
306 self.result.stderr = self._stderr_file.getvalue()
Allen Li5ed7e632017-02-03 16:31:33 -0800307 finally:
308 self._cleanup_called = True
309
Allen Li5ed7e632017-02-03 16:31:33 -0800310 def _reset_sigpipe(self):
Aviv Keshet12aa4a22017-05-18 16:48:05 -0700311 if not env.IN_MOD_WSGI:
312 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
Allen Li5ed7e632017-02-03 16:31:33 -0800313
314
315def ip_to_long(ip):
316 # !L is a long in network byte order
317 return struct.unpack('!L', socket.inet_aton(ip))[0]
318
319
320def long_to_ip(number):
321 # See above comment.
322 return socket.inet_ntoa(struct.pack('!L', number))
323
324
325def create_subnet_mask(bits):
326 return (1 << 32) - (1 << 32-bits)
327
328
329def format_ip_with_mask(ip, mask_bits):
330 masked_ip = ip_to_long(ip) & create_subnet_mask(mask_bits)
331 return "%s/%s" % (long_to_ip(masked_ip), mask_bits)
332
333
334def normalize_hostname(alias):
335 ip = socket.gethostbyname(alias)
336 return socket.gethostbyaddr(ip)[0]
337
338
339def get_ip_local_port_range():
340 match = re.match(r'\s*(\d+)\s*(\d+)\s*$',
341 read_one_line('/proc/sys/net/ipv4/ip_local_port_range'))
342 return (int(match.group(1)), int(match.group(2)))
343
344
345def set_ip_local_port_range(lower, upper):
346 write_one_line('/proc/sys/net/ipv4/ip_local_port_range',
347 '%d %d\n' % (lower, upper))
348
349
350def read_one_line(filename):
351 return open(filename, 'r').readline().rstrip('\n')
352
353
354def read_file(filename):
355 f = open(filename)
356 try:
357 return f.read()
358 finally:
359 f.close()
360
361
362def get_field(data, param, linestart="", sep=" "):
363 """
364 Parse data from string.
365 @param data: Data to parse.
366 example:
367 data:
368 cpu 324 345 34 5 345
369 cpu0 34 11 34 34 33
370 ^^^^
371 start of line
372 params 0 1 2 3 4
373 @param param: Position of parameter after linestart marker.
374 @param linestart: String to which start line with parameters.
375 @param sep: Separator between parameters regular expression.
376 """
377 search = re.compile(r"(?<=^%s)\s*(.*)" % linestart, re.MULTILINE)
378 find = search.search(data)
379 if find != None:
380 return re.split("%s" % sep, find.group(1))[param]
381 else:
382 print "There is no line which starts with %s in data." % linestart
383 return None
384
385
386def write_one_line(filename, line):
387 open_write_close(filename, str(line).rstrip('\n') + '\n')
388
389
390def open_write_close(filename, data):
391 f = open(filename, 'w')
392 try:
393 f.write(data)
394 finally:
395 f.close()
396
397
398def locate_file(path, base_dir=None):
399 """Locates a file.
400
401 @param path: The path of the file being located. Could be absolute or relative
402 path. For relative path, it tries to locate the file from base_dir.
403 @param base_dir (optional): Base directory of the relative path.
404
405 @returns Absolute path of the file if found. None if path is None.
406 @raises error.TestFail if the file is not found.
407 """
408 if path is None:
409 return None
410
411 if not os.path.isabs(path) and base_dir is not None:
412 # Assume the relative path is based in autotest directory.
413 path = os.path.join(base_dir, path)
414 if not os.path.isfile(path):
415 raise error.TestFail('ERROR: Unable to find %s' % path)
416 return path
417
418
419def matrix_to_string(matrix, header=None):
420 """
421 Return a pretty, aligned string representation of a nxm matrix.
422
423 This representation can be used to print any tabular data, such as
424 database results. It works by scanning the lengths of each element
425 in each column, and determining the format string dynamically.
426
427 @param matrix: Matrix representation (list with n rows of m elements).
428 @param header: Optional tuple or list with header elements to be displayed.
429 """
430 if type(header) is list:
431 header = tuple(header)
432 lengths = []
433 if header:
434 for column in header:
435 lengths.append(len(column))
436 for row in matrix:
437 for i, column in enumerate(row):
438 column = unicode(column).encode("utf-8")
439 cl = len(column)
440 try:
441 ml = lengths[i]
442 if cl > ml:
443 lengths[i] = cl
444 except IndexError:
445 lengths.append(cl)
446
447 lengths = tuple(lengths)
448 format_string = ""
449 for length in lengths:
450 format_string += "%-" + str(length) + "s "
451 format_string += "\n"
452
453 matrix_str = ""
454 if header:
455 matrix_str += format_string % header
456 for row in matrix:
457 matrix_str += format_string % tuple(row)
458
459 return matrix_str
460
461
462def read_keyval(path, type_tag=None):
463 """
464 Read a key-value pair format file into a dictionary, and return it.
465 Takes either a filename or directory name as input. If it's a
466 directory name, we assume you want the file to be called keyval.
467
468 @param path: Full path of the file to read from.
469 @param type_tag: If not None, only keyvals with key ending
470 in a suffix {type_tag} will be collected.
471 """
472 if os.path.isdir(path):
473 path = os.path.join(path, 'keyval')
474 if not os.path.exists(path):
475 return {}
476
477 if type_tag:
478 pattern = r'^([-\.\w]+)\{%s\}=(.*)$' % type_tag
479 else:
480 pattern = r'^([-\.\w]+)=(.*)$'
481
482 keyval = {}
483 f = open(path)
484 for line in f:
485 line = re.sub('#.*', '', line).rstrip()
486 if not line:
487 continue
488 match = re.match(pattern, line)
489 if match:
490 key = match.group(1)
491 value = match.group(2)
492 if re.search('^\d+$', value):
493 value = int(value)
494 elif re.search('^(\d+\.)?\d+$', value):
495 value = float(value)
496 keyval[key] = value
497 else:
498 raise ValueError('Invalid format line: %s' % line)
499 f.close()
500 return keyval
501
502
Aviv Keshetf0c82242017-05-18 22:06:40 -0700503def write_keyval(path, dictionary, type_tag=None):
Allen Li5ed7e632017-02-03 16:31:33 -0800504 """
505 Write a key-value pair format file out to a file. This uses append
506 mode to open the file, so existing text will not be overwritten or
507 reparsed.
508
509 If type_tag is None, then the key must be composed of alphanumeric
510 characters (or dashes+underscores). However, if type-tag is not
511 null then the keys must also have "{type_tag}" as a suffix. At
512 the moment the only valid values of type_tag are "attr" and "perf".
513
514 @param path: full path of the file to be written
515 @param dictionary: the items to write
516 @param type_tag: see text above
517 """
518 if os.path.isdir(path):
519 path = os.path.join(path, 'keyval')
520 keyval = open(path, 'a')
521
522 if type_tag is None:
523 key_regex = re.compile(r'^[-\.\w]+$')
524 else:
525 if type_tag not in ('attr', 'perf'):
526 raise ValueError('Invalid type tag: %s' % type_tag)
527 escaped_tag = re.escape(type_tag)
528 key_regex = re.compile(r'^[-\.\w]+\{%s\}$' % escaped_tag)
529 try:
530 for key in sorted(dictionary.keys()):
531 if not key_regex.search(key):
532 raise ValueError('Invalid key: %s' % key)
533 keyval.write('%s=%s\n' % (key, dictionary[key]))
534 finally:
535 keyval.close()
536
Allen Li5ed7e632017-02-03 16:31:33 -0800537
538def is_url(path):
539 """Return true if path looks like a URL"""
540 # for now, just handle http and ftp
541 url_parts = urlparse.urlparse(path)
542 return (url_parts[0] in ('http', 'ftp'))
543
544
545def urlopen(url, data=None, timeout=5):
546 """Wrapper to urllib2.urlopen with timeout addition."""
547
548 # Save old timeout
549 old_timeout = socket.getdefaulttimeout()
550 socket.setdefaulttimeout(timeout)
551 try:
552 return urllib2.urlopen(url, data=data)
553 finally:
554 socket.setdefaulttimeout(old_timeout)
555
556
557def urlretrieve(url, filename, data=None, timeout=300):
558 """Retrieve a file from given url."""
559 logging.debug('Fetching %s -> %s', url, filename)
560
561 src_file = urlopen(url, data=data, timeout=timeout)
562 try:
563 dest_file = open(filename, 'wb')
564 try:
565 shutil.copyfileobj(src_file, dest_file)
566 finally:
567 dest_file.close()
568 finally:
569 src_file.close()
570
571
572def hash(type, input=None):
573 """
574 Returns an hash object of type md5 or sha1. This function is implemented in
575 order to encapsulate hash objects in a way that is compatible with python
576 2.4 and python 2.6 without warnings.
577
578 Note that even though python 2.6 hashlib supports hash types other than
579 md5 and sha1, we are artificially limiting the input values in order to
580 make the function to behave exactly the same among both python
581 implementations.
582
583 @param input: Optional input string that will be used to update the hash.
584 """
585 if type not in ['md5', 'sha1']:
586 raise ValueError("Unsupported hash type: %s" % type)
587
588 try:
589 hash = hashlib.new(type)
590 except NameError:
591 if type == 'md5':
592 hash = md5.new()
593 elif type == 'sha1':
594 hash = sha.new()
595
596 if input:
597 hash.update(input)
598
599 return hash
600
601
602def get_file(src, dest, permissions=None):
603 """Get a file from src, which can be local or a remote URL"""
604 if src == dest:
605 return
606
607 if is_url(src):
608 urlretrieve(src, dest)
609 else:
610 shutil.copyfile(src, dest)
611
612 if permissions:
613 os.chmod(dest, permissions)
614 return dest
615
616
617def unmap_url(srcdir, src, destdir='.'):
618 """
619 Receives either a path to a local file or a URL.
620 returns either the path to the local file, or the fetched URL
621
622 unmap_url('/usr/src', 'foo.tar', '/tmp')
623 = '/usr/src/foo.tar'
624 unmap_url('/usr/src', 'http://site/file', '/tmp')
625 = '/tmp/file'
626 (after retrieving it)
627 """
628 if is_url(src):
629 url_parts = urlparse.urlparse(src)
630 filename = os.path.basename(url_parts[2])
631 dest = os.path.join(destdir, filename)
632 return get_file(src, dest)
633 else:
634 return os.path.join(srcdir, src)
635
636
637def update_version(srcdir, preserve_srcdir, new_version, install,
638 *args, **dargs):
639 """
640 Make sure srcdir is version new_version
641
642 If not, delete it and install() the new version.
643
644 In the preserve_srcdir case, we just check it's up to date,
645 and if not, we rerun install, without removing srcdir
646 """
647 versionfile = os.path.join(srcdir, '.version')
648 install_needed = True
649
650 if os.path.exists(versionfile):
651 old_version = pickle.load(open(versionfile))
652 if old_version == new_version:
653 install_needed = False
654
655 if install_needed:
656 if not preserve_srcdir and os.path.exists(srcdir):
657 shutil.rmtree(srcdir)
658 install(*args, **dargs)
659 if os.path.exists(srcdir):
660 pickle.dump(new_version, open(versionfile, 'w'))
661
662
663def get_stderr_level(stderr_is_expected):
664 if stderr_is_expected:
665 return DEFAULT_STDOUT_LEVEL
666 return DEFAULT_STDERR_LEVEL
667
668
669def run(command, timeout=None, ignore_status=False,
670 stdout_tee=None, stderr_tee=None, verbose=True, stdin=None,
671 stderr_is_expected=None, args=(), nickname=None, ignore_timeout=False,
672 env=None, extra_paths=None):
673 """
674 Run a command on the host.
675
676 @param command: the command line string.
677 @param timeout: time limit in seconds before attempting to kill the
678 running process. The run() function will take a few seconds
679 longer than 'timeout' to complete if it has to kill the process.
680 @param ignore_status: do not raise an exception, no matter what the exit
681 code of the command is.
682 @param stdout_tee: optional file-like object to which stdout data
683 will be written as it is generated (data will still be stored
684 in result.stdout).
685 @param stderr_tee: likewise for stderr.
686 @param verbose: if True, log the command being run.
687 @param stdin: stdin to pass to the executed process (can be a file
688 descriptor, a file object of a real file or a string).
689 @param stderr_is_expected: if True, stderr will be logged at the same level
690 as stdout
691 @param args: sequence of strings of arguments to be given to the command
692 inside " quotes after they have been escaped for that; each
693 element in the sequence will be given as a separate command
694 argument
695 @param nickname: Short string that will appear in logging messages
696 associated with this command.
697 @param ignore_timeout: If True, timeouts are ignored otherwise if a
698 timeout occurs it will raise CmdTimeoutError.
699 @param env: Dict containing environment variables used in a subprocess.
700 @param extra_paths: Optional string list, to be prepended to the PATH
701 env variable in env (or os.environ dict if env is
702 not specified).
703
704 @return a CmdResult object or None if the command timed out and
705 ignore_timeout is True
706
707 @raise CmdError: the exit code of the command execution was not 0
708 @raise CmdTimeoutError: the command timed out and ignore_timeout is False.
709 """
710 if isinstance(args, basestring):
711 raise TypeError('Got a string for the "args" keyword argument, '
712 'need a sequence.')
713
714 # In some cases, command will actually be a list
715 # (For example, see get_user_hash in client/cros/cryptohome.py.)
716 # So, to cover that case, detect if it's a string or not and convert it
717 # into one if necessary.
718 if not isinstance(command, basestring):
719 command = ' '.join([sh_quote_word(arg) for arg in command])
720
721 command = ' '.join([command] + [sh_quote_word(arg) for arg in args])
722 if stderr_is_expected is None:
723 stderr_is_expected = ignore_status
724
725 try:
726 bg_job = join_bg_jobs(
727 (BgJob(command, stdout_tee, stderr_tee, verbose, stdin=stdin,
728 stderr_level=get_stderr_level(stderr_is_expected),
729 nickname=nickname, env=env, extra_paths=extra_paths),),
730 timeout)[0]
731 except error.CmdTimeoutError:
732 if not ignore_timeout:
733 raise
734 return None
735
736 if not ignore_status and bg_job.result.exit_status:
737 raise error.CmdError(command, bg_job.result,
738 "Command returned non-zero exit status")
739
740 return bg_job.result
741
742
743def run_parallel(commands, timeout=None, ignore_status=False,
744 stdout_tee=None, stderr_tee=None,
745 nicknames=[]):
746 """
747 Behaves the same as run() with the following exceptions:
748
749 - commands is a list of commands to run in parallel.
750 - ignore_status toggles whether or not an exception should be raised
751 on any error.
752
753 @return: a list of CmdResult objects
754 """
755 bg_jobs = []
756 for (command, nickname) in itertools.izip_longest(commands, nicknames):
757 bg_jobs.append(BgJob(command, stdout_tee, stderr_tee,
758 stderr_level=get_stderr_level(ignore_status),
759 nickname=nickname))
760
761 # Updates objects in bg_jobs list with their process information
762 join_bg_jobs(bg_jobs, timeout)
763
764 for bg_job in bg_jobs:
765 if not ignore_status and bg_job.result.exit_status:
766 raise error.CmdError(command, bg_job.result,
767 "Command returned non-zero exit status")
768
769 return [bg_job.result for bg_job in bg_jobs]
770
771
772@deprecated
773def run_bg(command):
774 """Function deprecated. Please use BgJob class instead."""
775 bg_job = BgJob(command)
776 return bg_job.sp, bg_job.result
777
778
779def join_bg_jobs(bg_jobs, timeout=None):
780 """Joins the bg_jobs with the current thread.
781
782 Returns the same list of bg_jobs objects that was passed in.
783 """
Hidehiko Abed939d662017-06-13 20:25:03 +0900784 if any(bg_job.unjoinable for bg_job in bg_jobs):
785 raise error.InvalidBgJobCall(
786 'join_bg_jobs cannot be called for unjoinable bg_job')
Allen Li5ed7e632017-02-03 16:31:33 -0800787
Hidehiko Abed939d662017-06-13 20:25:03 +0900788 timeout_error = False
Allen Li5ed7e632017-02-03 16:31:33 -0800789 try:
790 # We are holding ends to stdin, stdout pipes
791 # hence we need to be sure to close those fds no mater what
792 start_time = time.time()
793 timeout_error = _wait_for_commands(bg_jobs, start_time, timeout)
794
795 for bg_job in bg_jobs:
796 # Process stdout and stderr
797 bg_job.process_output(stdout=True,final_read=True)
798 bg_job.process_output(stdout=False,final_read=True)
799 finally:
800 # close our ends of the pipes to the sp no matter what
801 for bg_job in bg_jobs:
802 bg_job.cleanup()
803
804 if timeout_error:
805 # TODO: This needs to be fixed to better represent what happens when
806 # running in parallel. However this is backwards compatable, so it will
807 # do for the time being.
808 raise error.CmdTimeoutError(
809 bg_jobs[0].command, bg_jobs[0].result,
810 "Command(s) did not complete within %d seconds" % timeout)
811
812
813 return bg_jobs
814
815
816def _wait_for_commands(bg_jobs, start_time, timeout):
817 """Waits for background jobs by select polling their stdout/stderr.
818
819 @param bg_jobs: A list of background jobs to wait on.
820 @param start_time: Time used to calculate the timeout lifetime of a job.
821 @param timeout: The timeout of the list of bg_jobs.
822
823 @return: True if the return was due to a timeout, False otherwise.
824 """
825
826 # To check for processes which terminate without producing any output
827 # a 1 second timeout is used in select.
828 SELECT_TIMEOUT = 1
829
830 read_list = []
831 write_list = []
832 reverse_dict = {}
833
834 for bg_job in bg_jobs:
Hidehiko Abed939d662017-06-13 20:25:03 +0900835 if bg_job.sp.stdout:
836 read_list.append(bg_job.sp.stdout)
837 reverse_dict[bg_job.sp.stdout] = (bg_job, True)
838 if bg_job.sp.stderr:
839 read_list.append(bg_job.sp.stderr)
840 reverse_dict[bg_job.sp.stderr] = (bg_job, False)
Allen Li5ed7e632017-02-03 16:31:33 -0800841 if bg_job.string_stdin is not None:
842 write_list.append(bg_job.sp.stdin)
843 reverse_dict[bg_job.sp.stdin] = bg_job
844
845 if timeout:
846 stop_time = start_time + timeout
847 time_left = stop_time - time.time()
848 else:
849 time_left = None # so that select never times out
850
851 while not timeout or time_left > 0:
852 # select will return when we may write to stdin, when there is
853 # stdout/stderr output we can read (including when it is
854 # EOF, that is the process has terminated) or when a non-fatal
855 # signal was sent to the process. In the last case the select returns
856 # EINTR, and we continue waiting for the job if the signal handler for
857 # the signal that interrupted the call allows us to.
858 try:
859 read_ready, write_ready, _ = select.select(read_list, write_list,
860 [], SELECT_TIMEOUT)
861 except select.error as v:
862 if v[0] == errno.EINTR:
863 logging.warning(v)
864 continue
865 else:
866 raise
867 # os.read() has to be used instead of
868 # subproc.stdout.read() which will otherwise block
869 for file_obj in read_ready:
870 bg_job, is_stdout = reverse_dict[file_obj]
871 bg_job.process_output(is_stdout)
872
873 for file_obj in write_ready:
874 # we can write PIPE_BUF bytes without blocking
875 # POSIX requires PIPE_BUF is >= 512
876 bg_job = reverse_dict[file_obj]
877 file_obj.write(bg_job.string_stdin[:512])
878 bg_job.string_stdin = bg_job.string_stdin[512:]
879 # no more input data, close stdin, remove it from the select set
880 if not bg_job.string_stdin:
881 file_obj.close()
882 write_list.remove(file_obj)
883 del reverse_dict[file_obj]
884
885 all_jobs_finished = True
886 for bg_job in bg_jobs:
887 if bg_job.result.exit_status is not None:
888 continue
889
890 bg_job.result.exit_status = bg_job.sp.poll()
891 if bg_job.result.exit_status is not None:
892 # process exited, remove its stdout/stdin from the select set
893 bg_job.result.duration = time.time() - start_time
Hidehiko Abed939d662017-06-13 20:25:03 +0900894 if bg_job.sp.stdout:
895 read_list.remove(bg_job.sp.stdout)
896 del reverse_dict[bg_job.sp.stdout]
897 if bg_job.sp.stderr:
898 read_list.remove(bg_job.sp.stderr)
899 del reverse_dict[bg_job.sp.stderr]
Allen Li5ed7e632017-02-03 16:31:33 -0800900 else:
901 all_jobs_finished = False
902
903 if all_jobs_finished:
904 return False
905
906 if timeout:
907 time_left = stop_time - time.time()
908
909 # Kill all processes which did not complete prior to timeout
910 for bg_job in bg_jobs:
911 if bg_job.result.exit_status is not None:
912 continue
913
914 logging.warning('run process timeout (%s) fired on: %s', timeout,
915 bg_job.command)
916 if nuke_subprocess(bg_job.sp) is None:
917 # If process could not be SIGKILL'd, log kernel stack.
918 logging.warning(read_file('/proc/%d/stack' % bg_job.sp.pid))
919 bg_job.result.exit_status = bg_job.sp.poll()
920 bg_job.result.duration = time.time() - start_time
921
922 return True
923
924
925def pid_is_alive(pid):
926 """
927 True if process pid exists and is not yet stuck in Zombie state.
928 Zombies are impossible to move between cgroups, etc.
929 pid can be integer, or text of integer.
930 """
931 path = '/proc/%s/stat' % pid
932
933 try:
934 stat = read_one_line(path)
935 except IOError:
936 if not os.path.exists(path):
937 # file went away
938 return False
939 raise
940
941 return stat.split()[2] != 'Z'
942
943
944def signal_pid(pid, sig):
945 """
946 Sends a signal to a process id. Returns True if the process terminated
947 successfully, False otherwise.
948 """
949 try:
950 os.kill(pid, sig)
951 except OSError:
952 # The process may have died before we could kill it.
953 pass
954
955 for i in range(5):
956 if not pid_is_alive(pid):
957 return True
958 time.sleep(1)
959
960 # The process is still alive
961 return False
962
963
964def nuke_subprocess(subproc):
965 # check if the subprocess is still alive, first
966 if subproc.poll() is not None:
967 return subproc.poll()
968
969 # the process has not terminated within timeout,
970 # kill it via an escalating series of signals.
971 signal_queue = [signal.SIGTERM, signal.SIGKILL]
972 for sig in signal_queue:
973 signal_pid(subproc.pid, sig)
974 if subproc.poll() is not None:
975 return subproc.poll()
976
977
978def nuke_pid(pid, signal_queue=(signal.SIGTERM, signal.SIGKILL)):
979 # the process has not terminated within timeout,
980 # kill it via an escalating series of signals.
981 pid_path = '/proc/%d/'
982 if not os.path.exists(pid_path % pid):
983 # Assume that if the pid does not exist in proc it is already dead.
984 logging.error('No listing in /proc for pid:%d.', pid)
985 raise error.AutoservPidAlreadyDeadError('Could not kill nonexistant '
986 'pid: %s.', pid)
987 for sig in signal_queue:
988 if signal_pid(pid, sig):
989 return
990
991 # no signal successfully terminated the process
992 raise error.AutoservRunError('Could not kill %d for process name: %s' % (
993 pid, get_process_name(pid)), None)
994
995
996def system(command, timeout=None, ignore_status=False):
997 """
998 Run a command
999
1000 @param timeout: timeout in seconds
1001 @param ignore_status: if ignore_status=False, throw an exception if the
1002 command's exit code is non-zero
1003 if ignore_stauts=True, return the exit code.
1004
1005 @return exit status of command
1006 (note, this will always be zero unless ignore_status=True)
1007 """
1008 return run(command, timeout=timeout, ignore_status=ignore_status,
1009 stdout_tee=TEE_TO_LOGS, stderr_tee=TEE_TO_LOGS).exit_status
1010
1011
1012def system_parallel(commands, timeout=None, ignore_status=False):
1013 """This function returns a list of exit statuses for the respective
1014 list of commands."""
1015 return [bg_jobs.exit_status for bg_jobs in
1016 run_parallel(commands, timeout=timeout, ignore_status=ignore_status,
1017 stdout_tee=TEE_TO_LOGS, stderr_tee=TEE_TO_LOGS)]
1018
1019
1020def system_output(command, timeout=None, ignore_status=False,
1021 retain_output=False, args=()):
1022 """
1023 Run a command and return the stdout output.
1024
1025 @param command: command string to execute.
1026 @param timeout: time limit in seconds before attempting to kill the
1027 running process. The function will take a few seconds longer
1028 than 'timeout' to complete if it has to kill the process.
1029 @param ignore_status: do not raise an exception, no matter what the exit
1030 code of the command is.
1031 @param retain_output: set to True to make stdout/stderr of the command
1032 output to be also sent to the logging system
1033 @param args: sequence of strings of arguments to be given to the command
1034 inside " quotes after they have been escaped for that; each
1035 element in the sequence will be given as a separate command
1036 argument
1037
1038 @return a string with the stdout output of the command.
1039 """
1040 if retain_output:
1041 out = run(command, timeout=timeout, ignore_status=ignore_status,
1042 stdout_tee=TEE_TO_LOGS, stderr_tee=TEE_TO_LOGS,
1043 args=args).stdout
1044 else:
1045 out = run(command, timeout=timeout, ignore_status=ignore_status,
1046 args=args).stdout
1047 if out[-1:] == '\n':
1048 out = out[:-1]
1049 return out
1050
1051
1052def system_output_parallel(commands, timeout=None, ignore_status=False,
1053 retain_output=False):
1054 if retain_output:
1055 out = [bg_job.stdout for bg_job
1056 in run_parallel(commands, timeout=timeout,
1057 ignore_status=ignore_status,
1058 stdout_tee=TEE_TO_LOGS, stderr_tee=TEE_TO_LOGS)]
1059 else:
1060 out = [bg_job.stdout for bg_job in run_parallel(commands,
1061 timeout=timeout, ignore_status=ignore_status)]
1062 for x in out:
1063 if out[-1:] == '\n': out = out[:-1]
1064 return out
1065
1066
1067def strip_unicode(input):
1068 if type(input) == list:
1069 return [strip_unicode(i) for i in input]
1070 elif type(input) == dict:
1071 output = {}
1072 for key in input.keys():
1073 output[str(key)] = strip_unicode(input[key])
1074 return output
1075 elif type(input) == unicode:
1076 return str(input)
1077 else:
1078 return input
1079
1080
1081def get_cpu_percentage(function, *args, **dargs):
1082 """Returns a tuple containing the CPU% and return value from function call.
1083
1084 This function calculates the usage time by taking the difference of
1085 the user and system times both before and after the function call.
1086 """
1087 child_pre = resource.getrusage(resource.RUSAGE_CHILDREN)
1088 self_pre = resource.getrusage(resource.RUSAGE_SELF)
1089 start = time.time()
1090 to_return = function(*args, **dargs)
1091 elapsed = time.time() - start
1092 self_post = resource.getrusage(resource.RUSAGE_SELF)
1093 child_post = resource.getrusage(resource.RUSAGE_CHILDREN)
1094
1095 # Calculate CPU Percentage
1096 s_user, s_system = [a - b for a, b in zip(self_post, self_pre)[:2]]
1097 c_user, c_system = [a - b for a, b in zip(child_post, child_pre)[:2]]
1098 cpu_percent = (s_user + c_user + s_system + c_system) / elapsed
1099
1100 return cpu_percent, to_return
1101
1102
1103def get_arch(run_function=run):
1104 """
1105 Get the hardware architecture of the machine.
1106 If specified, run_function should return a CmdResult object and throw a
1107 CmdError exception.
1108 If run_function is anything other than utils.run(), it is used to
1109 execute the commands. By default (when set to utils.run()) this will
1110 just examine os.uname()[4].
1111 """
1112
1113 # Short circuit from the common case.
1114 if run_function == run:
1115 return re.sub(r'i\d86$', 'i386', os.uname()[4])
1116
1117 # Otherwise, use the run_function in case it hits a remote machine.
1118 arch = run_function('/bin/uname -m').stdout.rstrip()
1119 if re.match(r'i\d86$', arch):
1120 arch = 'i386'
1121 return arch
1122
1123def get_arch_userspace(run_function=run):
1124 """
1125 Get the architecture by userspace (possibly different from kernel).
1126 """
1127 archs = {
1128 'arm': 'ELF 32-bit.*, ARM,',
1129 'i386': 'ELF 32-bit.*, Intel 80386,',
1130 'x86_64': 'ELF 64-bit.*, x86-64,',
1131 }
1132
1133 cmd = 'file --brief --dereference /bin/sh'
1134 filestr = run_function(cmd).stdout.rstrip()
1135 for a, regex in archs.iteritems():
1136 if re.match(regex, filestr):
1137 return a
1138
1139 return get_arch()
1140
1141
1142def get_num_logical_cpus_per_socket(run_function=run):
1143 """
1144 Get the number of cores (including hyperthreading) per cpu.
1145 run_function is used to execute the commands. It defaults to
1146 utils.run() but a custom method (if provided) should be of the
1147 same schema as utils.run. It should return a CmdResult object and
1148 throw a CmdError exception.
1149 """
1150 siblings = run_function('grep "^siblings" /proc/cpuinfo').stdout.rstrip()
1151 num_siblings = map(int,
1152 re.findall(r'^siblings\s*:\s*(\d+)\s*$',
1153 siblings, re.M))
1154 if len(num_siblings) == 0:
1155 raise error.TestError('Unable to find siblings info in /proc/cpuinfo')
1156 if min(num_siblings) != max(num_siblings):
1157 raise error.TestError('Number of siblings differ %r' %
1158 num_siblings)
1159 return num_siblings[0]
1160
1161
1162def merge_trees(src, dest):
1163 """
1164 Merges a source directory tree at 'src' into a destination tree at
1165 'dest'. If a path is a file in both trees than the file in the source
1166 tree is APPENDED to the one in the destination tree. If a path is
1167 a directory in both trees then the directories are recursively merged
1168 with this function. In any other case, the function will skip the
1169 paths that cannot be merged (instead of failing).
1170 """
1171 if not os.path.exists(src):
1172 return # exists only in dest
1173 elif not os.path.exists(dest):
1174 if os.path.isfile(src):
1175 shutil.copy2(src, dest) # file only in src
1176 else:
1177 shutil.copytree(src, dest, symlinks=True) # dir only in src
1178 return
1179 elif os.path.isfile(src) and os.path.isfile(dest):
1180 # src & dest are files in both trees, append src to dest
1181 destfile = open(dest, "a")
1182 try:
1183 srcfile = open(src)
1184 try:
1185 destfile.write(srcfile.read())
1186 finally:
1187 srcfile.close()
1188 finally:
1189 destfile.close()
1190 elif os.path.isdir(src) and os.path.isdir(dest):
1191 # src & dest are directories in both trees, so recursively merge
1192 for name in os.listdir(src):
1193 merge_trees(os.path.join(src, name), os.path.join(dest, name))
1194 else:
1195 # src & dest both exist, but are incompatible
1196 return
1197
1198
1199class CmdResult(object):
1200 """
1201 Command execution result.
1202
1203 command: String containing the command line itself
1204 exit_status: Integer exit code of the process
1205 stdout: String containing stdout of the process
1206 stderr: String containing stderr of the process
1207 duration: Elapsed wall clock time running the process
1208 """
1209
1210
1211 def __init__(self, command="", stdout="", stderr="",
1212 exit_status=None, duration=0):
1213 self.command = command
1214 self.exit_status = exit_status
1215 self.stdout = stdout
1216 self.stderr = stderr
1217 self.duration = duration
1218
1219
Prathmesh Prabhu54905412017-07-11 17:08:38 -07001220 def __eq__(self, other):
1221 if type(self) == type(other):
1222 return (self.command == other.command
1223 and self.exit_status == other.exit_status
1224 and self.stdout == other.stdout
1225 and self.stderr == other.stderr
1226 and self.duration == other.duration)
1227 else:
1228 return NotImplemented
1229
1230
Allen Li5ed7e632017-02-03 16:31:33 -08001231 def __repr__(self):
1232 wrapper = textwrap.TextWrapper(width = 78,
1233 initial_indent="\n ",
1234 subsequent_indent=" ")
1235
1236 stdout = self.stdout.rstrip()
1237 if stdout:
1238 stdout = "\nstdout:\n%s" % stdout
1239
1240 stderr = self.stderr.rstrip()
1241 if stderr:
1242 stderr = "\nstderr:\n%s" % stderr
1243
1244 return ("* Command: %s\n"
1245 "Exit status: %s\n"
1246 "Duration: %s\n"
1247 "%s"
1248 "%s"
1249 % (wrapper.fill(str(self.command)), self.exit_status,
1250 self.duration, stdout, stderr))
1251
1252
1253class run_randomly:
1254 def __init__(self, run_sequentially=False):
1255 # Run sequentially is for debugging control files
1256 self.test_list = []
1257 self.run_sequentially = run_sequentially
1258
1259
1260 def add(self, *args, **dargs):
1261 test = (args, dargs)
1262 self.test_list.append(test)
1263
1264
1265 def run(self, fn):
1266 while self.test_list:
1267 test_index = random.randint(0, len(self.test_list)-1)
1268 if self.run_sequentially:
1269 test_index = 0
1270 (args, dargs) = self.test_list.pop(test_index)
1271 fn(*args, **dargs)
1272
1273
1274def import_site_module(path, module, dummy=None, modulefile=None):
1275 """
1276 Try to import the site specific module if it exists.
1277
1278 @param path full filename of the source file calling this (ie __file__)
1279 @param module full module name
1280 @param dummy dummy value to return in case there is no symbol to import
1281 @param modulefile module filename
1282
1283 @return site specific module or dummy
1284
1285 @raises ImportError if the site file exists but imports fails
1286 """
1287 short_module = module[module.rfind(".") + 1:]
1288
1289 if not modulefile:
1290 modulefile = short_module + ".py"
1291
1292 if os.path.exists(os.path.join(os.path.dirname(path), modulefile)):
1293 return __import__(module, {}, {}, [short_module])
1294 return dummy
1295
1296
1297def import_site_symbol(path, module, name, dummy=None, modulefile=None):
1298 """
1299 Try to import site specific symbol from site specific file if it exists
1300
1301 @param path full filename of the source file calling this (ie __file__)
1302 @param module full module name
1303 @param name symbol name to be imported from the site file
1304 @param dummy dummy value to return in case there is no symbol to import
1305 @param modulefile module filename
1306
1307 @return site specific symbol or dummy
1308
1309 @raises ImportError if the site file exists but imports fails
1310 """
1311 module = import_site_module(path, module, modulefile=modulefile)
1312 if not module:
1313 return dummy
1314
1315 # special unique value to tell us if the symbol can't be imported
1316 cant_import = object()
1317
1318 obj = getattr(module, name, cant_import)
1319 if obj is cant_import:
1320 return dummy
1321
1322 return obj
1323
1324
1325def import_site_class(path, module, classname, baseclass, modulefile=None):
1326 """
1327 Try to import site specific class from site specific file if it exists
1328
1329 Args:
1330 path: full filename of the source file calling this (ie __file__)
1331 module: full module name
1332 classname: class name to be loaded from site file
1333 baseclass: base class object to return when no site file present or
1334 to mixin when site class exists but is not inherited from baseclass
1335 modulefile: module filename
1336
1337 Returns: baseclass if site specific class does not exist, the site specific
1338 class if it exists and is inherited from baseclass or a mixin of the
1339 site specific class and baseclass when the site specific class exists
1340 and is not inherited from baseclass
1341
1342 Raises: ImportError if the site file exists but imports fails
1343 """
1344
1345 res = import_site_symbol(path, module, classname, None, modulefile)
1346 if res:
1347 if not issubclass(res, baseclass):
1348 # if not a subclass of baseclass then mix in baseclass with the
1349 # site specific class object and return the result
1350 res = type(classname, (res, baseclass), {})
1351 else:
1352 res = baseclass
1353
1354 return res
1355
1356
1357def import_site_function(path, module, funcname, dummy, modulefile=None):
1358 """
1359 Try to import site specific function from site specific file if it exists
1360
1361 Args:
1362 path: full filename of the source file calling this (ie __file__)
1363 module: full module name
1364 funcname: function name to be imported from site file
1365 dummy: dummy function to return in case there is no function to import
1366 modulefile: module filename
1367
1368 Returns: site specific function object or dummy
1369
1370 Raises: ImportError if the site file exists but imports fails
1371 """
1372
1373 return import_site_symbol(path, module, funcname, dummy, modulefile)
1374
1375
1376def _get_pid_path(program_name):
1377 my_path = os.path.dirname(__file__)
1378 return os.path.abspath(os.path.join(my_path, "..", "..",
1379 "%s.pid" % program_name))
1380
1381
1382def write_pid(program_name):
1383 """
1384 Try to drop <program_name>.pid in the main autotest directory.
1385
1386 Args:
1387 program_name: prefix for file name
1388 """
1389 pidfile = open(_get_pid_path(program_name), "w")
1390 try:
1391 pidfile.write("%s\n" % os.getpid())
1392 finally:
1393 pidfile.close()
1394
1395
1396def delete_pid_file_if_exists(program_name):
1397 """
1398 Tries to remove <program_name>.pid from the main autotest directory.
1399 """
1400 pidfile_path = _get_pid_path(program_name)
1401
1402 try:
1403 os.remove(pidfile_path)
1404 except OSError:
1405 if not os.path.exists(pidfile_path):
1406 return
1407 raise
1408
1409
1410def get_pid_from_file(program_name):
1411 """
1412 Reads the pid from <program_name>.pid in the autotest directory.
1413
1414 @param program_name the name of the program
1415 @return the pid if the file exists, None otherwise.
1416 """
1417 pidfile_path = _get_pid_path(program_name)
1418 if not os.path.exists(pidfile_path):
1419 return None
1420
1421 pidfile = open(_get_pid_path(program_name), 'r')
1422
1423 try:
1424 try:
1425 pid = int(pidfile.readline())
1426 except IOError:
1427 if not os.path.exists(pidfile_path):
1428 return None
1429 raise
1430 finally:
1431 pidfile.close()
1432
1433 return pid
1434
1435
1436def get_process_name(pid):
1437 """
1438 Get process name from PID.
1439 @param pid: PID of process.
1440 @return: Process name if PID stat file exists or 'Dead PID' if it does not.
1441 """
1442 pid_stat_path = "/proc/%d/stat"
1443 if not os.path.exists(pid_stat_path % pid):
1444 return "Dead Pid"
1445 return get_field(read_file(pid_stat_path % pid), 1)[1:-1]
1446
1447
1448def program_is_alive(program_name):
1449 """
1450 Checks if the process is alive and not in Zombie state.
1451
1452 @param program_name the name of the program
1453 @return True if still alive, False otherwise
1454 """
1455 pid = get_pid_from_file(program_name)
1456 if pid is None:
1457 return False
1458 return pid_is_alive(pid)
1459
1460
1461def signal_program(program_name, sig=signal.SIGTERM):
1462 """
1463 Sends a signal to the process listed in <program_name>.pid
1464
1465 @param program_name the name of the program
1466 @param sig signal to send
1467 """
1468 pid = get_pid_from_file(program_name)
1469 if pid:
1470 signal_pid(pid, sig)
1471
1472
1473def get_relative_path(path, reference):
1474 """Given 2 absolute paths "path" and "reference", compute the path of
1475 "path" as relative to the directory "reference".
1476
1477 @param path the absolute path to convert to a relative path
1478 @param reference an absolute directory path to which the relative
1479 path will be computed
1480 """
1481 # normalize the paths (remove double slashes, etc)
1482 assert(os.path.isabs(path))
1483 assert(os.path.isabs(reference))
1484
1485 path = os.path.normpath(path)
1486 reference = os.path.normpath(reference)
1487
1488 # we could use os.path.split() but it splits from the end
1489 path_list = path.split(os.path.sep)[1:]
1490 ref_list = reference.split(os.path.sep)[1:]
1491
1492 # find the longest leading common path
1493 for i in xrange(min(len(path_list), len(ref_list))):
1494 if path_list[i] != ref_list[i]:
1495 # decrement i so when exiting this loop either by no match or by
1496 # end of range we are one step behind
1497 i -= 1
1498 break
1499 i += 1
1500 # drop the common part of the paths, not interested in that anymore
1501 del path_list[:i]
1502
1503 # for each uncommon component in the reference prepend a ".."
1504 path_list[:0] = ['..'] * (len(ref_list) - i)
1505
1506 return os.path.join(*path_list)
1507
1508
1509def sh_escape(command):
1510 """
1511 Escape special characters from a command so that it can be passed
1512 as a double quoted (" ") string in a (ba)sh command.
1513
1514 Args:
1515 command: the command string to escape.
1516
1517 Returns:
1518 The escaped command string. The required englobing double
1519 quotes are NOT added and so should be added at some point by
1520 the caller.
1521
1522 See also: http://www.tldp.org/LDP/abs/html/escapingsection.html
1523 """
1524 command = command.replace("\\", "\\\\")
1525 command = command.replace("$", r'\$')
1526 command = command.replace('"', r'\"')
1527 command = command.replace('`', r'\`')
1528 return command
1529
1530
1531def sh_quote_word(text, whitelist=SHELL_QUOTING_WHITELIST):
1532 r"""Quote a string to make it safe as a single word in a shell command.
1533
1534 POSIX shell syntax recognizes no escape characters inside a single-quoted
1535 string. So, single quotes can safely quote any string of characters except
1536 a string with a single quote character. A single quote character must be
1537 quoted with the sequence '\'' which translates to:
1538 ' -> close current quote
1539 \' -> insert a literal single quote
1540 ' -> reopen quoting again.
1541
1542 This is safe for all combinations of characters, including embedded and
1543 trailing backslashes in odd or even numbers.
1544
1545 This is also safe for nesting, e.g. the following is a valid use:
1546
1547 adb_command = 'adb shell %s' % (
1548 sh_quote_word('echo %s' % sh_quote_word('hello world')))
1549
1550 @param text: The string to be quoted into a single word for the shell.
1551 @param whitelist: Optional list of characters that do not need quoting.
1552 Defaults to a known good list of characters.
1553
1554 @return A string, possibly quoted, safe as a single word for a shell.
1555 """
1556 if all(c in whitelist for c in text):
1557 return text
1558 return "'" + text.replace("'", r"'\''") + "'"
1559
1560
1561def configure(extra=None, configure='./configure'):
1562 """
1563 Run configure passing in the correct host, build, and target options.
1564
1565 @param extra: extra command line arguments to pass to configure
1566 @param configure: which configure script to use
1567 """
1568 args = []
1569 if 'CHOST' in os.environ:
1570 args.append('--host=' + os.environ['CHOST'])
1571 if 'CBUILD' in os.environ:
1572 args.append('--build=' + os.environ['CBUILD'])
1573 if 'CTARGET' in os.environ:
1574 args.append('--target=' + os.environ['CTARGET'])
1575 if extra:
1576 args.append(extra)
1577
1578 system('%s %s' % (configure, ' '.join(args)))
1579
1580
1581def make(extra='', make='make', timeout=None, ignore_status=False):
1582 """
1583 Run make, adding MAKEOPTS to the list of options.
1584
1585 @param extra: extra command line arguments to pass to make.
1586 """
1587 cmd = '%s %s %s' % (make, os.environ.get('MAKEOPTS', ''), extra)
1588 return system(cmd, timeout=timeout, ignore_status=ignore_status)
1589
1590
1591def compare_versions(ver1, ver2):
1592 """Version number comparison between ver1 and ver2 strings.
1593
1594 >>> compare_tuple("1", "2")
1595 -1
1596 >>> compare_tuple("foo-1.1", "foo-1.2")
1597 -1
1598 >>> compare_tuple("1.2", "1.2a")
1599 -1
1600 >>> compare_tuple("1.2b", "1.2a")
1601 1
1602 >>> compare_tuple("1.3.5.3a", "1.3.5.3b")
1603 -1
1604
1605 Args:
1606 ver1: version string
1607 ver2: version string
1608
1609 Returns:
1610 int: 1 if ver1 > ver2
1611 0 if ver1 == ver2
1612 -1 if ver1 < ver2
1613 """
1614 ax = re.split('[.-]', ver1)
1615 ay = re.split('[.-]', ver2)
1616 while len(ax) > 0 and len(ay) > 0:
1617 cx = ax.pop(0)
1618 cy = ay.pop(0)
1619 maxlen = max(len(cx), len(cy))
1620 c = cmp(cx.zfill(maxlen), cy.zfill(maxlen))
1621 if c != 0:
1622 return c
1623 return cmp(len(ax), len(ay))
1624
1625
1626def args_to_dict(args):
1627 """Convert autoserv extra arguments in the form of key=val or key:val to a
1628 dictionary. Each argument key is converted to lowercase dictionary key.
1629
1630 Args:
1631 args - list of autoserv extra arguments.
1632
1633 Returns:
1634 dictionary
1635 """
1636 arg_re = re.compile(r'(\w+)[:=](.*)$')
1637 dict = {}
1638 for arg in args:
1639 match = arg_re.match(arg)
1640 if match:
1641 dict[match.group(1).lower()] = match.group(2)
1642 else:
1643 logging.warning("args_to_dict: argument '%s' doesn't match "
1644 "'%s' pattern. Ignored.", arg, arg_re.pattern)
1645 return dict
1646
1647
1648def get_unused_port():
1649 """
1650 Finds a semi-random available port. A race condition is still
1651 possible after the port number is returned, if another process
1652 happens to bind it.
1653
1654 Returns:
1655 A port number that is unused on both TCP and UDP.
1656 """
1657
1658 def try_bind(port, socket_type, socket_proto):
1659 s = socket.socket(socket.AF_INET, socket_type, socket_proto)
1660 try:
1661 try:
1662 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1663 s.bind(('', port))
1664 return s.getsockname()[1]
1665 except socket.error:
1666 return None
1667 finally:
1668 s.close()
1669
1670 # On the 2.6 kernel, calling try_bind() on UDP socket returns the
1671 # same port over and over. So always try TCP first.
1672 while True:
1673 # Ask the OS for an unused port.
1674 port = try_bind(0, socket.SOCK_STREAM, socket.IPPROTO_TCP)
1675 # Check if this port is unused on the other protocol.
1676 if port and try_bind(port, socket.SOCK_DGRAM, socket.IPPROTO_UDP):
1677 return port
1678
1679
1680def ask(question, auto=False):
1681 """
1682 Raw input with a prompt that emulates logging.
1683
1684 @param question: Question to be asked
1685 @param auto: Whether to return "y" instead of asking the question
1686 """
1687 if auto:
1688 logging.info("%s (y/n) y", question)
1689 return "y"
1690 return raw_input("%s INFO | %s (y/n) " %
1691 (time.strftime("%H:%M:%S", time.localtime()), question))
1692
1693
1694def rdmsr(address, cpu=0):
1695 """
1696 Reads an x86 MSR from the specified CPU, returns as long integer.
1697 """
1698 with open('/dev/cpu/%s/msr' % cpu, 'r', 0) as fd:
1699 fd.seek(address)
1700 return struct.unpack('=Q', fd.read(8))[0]
1701
1702
1703def wait_for_value(func,
1704 expected_value=None,
1705 min_threshold=None,
1706 max_threshold=None,
1707 timeout_sec=10):
1708 """
1709 Returns the value of func(). If |expected_value|, |min_threshold|, and
1710 |max_threshold| are not set, returns immediately.
1711
1712 If |expected_value| is set, polls the return value until |expected_value| is
1713 reached, and returns that value.
1714
1715 If either |max_threshold| or |min_threshold| is set, this function will
1716 will repeatedly call func() until the return value reaches or exceeds one of
1717 these thresholds.
1718
1719 Polling will stop after |timeout_sec| regardless of these thresholds.
1720
1721 @param func: function whose return value is to be waited on.
1722 @param expected_value: wait for func to return this value.
1723 @param min_threshold: wait for func value to reach or fall below this value.
1724 @param max_threshold: wait for func value to reach or rise above this value.
1725 @param timeout_sec: Number of seconds to wait before giving up and
1726 returning whatever value func() last returned.
1727
1728 Return value:
1729 The most recent return value of func().
1730 """
1731 value = None
1732 start_time_sec = time.time()
1733 while True:
1734 value = func()
1735 if (expected_value is None and \
1736 min_threshold is None and \
1737 max_threshold is None) or \
1738 (expected_value is not None and value == expected_value) or \
1739 (min_threshold is not None and value <= min_threshold) or \
1740 (max_threshold is not None and value >= max_threshold):
1741 break
1742
1743 if time.time() - start_time_sec >= timeout_sec:
1744 break
1745 time.sleep(0.1)
1746
1747 return value
1748
1749
1750def wait_for_value_changed(func,
1751 old_value=None,
1752 timeout_sec=10):
1753 """
1754 Returns the value of func().
1755
1756 The function polls the return value until it is different from |old_value|,
1757 and returns that value.
1758
1759 Polling will stop after |timeout_sec|.
1760
1761 @param func: function whose return value is to be waited on.
1762 @param old_value: wait for func to return a value different from this.
1763 @param timeout_sec: Number of seconds to wait before giving up and
1764 returning whatever value func() last returned.
1765
1766 @returns The most recent return value of func().
1767 """
1768 value = None
1769 start_time_sec = time.time()
1770 while True:
1771 value = func()
1772 if value != old_value:
1773 break
1774
1775 if time.time() - start_time_sec >= timeout_sec:
1776 break
1777 time.sleep(0.1)
1778
1779 return value
1780
1781
1782CONFIG = global_config.global_config
1783
1784# Keep checking if the pid is alive every second until the timeout (in seconds)
1785CHECK_PID_IS_ALIVE_TIMEOUT = 6
1786
1787_LOCAL_HOST_LIST = ('localhost', '127.0.0.1')
1788
1789# The default address of a vm gateway.
1790DEFAULT_VM_GATEWAY = '10.0.2.2'
1791
1792# Google Storage bucket URI to store results in.
1793DEFAULT_OFFLOAD_GSURI = CONFIG.get_config_value(
1794 'CROS', 'results_storage_server', default=None)
1795
1796# Default Moblab Ethernet Interface.
1797_MOBLAB_ETH_0 = 'eth0'
1798_MOBLAB_ETH_1 = 'eth1'
1799
1800# A list of subnets that requires dedicated devserver and drone in the same
1801# subnet. Each item is a tuple of (subnet_ip, mask_bits), e.g.,
1802# ('192.168.0.0', 24))
1803RESTRICTED_SUBNETS = []
1804
1805def _setup_restricted_subnets():
1806 restricted_subnets_list = CONFIG.get_config_value(
1807 'CROS', 'restricted_subnets', type=list, default=[])
1808 # TODO(dshi): Remove the code to split subnet with `:` after R51 is
1809 # off stable channel, and update shadow config to use `/` as
1810 # delimiter for consistency.
1811 for subnet in restricted_subnets_list:
1812 ip, mask_bits = subnet.split('/') if '/' in subnet \
1813 else subnet.split(':')
1814 RESTRICTED_SUBNETS.append((ip, int(mask_bits)))
1815
1816_setup_restricted_subnets()
1817
1818# regex pattern for CLIENT/wireless_ssid_ config. For example, global config
1819# can have following config in CLIENT section to indicate that hosts in subnet
1820# 192.168.0.1/24 should use wireless ssid of `ssid_1`
1821# wireless_ssid_192.168.0.1/24: ssid_1
1822WIRELESS_SSID_PATTERN = 'wireless_ssid_(.*)/(\d+)'
1823
1824
1825def get_built_in_ethernet_nic_name():
1826 """Gets the moblab public network interface.
1827
1828 If the eth0 is an USB interface, try to use eth1 instead. Otherwise
1829 use eth0 by default.
1830 """
1831 try:
1832 cmd_result = run('readlink -f /sys/class/net/eth0')
1833 if cmd_result.exit_status == 0 and 'usb' in cmd_result.stdout:
1834 cmd_result = run('readlink -f /sys/class/net/eth1')
1835 if cmd_result.exit_status == 0 and not ('usb' in cmd_result.stdout):
1836 logging.info('Eth0 is a USB dongle, use eth1 as moblab nic.')
1837 return _MOBLAB_ETH_1
1838 except error.CmdError:
1839 # readlink is not supported.
1840 logging.info('No readlink available, use eth0 as moblab nic.')
1841 pass
1842 return _MOBLAB_ETH_0
1843
1844
1845def ping(host, deadline=None, tries=None, timeout=60):
1846 """Attempt to ping |host|.
1847
1848 Shell out to 'ping' if host is an IPv4 addres or 'ping6' if host is an
1849 IPv6 address to try to reach |host| for |timeout| seconds.
1850 Returns exit code of ping.
1851
1852 Per 'man ping', if you specify BOTH |deadline| and |tries|, ping only
1853 returns 0 if we get responses to |tries| pings within |deadline| seconds.
1854
1855 Specifying |deadline| or |count| alone should return 0 as long as
1856 some packets receive responses.
1857
1858 Note that while this works with literal IPv6 addresses it will not work
1859 with hostnames that resolve to IPv6 only.
1860
1861 @param host: the host to ping.
1862 @param deadline: seconds within which |tries| pings must succeed.
1863 @param tries: number of pings to send.
1864 @param timeout: number of seconds after which to kill 'ping' command.
1865 @return exit code of ping command.
1866 """
1867 args = [host]
1868 ping_cmd = 'ping6' if re.search(r':.*:', host) else 'ping'
1869
1870 if deadline:
1871 args.append('-w%d' % deadline)
1872 if tries:
1873 args.append('-c%d' % tries)
1874
1875 return run(ping_cmd, args=args, verbose=True,
1876 ignore_status=True, timeout=timeout,
1877 stdout_tee=TEE_TO_LOGS,
1878 stderr_tee=TEE_TO_LOGS).exit_status
1879
1880
1881def host_is_in_lab_zone(hostname):
1882 """Check if the host is in the CLIENT.dns_zone.
1883
1884 @param hostname: The hostname to check.
1885 @returns True if hostname.dns_zone resolves, otherwise False.
1886 """
1887 host_parts = hostname.split('.')
1888 dns_zone = CONFIG.get_config_value('CLIENT', 'dns_zone', default=None)
1889 fqdn = '%s.%s' % (host_parts[0], dns_zone)
1890 try:
1891 socket.gethostbyname(fqdn)
1892 return True
1893 except socket.gaierror:
1894 return False
1895
1896
1897def host_could_be_in_afe(hostname):
1898 """Check if the host could be in Autotest Front End.
1899
1900 Report whether or not a host could be in AFE, without actually
1901 consulting AFE. This method exists because some systems are in the
1902 lab zone, but not actually managed by AFE.
1903
1904 @param hostname: The hostname to check.
1905 @returns True if hostname is in lab zone, and does not match *-dev-*
1906 """
1907 # Do the 'dev' check first, so that we skip DNS lookup if the
1908 # hostname matches. This should give us greater resilience to lab
1909 # failures.
1910 return (hostname.find('-dev-') == -1) and host_is_in_lab_zone(hostname)
1911
1912
1913def get_chrome_version(job_views):
1914 """
1915 Retrieves the version of the chrome binary associated with a job.
1916
1917 When a test runs we query the chrome binary for it's version and drop
1918 that value into a client keyval. To retrieve the chrome version we get all
1919 the views associated with a test from the db, including those of the
1920 server and client jobs, and parse the version out of the first test view
1921 that has it. If we never ran a single test in the suite the job_views
1922 dictionary will not contain a chrome version.
1923
1924 This method cannot retrieve the chrome version from a dictionary that
1925 does not conform to the structure of an autotest tko view.
1926
1927 @param job_views: a list of a job's result views, as returned by
1928 the get_detailed_test_views method in rpc_interface.
1929 @return: The chrome version string, or None if one can't be found.
1930 """
1931
1932 # Aborted jobs have no views.
1933 if not job_views:
1934 return None
1935
1936 for view in job_views:
1937 if (view.get('attributes')
1938 and constants.CHROME_VERSION in view['attributes'].keys()):
1939
1940 return view['attributes'].get(constants.CHROME_VERSION)
1941
1942 logging.warning('Could not find chrome version for failure.')
1943 return None
1944
1945
1946def get_default_interface_mac_address():
1947 """Returns the default moblab MAC address."""
1948 return get_interface_mac_address(
1949 get_built_in_ethernet_nic_name())
1950
1951
1952def get_interface_mac_address(interface):
1953 """Return the MAC address of a given interface.
1954
1955 @param interface: Interface to look up the MAC address of.
1956 """
1957 interface_link = run(
1958 'ip addr show %s | grep link/ether' % interface).stdout
1959 # The output will be in the format of:
1960 # 'link/ether <mac> brd ff:ff:ff:ff:ff:ff'
1961 return interface_link.split()[1]
1962
1963
1964def get_moblab_id():
1965 """Gets the moblab random id.
1966
1967 The random id file is cached on disk. If it does not exist, a new file is
1968 created the first time.
1969
1970 @returns the moblab random id.
1971 """
1972 moblab_id_filepath = '/home/moblab/.moblab_id'
Keith Haddowfa0412a2017-05-18 09:17:22 -07001973 try:
1974 if os.path.exists(moblab_id_filepath):
1975 with open(moblab_id_filepath, 'r') as moblab_id_file:
1976 random_id = moblab_id_file.read()
1977 else:
1978 random_id = uuid.uuid1().hex
1979 with open(moblab_id_filepath, 'w') as moblab_id_file:
1980 moblab_id_file.write('%s' % random_id)
1981 except IOError as e:
1982 # Possible race condition, another process has created the file.
1983 # Sleep a second to make sure the file gets closed.
1984 logging.info(e)
1985 time.sleep(1)
Allen Li5ed7e632017-02-03 16:31:33 -08001986 with open(moblab_id_filepath, 'r') as moblab_id_file:
1987 random_id = moblab_id_file.read()
Allen Li5ed7e632017-02-03 16:31:33 -08001988 return random_id
1989
1990
1991def get_offload_gsuri():
1992 """Return the GSURI to offload test results to.
1993
1994 For the normal use case this is the results_storage_server in the
1995 global_config.
1996
1997 However partners using Moblab will be offloading their results to a
1998 subdirectory of their image storage buckets. The subdirectory is
1999 determined by the MAC Address of the Moblab device.
2000
2001 @returns gsuri to offload test results to.
2002 """
2003 # For non-moblab, use results_storage_server or default.
2004 if not is_moblab():
2005 return DEFAULT_OFFLOAD_GSURI
2006
2007 # For moblab, use results_storage_server or image_storage_server as bucket
2008 # name and mac-address/moblab_id as path.
2009 gsuri = DEFAULT_OFFLOAD_GSURI
2010 if not gsuri:
2011 gsuri = "%sresults/" % CONFIG.get_config_value('CROS', 'image_storage_server')
2012
2013 return '%s%s/%s/' % (
2014 gsuri, get_interface_mac_address(get_built_in_ethernet_nic_name()),
2015 get_moblab_id())
2016
2017
2018# TODO(petermayo): crosbug.com/31826 Share this with _GsUpload in
2019# //chromite.git/buildbot/prebuilt.py somewhere/somehow
2020def gs_upload(local_file, remote_file, acl, result_dir=None,
2021 transfer_timeout=300, acl_timeout=300):
2022 """Upload to GS bucket.
2023
2024 @param local_file: Local file to upload
2025 @param remote_file: Remote location to upload the local_file to.
2026 @param acl: name or file used for controlling access to the uploaded
2027 file.
2028 @param result_dir: Result directory if you want to add tracing to the
2029 upload.
2030 @param transfer_timeout: Timeout for this upload call.
2031 @param acl_timeout: Timeout for the acl call needed to confirm that
2032 the uploader has permissions to execute the upload.
2033
2034 @raise CmdError: the exit code of the gsutil call was not 0.
2035
2036 @returns True/False - depending on if the upload succeeded or failed.
2037 """
2038 # https://developers.google.com/storage/docs/accesscontrol#extension
2039 CANNED_ACLS = ['project-private', 'private', 'public-read',
2040 'public-read-write', 'authenticated-read',
2041 'bucket-owner-read', 'bucket-owner-full-control']
2042 _GSUTIL_BIN = 'gsutil'
2043 acl_cmd = None
2044 if acl in CANNED_ACLS:
2045 cmd = '%s cp -a %s %s %s' % (_GSUTIL_BIN, acl, local_file, remote_file)
2046 else:
2047 # For private uploads we assume that the overlay board is set up
2048 # properly and a googlestore_acl.xml is present, if not this script
2049 # errors
2050 cmd = '%s cp -a private %s %s' % (_GSUTIL_BIN, local_file, remote_file)
2051 if not os.path.exists(acl):
2052 logging.error('Unable to find ACL File %s.', acl)
2053 return False
2054 acl_cmd = '%s setacl %s %s' % (_GSUTIL_BIN, acl, remote_file)
2055 if not result_dir:
2056 run(cmd, timeout=transfer_timeout, verbose=True)
2057 if acl_cmd:
2058 run(acl_cmd, timeout=acl_timeout, verbose=True)
2059 return True
2060 with open(os.path.join(result_dir, 'tracing'), 'w') as ftrace:
2061 ftrace.write('Preamble\n')
2062 run(cmd, timeout=transfer_timeout, verbose=True,
2063 stdout_tee=ftrace, stderr_tee=ftrace)
2064 if acl_cmd:
2065 ftrace.write('\nACL setting\n')
2066 # Apply the passed in ACL xml file to the uploaded object.
2067 run(acl_cmd, timeout=acl_timeout, verbose=True,
2068 stdout_tee=ftrace, stderr_tee=ftrace)
2069 ftrace.write('Postamble\n')
2070 return True
2071
2072
2073def gs_ls(uri_pattern):
2074 """Returns a list of URIs that match a given pattern.
2075
2076 @param uri_pattern: a GS URI pattern, may contain wildcards
2077
2078 @return A list of URIs matching the given pattern.
2079
2080 @raise CmdError: the gsutil command failed.
2081
2082 """
2083 gs_cmd = ' '.join(['gsutil', 'ls', uri_pattern])
2084 result = system_output(gs_cmd).splitlines()
2085 return [path.rstrip() for path in result if path]
2086
2087
2088def nuke_pids(pid_list, signal_queue=[signal.SIGTERM, signal.SIGKILL]):
2089 """
2090 Given a list of pid's, kill them via an esclating series of signals.
2091
2092 @param pid_list: List of PID's to kill.
2093 @param signal_queue: Queue of signals to send the PID's to terminate them.
2094
2095 @return: A mapping of the signal name to the number of processes it
2096 was sent to.
2097 """
2098 sig_count = {}
2099 # Though this is slightly hacky it beats hardcoding names anyday.
2100 sig_names = dict((k, v) for v, k in signal.__dict__.iteritems()
2101 if v.startswith('SIG'))
2102 for sig in signal_queue:
2103 logging.debug('Sending signal %s to the following pids:', sig)
2104 sig_count[sig_names.get(sig, 'unknown_signal')] = len(pid_list)
2105 for pid in pid_list:
2106 logging.debug('Pid %d', pid)
2107 try:
2108 os.kill(pid, sig)
2109 except OSError:
2110 # The process may have died from a previous signal before we
2111 # could kill it.
2112 pass
2113 if sig == signal.SIGKILL:
2114 return sig_count
2115 pid_list = [pid for pid in pid_list if pid_is_alive(pid)]
2116 if not pid_list:
2117 break
2118 time.sleep(CHECK_PID_IS_ALIVE_TIMEOUT)
2119 failed_list = []
2120 for pid in pid_list:
2121 if pid_is_alive(pid):
2122 failed_list.append('Could not kill %d for process name: %s.' % pid,
2123 get_process_name(pid))
2124 if failed_list:
2125 raise error.AutoservRunError('Following errors occured: %s' %
2126 failed_list, None)
2127 return sig_count
2128
2129
2130def externalize_host(host):
2131 """Returns an externally accessible host name.
2132
2133 @param host: a host name or address (string)
2134
2135 @return An externally visible host name or address
2136
2137 """
2138 return socket.gethostname() if host in _LOCAL_HOST_LIST else host
2139
2140
2141def urlopen_socket_timeout(url, data=None, timeout=5):
2142 """
2143 Wrapper to urllib2.urlopen with a socket timeout.
2144
2145 This method will convert all socket timeouts to
2146 TimeoutExceptions, so we can use it in conjunction
2147 with the rpc retry decorator and continue to handle
2148 other URLErrors as we see fit.
2149
2150 @param url: The url to open.
2151 @param data: The data to send to the url (eg: the urlencoded dictionary
2152 used with a POST call).
2153 @param timeout: The timeout for this urlopen call.
2154
2155 @return: The response of the urlopen call.
2156
2157 @raises: error.TimeoutException when a socket timeout occurs.
2158 urllib2.URLError for errors that not caused by timeout.
2159 urllib2.HTTPError for errors like 404 url not found.
2160 """
2161 old_timeout = socket.getdefaulttimeout()
2162 socket.setdefaulttimeout(timeout)
2163 try:
2164 return urllib2.urlopen(url, data=data)
2165 except urllib2.URLError as e:
2166 if type(e.reason) is socket.timeout:
2167 raise error.TimeoutException(str(e))
2168 raise
2169 finally:
2170 socket.setdefaulttimeout(old_timeout)
2171
2172
2173def parse_chrome_version(version_string):
2174 """
2175 Parse a chrome version string and return version and milestone.
2176
2177 Given a chrome version of the form "W.X.Y.Z", return "W.X.Y.Z" as
2178 the version and "W" as the milestone.
2179
2180 @param version_string: Chrome version string.
2181 @return: a tuple (chrome_version, milestone). If the incoming version
2182 string is not of the form "W.X.Y.Z", chrome_version will
2183 be set to the incoming "version_string" argument and the
2184 milestone will be set to the empty string.
2185 """
2186 match = re.search('(\d+)\.\d+\.\d+\.\d+', version_string)
2187 ver = match.group(0) if match else version_string
2188 milestone = match.group(1) if match else ''
2189 return ver, milestone
2190
2191
2192def is_localhost(server):
2193 """Check if server is equivalent to localhost.
2194
2195 @param server: Name of the server to check.
2196
2197 @return: True if given server is equivalent to localhost.
2198
2199 @raise socket.gaierror: If server name failed to be resolved.
2200 """
2201 if server in _LOCAL_HOST_LIST:
2202 return True
2203 try:
2204 return (socket.gethostbyname(socket.gethostname()) ==
2205 socket.gethostbyname(server))
2206 except socket.gaierror:
2207 logging.error('Failed to resolve server name %s.', server)
2208 return False
2209
2210
2211def is_puppylab_vm(server):
2212 """Check if server is a virtual machine in puppylab.
2213
2214 In the virtual machine testing environment (i.e., puppylab), each
2215 shard VM has a hostname like localhost:<port>.
2216
2217 @param server: Server name to check.
2218
2219 @return True if given server is a virtual machine in puppylab.
2220
2221 """
2222 # TODO(mkryu): This is a puppylab specific hack. Please update
2223 # this method if you have a better solution.
2224 regex = re.compile(r'(.+):\d+')
2225 m = regex.match(server)
2226 if m:
2227 return m.group(1) in _LOCAL_HOST_LIST
2228 return False
2229
2230
2231def get_function_arg_value(func, arg_name, args, kwargs):
2232 """Get the value of the given argument for the function.
2233
2234 @param func: Function being called with given arguments.
2235 @param arg_name: Name of the argument to look for value.
2236 @param args: arguments for function to be called.
2237 @param kwargs: keyword arguments for function to be called.
2238
2239 @return: The value of the given argument for the function.
2240
2241 @raise ValueError: If the argument is not listed function arguemnts.
2242 @raise KeyError: If no value is found for the given argument.
2243 """
2244 if arg_name in kwargs:
2245 return kwargs[arg_name]
2246
2247 argspec = inspect.getargspec(func)
2248 index = argspec.args.index(arg_name)
2249 try:
2250 return args[index]
2251 except IndexError:
2252 try:
2253 # The argument can use a default value. Reverse the default value
2254 # so argument with default value can be counted from the last to
2255 # the first.
2256 return argspec.defaults[::-1][len(argspec.args) - index - 1]
2257 except IndexError:
2258 raise KeyError('Argument %s is not given a value. argspec: %s, '
2259 'args:%s, kwargs:%s' %
2260 (arg_name, argspec, args, kwargs))
2261
2262
2263def has_systemd():
2264 """Check if the host is running systemd.
2265
2266 @return: True if the host uses systemd, otherwise returns False.
2267 """
2268 return os.path.basename(os.readlink('/proc/1/exe')) == 'systemd'
2269
2270
2271def version_match(build_version, release_version, update_url=''):
2272 """Compare release versino from lsb-release with cros-version label.
2273
2274 build_version is a string based on build name. It is prefixed with builder
2275 info and branch ID, e.g., lumpy-release/R43-6809.0.0. It may not include
2276 builder info, e.g., lumpy-release, in which case, update_url shall be passed
2277 in to determine if the build is a trybot or pgo-generate build.
2278 release_version is retrieved from lsb-release.
2279 These two values might not match exactly.
2280
2281 The method is designed to compare version for following 6 scenarios with
2282 samples of build version and expected release version:
2283 1. trybot non-release build (paladin, pre-cq or test-ap build).
2284 build version: trybot-lumpy-paladin/R27-3837.0.0-b123
2285 release version: 3837.0.2013_03_21_1340
2286
2287 2. trybot release build.
2288 build version: trybot-lumpy-release/R27-3837.0.0-b456
2289 release version: 3837.0.0
2290
2291 3. buildbot official release build.
2292 build version: lumpy-release/R27-3837.0.0
2293 release version: 3837.0.0
2294
2295 4. non-official paladin rc build.
2296 build version: lumpy-paladin/R27-3878.0.0-rc7
2297 release version: 3837.0.0-rc7
2298
2299 5. chrome-perf build.
2300 build version: lumpy-chrome-perf/R28-3837.0.0-b2996
2301 release version: 3837.0.0
2302
2303 6. pgo-generate build.
2304 build version: lumpy-release-pgo-generate/R28-3837.0.0-b2996
2305 release version: 3837.0.0-pgo-generate
2306
2307 TODO: This logic has a bug if a trybot paladin build failed to be
2308 installed in a DUT running an older trybot paladin build with same
2309 platform number, but different build number (-b###). So to conclusively
2310 determine if a tryjob paladin build is imaged successfully, we may need
2311 to find out the date string from update url.
2312
2313 @param build_version: Build name for cros version, e.g.
2314 peppy-release/R43-6809.0.0 or R43-6809.0.0
2315 @param release_version: Release version retrieved from lsb-release,
2316 e.g., 6809.0.0
2317 @param update_url: Update url which include the full builder information.
2318 Default is set to empty string.
2319
2320 @return: True if the values match, otherwise returns False.
2321 """
2322 # If the build is from release, CQ or PFQ builder, cros-version label must
2323 # be ended with release version in lsb-release.
2324 if build_version.endswith(release_version):
2325 return True
2326
2327 # Remove R#- and -b# at the end of build version
2328 stripped_version = re.sub(r'(R\d+-|-b\d+)', '', build_version)
2329 # Trim the builder info, e.g., trybot-lumpy-paladin/
2330 stripped_version = stripped_version.split('/')[-1]
2331
2332 is_trybot_non_release_build = (
2333 re.match(r'.*trybot-.+-(paladin|pre-cq|test-ap|toolchain)',
2334 build_version) or
2335 re.match(r'.*trybot-.+-(paladin|pre-cq|test-ap|toolchain)',
2336 update_url))
2337
2338 # Replace date string with 0 in release_version
2339 release_version_no_date = re.sub(r'\d{4}_\d{2}_\d{2}_\d+', '0',
2340 release_version)
2341 has_date_string = release_version != release_version_no_date
2342
2343 is_pgo_generate_build = (
2344 re.match(r'.+-pgo-generate', build_version) or
2345 re.match(r'.+-pgo-generate', update_url))
2346
2347 # Remove |-pgo-generate| in release_version
2348 release_version_no_pgo = release_version.replace('-pgo-generate', '')
2349 has_pgo_generate = release_version != release_version_no_pgo
2350
2351 if is_trybot_non_release_build:
2352 if not has_date_string:
2353 logging.error('A trybot paladin or pre-cq build is expected. '
2354 'Version "%s" is not a paladin or pre-cq build.',
2355 release_version)
2356 return False
2357 return stripped_version == release_version_no_date
2358 elif is_pgo_generate_build:
2359 if not has_pgo_generate:
2360 logging.error('A pgo-generate build is expected. Version '
2361 '"%s" is not a pgo-generate build.',
2362 release_version)
2363 return False
2364 return stripped_version == release_version_no_pgo
2365 else:
2366 if has_date_string:
2367 logging.error('Unexpected date found in a non trybot paladin or '
2368 'pre-cq build.')
2369 return False
2370 # Versioned build, i.e., rc or release build.
2371 return stripped_version == release_version
2372
2373
2374def get_real_user():
2375 """Get the real user that runs the script.
2376
2377 The function check environment variable SUDO_USER for the user if the
2378 script is run with sudo. Otherwise, it returns the value of environment
2379 variable USER.
2380
2381 @return: The user name that runs the script.
2382
2383 """
2384 user = os.environ.get('SUDO_USER')
2385 if not user:
2386 user = os.environ.get('USER')
2387 return user
2388
2389
2390def get_service_pid(service_name):
2391 """Return pid of service.
2392
2393 @param service_name: string name of service.
2394
2395 @return: pid or 0 if service is not running.
2396 """
2397 if has_systemd():
2398 # systemctl show prints 'MainPID=0' if the service is not running.
2399 cmd_result = run('systemctl show -p MainPID %s' %
2400 service_name, ignore_status=True)
2401 return int(cmd_result.stdout.split('=')[1])
2402 else:
2403 cmd_result = run('status %s' % service_name,
2404 ignore_status=True)
2405 if 'start/running' in cmd_result.stdout:
2406 return int(cmd_result.stdout.split()[3])
2407 return 0
2408
2409
2410def control_service(service_name, action='start', ignore_status=True):
2411 """Controls a service. It can be used to start, stop or restart
2412 a service.
2413
2414 @param service_name: string service to be restarted.
2415
2416 @param action: string choice of action to control command.
2417
2418 @param ignore_status: boolean ignore if system command fails.
2419
2420 @return: status code of the executed command.
2421 """
2422 if action not in ('start', 'stop', 'restart'):
2423 raise ValueError('Unknown action supplied as parameter.')
2424
2425 control_cmd = action + ' ' + service_name
2426 if has_systemd():
2427 control_cmd = 'systemctl ' + control_cmd
2428 return system(control_cmd, ignore_status=ignore_status)
2429
2430
2431def restart_service(service_name, ignore_status=True):
2432 """Restarts a service
2433
2434 @param service_name: string service to be restarted.
2435
2436 @param ignore_status: boolean ignore if system command fails.
2437
2438 @return: status code of the executed command.
2439 """
2440 return control_service(service_name, action='restart', ignore_status=ignore_status)
2441
2442
2443def start_service(service_name, ignore_status=True):
2444 """Starts a service
2445
2446 @param service_name: string service to be started.
2447
2448 @param ignore_status: boolean ignore if system command fails.
2449
2450 @return: status code of the executed command.
2451 """
2452 return control_service(service_name, action='start', ignore_status=ignore_status)
2453
2454
2455def stop_service(service_name, ignore_status=True):
2456 """Stops a service
2457
2458 @param service_name: string service to be stopped.
2459
2460 @param ignore_status: boolean ignore if system command fails.
2461
2462 @return: status code of the executed command.
2463 """
2464 return control_service(service_name, action='stop', ignore_status=ignore_status)
2465
2466
2467def sudo_require_password():
2468 """Test if the process can run sudo command without using password.
2469
2470 @return: True if the process needs password to run sudo command.
2471
2472 """
2473 try:
2474 run('sudo -n true')
2475 return False
2476 except error.CmdError:
2477 logging.warn('sudo command requires password.')
2478 return True
2479
2480
2481def is_in_container():
2482 """Check if the process is running inside a container.
2483
2484 @return: True if the process is running inside a container, otherwise False.
2485 """
2486 result = run('grep -q "/lxc/" /proc/1/cgroup',
2487 verbose=False, ignore_status=True)
Dan Shi30a90de2017-04-24 12:46:19 -07002488 if result.exit_status == 0:
2489 return True
2490
2491 # Check "container" environment variable for lxd/lxc containers.
Dan Shi8e312ec2017-04-24 14:23:36 -07002492 if os.environ.get('container') == 'lxc':
Dan Shi30a90de2017-04-24 12:46:19 -07002493 return True
2494
2495 return False
Allen Li5ed7e632017-02-03 16:31:33 -08002496
2497
2498def is_flash_installed():
2499 """
2500 The Adobe Flash binary is only distributed with internal builds.
2501 """
2502 return (os.path.exists('/opt/google/chrome/pepper/libpepflashplayer.so')
2503 and os.path.exists('/opt/google/chrome/pepper/pepper-flash.info'))
2504
2505
2506def verify_flash_installed():
2507 """
2508 The Adobe Flash binary is only distributed with internal builds.
2509 Warn users of public builds of the extra dependency.
2510 """
2511 if not is_flash_installed():
2512 raise error.TestNAError('No Adobe Flash binary installed.')
2513
2514
2515def is_in_same_subnet(ip_1, ip_2, mask_bits=24):
2516 """Check if two IP addresses are in the same subnet with given mask bits.
2517
2518 The two IP addresses are string of IPv4, e.g., '192.168.0.3'.
2519
2520 @param ip_1: First IP address to compare.
2521 @param ip_2: Second IP address to compare.
2522 @param mask_bits: Number of mask bits for subnet comparison. Default to 24.
2523
2524 @return: True if the two IP addresses are in the same subnet.
2525
2526 """
2527 mask = ((2L<<mask_bits-1) -1)<<(32-mask_bits)
2528 ip_1_num = struct.unpack('!I', socket.inet_aton(ip_1))[0]
2529 ip_2_num = struct.unpack('!I', socket.inet_aton(ip_2))[0]
2530 return ip_1_num & mask == ip_2_num & mask
2531
2532
2533def get_ip_address(hostname):
2534 """Get the IP address of given hostname.
2535
2536 @param hostname: Hostname of a DUT.
2537
2538 @return: The IP address of given hostname. None if failed to resolve
2539 hostname.
2540 """
2541 try:
2542 if hostname:
2543 return socket.gethostbyname(hostname)
2544 except socket.gaierror as e:
2545 logging.error('Failed to get IP address of %s, error: %s.', hostname, e)
2546
2547
2548def get_servers_in_same_subnet(host_ip, mask_bits, servers=None,
2549 server_ip_map=None):
2550 """Get the servers in the same subnet of the given host ip.
2551
2552 @param host_ip: The IP address of a dut to look for devserver.
2553 @param mask_bits: Number of mask bits.
2554 @param servers: A list of servers to be filtered by subnet specified by
2555 host_ip and mask_bits.
2556 @param server_ip_map: A map between the server name and its IP address.
2557 The map can be pre-built for better performance, e.g., when
2558 allocating a drone for an agent task.
2559
2560 @return: A list of servers in the same subnet of the given host ip.
2561
2562 """
2563 matched_servers = []
2564 if not servers and not server_ip_map:
2565 raise ValueError('Either `servers` or `server_ip_map` must be given.')
2566 if not servers:
2567 servers = server_ip_map.keys()
2568 # Make sure server_ip_map is an empty dict if it's not set.
2569 if not server_ip_map:
2570 server_ip_map = {}
2571 for server in servers:
2572 server_ip = server_ip_map.get(server, get_ip_address(server))
2573 if server_ip and is_in_same_subnet(server_ip, host_ip, mask_bits):
2574 matched_servers.append(server)
2575 return matched_servers
2576
2577
2578def get_restricted_subnet(hostname, restricted_subnets=RESTRICTED_SUBNETS):
2579 """Get the restricted subnet of given hostname.
2580
2581 @param hostname: Name of the host to look for matched restricted subnet.
2582 @param restricted_subnets: A list of restricted subnets, default is set to
2583 RESTRICTED_SUBNETS.
2584
2585 @return: A tuple of (subnet_ip, mask_bits), which defines a restricted
2586 subnet.
2587 """
2588 host_ip = get_ip_address(hostname)
2589 if not host_ip:
2590 return
2591 for subnet_ip, mask_bits in restricted_subnets:
2592 if is_in_same_subnet(subnet_ip, host_ip, mask_bits):
2593 return subnet_ip, mask_bits
2594
2595
2596def get_wireless_ssid(hostname):
2597 """Get the wireless ssid based on given hostname.
2598
2599 The method tries to locate the wireless ssid in the same subnet of given
2600 hostname first. If none is found, it returns the default setting in
2601 CLIENT/wireless_ssid.
2602
2603 @param hostname: Hostname of the test device.
2604
2605 @return: wireless ssid for the test device.
2606 """
2607 default_ssid = CONFIG.get_config_value('CLIENT', 'wireless_ssid',
2608 default=None)
2609 host_ip = get_ip_address(hostname)
2610 if not host_ip:
2611 return default_ssid
2612
2613 # Get all wireless ssid in the global config.
2614 ssids = CONFIG.get_config_value_regex('CLIENT', WIRELESS_SSID_PATTERN)
2615
2616 # There could be multiple subnet matches, pick the one with most strict
2617 # match, i.e., the one with highest maskbit.
2618 matched_ssid = default_ssid
2619 matched_maskbit = -1
2620 for key, value in ssids.items():
2621 # The config key filtered by regex WIRELESS_SSID_PATTERN has a format of
2622 # wireless_ssid_[subnet_ip]/[maskbit], for example:
2623 # wireless_ssid_192.168.0.1/24
2624 # Following line extract the subnet ip and mask bit from the key name.
2625 match = re.match(WIRELESS_SSID_PATTERN, key)
2626 subnet_ip, maskbit = match.groups()
2627 maskbit = int(maskbit)
2628 if (is_in_same_subnet(subnet_ip, host_ip, maskbit) and
2629 maskbit > matched_maskbit):
2630 matched_ssid = value
2631 matched_maskbit = maskbit
2632 return matched_ssid
2633
2634
2635def parse_launch_control_build(build_name):
2636 """Get branch, target, build_id from the given Launch Control build_name.
2637
2638 @param build_name: Name of a Launch Control build, should be formated as
2639 branch/target/build_id
2640
2641 @return: Tuple of branch, target, build_id
2642 @raise ValueError: If the build_name is not correctly formated.
2643 """
2644 branch, target, build_id = build_name.split('/')
2645 return branch, target, build_id
2646
2647
2648def parse_android_target(target):
2649 """Get board and build type from the given target.
2650
2651 @param target: Name of an Android build target, e.g., shamu-eng.
2652
2653 @return: Tuple of board, build_type
2654 @raise ValueError: If the target is not correctly formated.
2655 """
2656 board, build_type = target.split('-')
2657 return board, build_type
2658
2659
2660def parse_launch_control_target(target):
2661 """Parse the build target and type from a Launch Control target.
2662
2663 The Launch Control target has the format of build_target-build_type, e.g.,
2664 shamu-eng or dragonboard-userdebug. This method extracts the build target
2665 and type from the target name.
2666
2667 @param target: Name of a Launch Control target, e.g., shamu-eng.
2668
2669 @return: (build_target, build_type), e.g., ('shamu', 'userdebug')
2670 """
2671 match = re.match('(?P<build_target>.+)-(?P<build_type>[^-]+)', target)
2672 if match:
2673 return match.group('build_target'), match.group('build_type')
2674 else:
2675 return None, None
2676
2677
2678def is_launch_control_build(build):
2679 """Check if a given build is a Launch Control build.
2680
2681 @param build: Name of a build, e.g.,
2682 ChromeOS build: daisy-release/R50-1234.0.0
2683 Launch Control build: git_mnc_release/shamu-eng
2684
2685 @return: True if the build name matches the pattern of a Launch Control
2686 build, False otherwise.
2687 """
2688 try:
2689 _, target, _ = parse_launch_control_build(build)
2690 build_target, _ = parse_launch_control_target(target)
2691 if build_target:
2692 return True
2693 except ValueError:
2694 # parse_launch_control_build or parse_launch_control_target failed.
2695 pass
2696 return False
2697
2698
2699def which(exec_file):
2700 """Finds an executable file.
2701
2702 If the file name contains a path component, it is checked as-is.
2703 Otherwise, we check with each of the path components found in the system
2704 PATH prepended. This behavior is similar to the 'which' command-line tool.
2705
2706 @param exec_file: Name or path to desired executable.
2707
2708 @return: An actual path to the executable, or None if not found.
2709 """
2710 if os.path.dirname(exec_file):
2711 return exec_file if os.access(exec_file, os.X_OK) else None
2712 sys_path = os.environ.get('PATH')
2713 prefix_list = sys_path.split(os.pathsep) if sys_path else []
2714 for prefix in prefix_list:
2715 path = os.path.join(prefix, exec_file)
2716 if os.access(path, os.X_OK):
2717 return path
2718
2719
2720class TimeoutError(error.TestError):
2721 """Error raised when we time out when waiting on a condition."""
2722 pass
2723
2724
2725def poll_for_condition(condition,
2726 exception=None,
2727 timeout=10,
2728 sleep_interval=0.1,
2729 desc=None):
2730 """Polls until a condition becomes true.
2731
2732 @param condition: function taking no args and returning bool
2733 @param exception: exception to throw if condition doesn't become true
2734 @param timeout: maximum number of seconds to wait
2735 @param sleep_interval: time to sleep between polls
2736 @param desc: description of default TimeoutError used if 'exception' is
2737 None
2738
2739 @return The true value that caused the poll loop to terminate.
2740
2741 @raise 'exception' arg if supplied; TimeoutError otherwise
2742 """
2743 start_time = time.time()
2744 while True:
2745 value = condition()
2746 if value:
2747 return value
2748 if time.time() + sleep_interval - start_time > timeout:
2749 if exception:
2750 logging.error(exception)
2751 raise exception
2752
2753 if desc:
2754 desc = 'Timed out waiting for condition: ' + desc
2755 else:
2756 desc = 'Timed out waiting for unnamed condition'
2757 logging.error(desc)
2758 raise TimeoutError(desc)
2759
2760 time.sleep(sleep_interval)
2761
2762
2763class metrics_mock(stats_es_mock.mock_class_base):
2764 """mock class for metrics in case chromite is not installed."""
2765 pass