blob: 4eb17fa2317a170fc4dc090c6859549a88064eec [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
Luis Hector Chavez5473ee32018-05-15 10:12:50 -070015import collections
Zachary Marcus4e4cd7b2018-05-24 15:07:16 -070016import datetime
Allen Li5ed7e632017-02-03 16:31:33 -080017import errno
18import inspect
19import itertools
20import logging
21import os
22import pickle
Zachary Marcus4e4cd7b2018-05-24 15:07:16 -070023import Queue
Allen Li5ed7e632017-02-03 16:31:33 -080024import random
25import re
26import resource
27import select
28import shutil
29import signal
30import socket
31import string
32import struct
33import subprocess
34import textwrap
Zachary Marcus4e4cd7b2018-05-24 15:07:16 -070035import threading
Allen Li5ed7e632017-02-03 16:31:33 -080036import time
37import urllib2
38import urlparse
39import uuid
40import warnings
41
42try:
43 import hashlib
44except ImportError:
45 import md5
46 import sha
47
Shuqian Zhaoae2d0782016-11-15 16:58:47 -080048import common
Allen Li5ed7e632017-02-03 16:31:33 -080049
Aviv Keshet12aa4a22017-05-18 16:48:05 -070050from autotest_lib.client.common_lib import env
Allen Li5ed7e632017-02-03 16:31:33 -080051from autotest_lib.client.common_lib import error
52from autotest_lib.client.common_lib import global_config
53from autotest_lib.client.common_lib import logging_manager
Aviv Keshet98b179e2017-07-18 16:22:23 -070054from autotest_lib.client.common_lib import metrics_mock_class
Allen Li5ed7e632017-02-03 16:31:33 -080055from autotest_lib.client.cros import constants
56
Kirtika Ruchandani42eabe62018-08-25 12:57:15 -070057# pylint: disable=wildcard-import
Dan Shi60cf6a92015-01-29 17:22:49 -080058from autotest_lib.client.common_lib.lsbrelease_utils import *
Allen Li5ed7e632017-02-03 16:31:33 -080059
60
61def deprecated(func):
62 """This is a decorator which can be used to mark functions as deprecated.
63 It will result in a warning being emmitted when the function is used."""
64 def new_func(*args, **dargs):
65 warnings.warn("Call to deprecated function %s." % func.__name__,
66 category=DeprecationWarning)
67 return func(*args, **dargs)
68 new_func.__name__ = func.__name__
69 new_func.__doc__ = func.__doc__
70 new_func.__dict__.update(func.__dict__)
71 return new_func
72
73
74class _NullStream(object):
75 def write(self, data):
76 pass
77
78
79 def flush(self):
80 pass
81
82
83TEE_TO_LOGS = object()
84_the_null_stream = _NullStream()
85
Hidehiko Abed939d662017-06-13 20:25:03 +090086DEVNULL = object()
87
Allen Li5ed7e632017-02-03 16:31:33 -080088DEFAULT_STDOUT_LEVEL = logging.DEBUG
89DEFAULT_STDERR_LEVEL = logging.ERROR
90
91# prefixes for logging stdout/stderr of commands
92STDOUT_PREFIX = '[stdout] '
93STDERR_PREFIX = '[stderr] '
94
95# safe characters for the shell (do not need quoting)
96SHELL_QUOTING_WHITELIST = frozenset(string.ascii_letters +
97 string.digits +
98 '_-+=')
99
100def custom_warning_handler(message, category, filename, lineno, file=None,
101 line=None):
102 """Custom handler to log at the WARNING error level. Ignores |file|."""
103 logging.warning(warnings.formatwarning(message, category, filename, lineno,
104 line))
105
106warnings.showwarning = custom_warning_handler
107
108def get_stream_tee_file(stream, level, prefix=''):
109 if stream is None:
110 return _the_null_stream
Hidehiko Abed939d662017-06-13 20:25:03 +0900111 if stream is DEVNULL:
112 return None
Allen Li5ed7e632017-02-03 16:31:33 -0800113 if stream is TEE_TO_LOGS:
114 return logging_manager.LoggingFile(level=level, prefix=prefix)
115 return stream
116
117
118def _join_with_nickname(base_string, nickname):
119 if nickname:
120 return '%s BgJob "%s" ' % (base_string, nickname)
121 return base_string
122
123
Hidehiko Abed939d662017-06-13 20:25:03 +0900124# TODO: Cleanup and possibly eliminate |unjoinable|, which is only used in our
125# master-ssh connection process, while fixing underlying
Allen Li5ed7e632017-02-03 16:31:33 -0800126# semantics problem in BgJob. See crbug.com/279312
127class BgJob(object):
128 def __init__(self, command, stdout_tee=None, stderr_tee=None, verbose=True,
Daniel Erat3300f6e2018-01-09 17:34:31 -0800129 stdin=None, stdout_level=DEFAULT_STDOUT_LEVEL,
130 stderr_level=DEFAULT_STDERR_LEVEL, nickname=None,
Hidehiko Abed939d662017-06-13 20:25:03 +0900131 unjoinable=False, env=None, extra_paths=None):
Allen Li5ed7e632017-02-03 16:31:33 -0800132 """Create and start a new BgJob.
133
134 This constructor creates a new BgJob, and uses Popen to start a new
135 subprocess with given command. It returns without blocking on execution
136 of the subprocess.
137
138 After starting a new BgJob, use output_prepare to connect the process's
139 stdout and stderr pipes to the stream of your choice.
140
141 When the job is running, the jobs's output streams are only read from
142 when process_output is called.
143
144 @param command: command to be executed in new subprocess. May be either
145 a list, or a string (in which case Popen will be called
146 with shell=True)
Hidehiko Abed939d662017-06-13 20:25:03 +0900147 @param stdout_tee: (Optional) a file like object, TEE_TO_LOGS or
148 DEVNULL.
149 If not given, after finishing the process, the
150 stdout data from subprocess is available in
151 result.stdout.
152 If a file like object is given, in process_output(),
153 the stdout data from the subprocess will be handled
154 by the given file like object.
155 If TEE_TO_LOGS is given, in process_output(), the
156 stdout data from the subprocess will be handled by
Allen Li5ed7e632017-02-03 16:31:33 -0800157 the standard logging_manager.
Hidehiko Abed939d662017-06-13 20:25:03 +0900158 If DEVNULL is given, the stdout of the subprocess
159 will be just discarded. In addition, even after
160 cleanup(), result.stdout will be just an empty
161 string (unlike the case where stdout_tee is not
162 given).
Allen Li5ed7e632017-02-03 16:31:33 -0800163 @param stderr_tee: Same as stdout_tee, but for stderr.
164 @param verbose: Boolean, make BgJob logging more verbose.
165 @param stdin: Stream object, will be passed to Popen as the new
166 process's stdin.
Daniel Erat3300f6e2018-01-09 17:34:31 -0800167 @param stdout_level: A logging level value. If stdout_tee was set to
Allen Li5ed7e632017-02-03 16:31:33 -0800168 TEE_TO_LOGS, sets the level that tee'd
Daniel Erat3300f6e2018-01-09 17:34:31 -0800169 stdout output will be logged at. Ignored
Allen Li5ed7e632017-02-03 16:31:33 -0800170 otherwise.
Daniel Erat3300f6e2018-01-09 17:34:31 -0800171 @param stderr_level: Same as stdout_level, but for stderr.
Allen Li5ed7e632017-02-03 16:31:33 -0800172 @param nickname: Optional string, to be included in logging messages
Hidehiko Abed939d662017-06-13 20:25:03 +0900173 @param unjoinable: Optional bool, default False.
174 This should be True for BgJobs running in background
175 and will never be joined with join_bg_jobs(), such
176 as the master-ssh connection. Instead, it is
177 caller's responsibility to terminate the subprocess
178 correctly, e.g. by calling nuke_subprocess().
179 This will lead that, calling join_bg_jobs(),
180 process_output() or cleanup() will result in an
181 InvalidBgJobCall exception.
182 Also, |stdout_tee| and |stderr_tee| must be set to
183 DEVNULL, otherwise InvalidBgJobCall is raised.
Allen Li5ed7e632017-02-03 16:31:33 -0800184 @param env: Dict containing environment variables used in subprocess.
185 @param extra_paths: Optional string list, to be prepended to the PATH
186 env variable in env (or os.environ dict if env is
187 not specified).
188 """
189 self.command = command
Hidehiko Abed939d662017-06-13 20:25:03 +0900190 self.unjoinable = unjoinable
191 if (unjoinable and (stdout_tee != DEVNULL or stderr_tee != DEVNULL)):
192 raise error.InvalidBgJobCall(
193 'stdout_tee and stderr_tee must be DEVNULL for '
194 'unjoinable BgJob')
195 self._stdout_tee = get_stream_tee_file(
Daniel Erat3300f6e2018-01-09 17:34:31 -0800196 stdout_tee, stdout_level,
Allen Li5ed7e632017-02-03 16:31:33 -0800197 prefix=_join_with_nickname(STDOUT_PREFIX, nickname))
Hidehiko Abed939d662017-06-13 20:25:03 +0900198 self._stderr_tee = get_stream_tee_file(
199 stderr_tee, stderr_level,
Allen Li5ed7e632017-02-03 16:31:33 -0800200 prefix=_join_with_nickname(STDERR_PREFIX, nickname))
201 self.result = CmdResult(command)
202
203 # allow for easy stdin input by string, we'll let subprocess create
204 # a pipe for stdin input and we'll write to it in the wait loop
205 if isinstance(stdin, basestring):
206 self.string_stdin = stdin
207 stdin = subprocess.PIPE
208 else:
209 self.string_stdin = None
210
Allen Li5ed7e632017-02-03 16:31:33 -0800211 # Prepend extra_paths to env['PATH'] if necessary.
212 if extra_paths:
213 env = (os.environ if env is None else env).copy()
214 oldpath = env.get('PATH')
215 env['PATH'] = os.pathsep.join(
216 extra_paths + ([oldpath] if oldpath else []))
217
218 if verbose:
219 logging.debug("Running '%s'", command)
Hidehiko Abed939d662017-06-13 20:25:03 +0900220
Allen Li5ed7e632017-02-03 16:31:33 -0800221 if type(command) == list:
Hidehiko Abed939d662017-06-13 20:25:03 +0900222 shell = False
223 executable = None
Allen Li5ed7e632017-02-03 16:31:33 -0800224 else:
Hidehiko Abed939d662017-06-13 20:25:03 +0900225 shell = True
226 executable = '/bin/bash'
Allen Li5ed7e632017-02-03 16:31:33 -0800227
Hidehiko Abed939d662017-06-13 20:25:03 +0900228 with open('/dev/null', 'w') as devnull:
229 self.sp = subprocess.Popen(
230 command,
231 stdin=stdin,
232 stdout=devnull if stdout_tee == DEVNULL else subprocess.PIPE,
233 stderr=devnull if stderr_tee == DEVNULL else subprocess.PIPE,
234 preexec_fn=self._reset_sigpipe,
235 shell=shell, executable=executable,
236 env=env, close_fds=True)
237
Allen Li5ed7e632017-02-03 16:31:33 -0800238 self._cleanup_called = False
Hidehiko Abed939d662017-06-13 20:25:03 +0900239 self._stdout_file = (
240 None if stdout_tee == DEVNULL else StringIO.StringIO())
241 self._stderr_file = (
242 None if stderr_tee == DEVNULL else StringIO.StringIO())
Allen Li5ed7e632017-02-03 16:31:33 -0800243
244 def process_output(self, stdout=True, final_read=False):
245 """Read from process's output stream, and write data to destinations.
246
247 This function reads up to 1024 bytes from the background job's
248 stdout or stderr stream, and writes the resulting data to the BgJob's
249 output tee and to the stream set up in output_prepare.
250
251 Warning: Calls to process_output will block on reads from the
252 subprocess stream, and will block on writes to the configured
253 destination stream.
254
255 @param stdout: True = read and process data from job's stdout.
256 False = from stderr.
257 Default: True
258 @param final_read: Do not read only 1024 bytes from stream. Instead,
259 read and process all data until end of the stream.
260
261 """
Hidehiko Abed939d662017-06-13 20:25:03 +0900262 if self.unjoinable:
Allen Li5ed7e632017-02-03 16:31:33 -0800263 raise error.InvalidBgJobCall('Cannot call process_output on '
Hidehiko Abed939d662017-06-13 20:25:03 +0900264 'a job with unjoinable BgJob')
Allen Li5ed7e632017-02-03 16:31:33 -0800265 if stdout:
Hidehiko Abed939d662017-06-13 20:25:03 +0900266 pipe, buf, tee = (
267 self.sp.stdout, self._stdout_file, self._stdout_tee)
Allen Li5ed7e632017-02-03 16:31:33 -0800268 else:
Hidehiko Abed939d662017-06-13 20:25:03 +0900269 pipe, buf, tee = (
270 self.sp.stderr, self._stderr_file, self._stderr_tee)
271
272 if not pipe:
273 return
Allen Li5ed7e632017-02-03 16:31:33 -0800274
275 if final_read:
276 # read in all the data we can from pipe and then stop
277 data = []
278 while select.select([pipe], [], [], 0)[0]:
279 data.append(os.read(pipe.fileno(), 1024))
280 if len(data[-1]) == 0:
281 break
282 data = "".join(data)
283 else:
284 # perform a single read
285 data = os.read(pipe.fileno(), 1024)
286 buf.write(data)
287 tee.write(data)
288
Allen Li5ed7e632017-02-03 16:31:33 -0800289 def cleanup(self):
290 """Clean up after BgJob.
291
292 Flush the stdout_tee and stderr_tee buffers, close the
293 subprocess stdout and stderr buffers, and saves data from
294 the configured stdout and stderr destination streams to
295 self.result. Duplicate calls ignored with a warning.
296 """
Hidehiko Abed939d662017-06-13 20:25:03 +0900297 if self.unjoinable:
Allen Li5ed7e632017-02-03 16:31:33 -0800298 raise error.InvalidBgJobCall('Cannot call cleanup on '
Hidehiko Abed939d662017-06-13 20:25:03 +0900299 'a job with a unjoinable BgJob')
Allen Li5ed7e632017-02-03 16:31:33 -0800300 if self._cleanup_called:
301 logging.warning('BgJob [%s] received a duplicate call to '
302 'cleanup. Ignoring.', self.command)
303 return
304 try:
Hidehiko Abed939d662017-06-13 20:25:03 +0900305 if self.sp.stdout:
306 self._stdout_tee.flush()
307 self.sp.stdout.close()
308 self.result.stdout = self._stdout_file.getvalue()
309
310 if self.sp.stderr:
311 self._stderr_tee.flush()
312 self.sp.stderr.close()
313 self.result.stderr = self._stderr_file.getvalue()
Allen Li5ed7e632017-02-03 16:31:33 -0800314 finally:
315 self._cleanup_called = True
316
Allen Li5ed7e632017-02-03 16:31:33 -0800317 def _reset_sigpipe(self):
Aviv Keshet12aa4a22017-05-18 16:48:05 -0700318 if not env.IN_MOD_WSGI:
319 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
Allen Li5ed7e632017-02-03 16:31:33 -0800320
321
322def ip_to_long(ip):
323 # !L is a long in network byte order
324 return struct.unpack('!L', socket.inet_aton(ip))[0]
325
326
327def long_to_ip(number):
328 # See above comment.
329 return socket.inet_ntoa(struct.pack('!L', number))
330
331
332def create_subnet_mask(bits):
333 return (1 << 32) - (1 << 32-bits)
334
335
336def format_ip_with_mask(ip, mask_bits):
337 masked_ip = ip_to_long(ip) & create_subnet_mask(mask_bits)
338 return "%s/%s" % (long_to_ip(masked_ip), mask_bits)
339
340
341def normalize_hostname(alias):
342 ip = socket.gethostbyname(alias)
343 return socket.gethostbyaddr(ip)[0]
344
345
346def get_ip_local_port_range():
347 match = re.match(r'\s*(\d+)\s*(\d+)\s*$',
348 read_one_line('/proc/sys/net/ipv4/ip_local_port_range'))
349 return (int(match.group(1)), int(match.group(2)))
350
351
352def set_ip_local_port_range(lower, upper):
353 write_one_line('/proc/sys/net/ipv4/ip_local_port_range',
354 '%d %d\n' % (lower, upper))
355
356
357def read_one_line(filename):
Kuo-Hsin Yange42a4a82018-08-31 16:06:22 +0800358 f = open(filename, 'r')
359 try:
360 return f.readline().rstrip('\n')
361 finally:
362 f.close()
Allen Li5ed7e632017-02-03 16:31:33 -0800363
364
365def read_file(filename):
366 f = open(filename)
367 try:
368 return f.read()
369 finally:
370 f.close()
371
372
373def get_field(data, param, linestart="", sep=" "):
374 """
375 Parse data from string.
376 @param data: Data to parse.
377 example:
378 data:
379 cpu 324 345 34 5 345
380 cpu0 34 11 34 34 33
381 ^^^^
382 start of line
383 params 0 1 2 3 4
384 @param param: Position of parameter after linestart marker.
385 @param linestart: String to which start line with parameters.
386 @param sep: Separator between parameters regular expression.
387 """
388 search = re.compile(r"(?<=^%s)\s*(.*)" % linestart, re.MULTILINE)
389 find = search.search(data)
390 if find != None:
391 return re.split("%s" % sep, find.group(1))[param]
392 else:
393 print "There is no line which starts with %s in data." % linestart
394 return None
395
396
397def write_one_line(filename, line):
398 open_write_close(filename, str(line).rstrip('\n') + '\n')
399
400
401def open_write_close(filename, data):
402 f = open(filename, 'w')
403 try:
404 f.write(data)
405 finally:
406 f.close()
407
408
409def locate_file(path, base_dir=None):
410 """Locates a file.
411
Dean Liao34be76d2018-07-19 00:48:47 +0800412 @param path: The path of the file being located. Could be absolute or
413 relative path. For relative path, it tries to locate the file from
414 base_dir.
415
Allen Li5ed7e632017-02-03 16:31:33 -0800416 @param base_dir (optional): Base directory of the relative path.
417
418 @returns Absolute path of the file if found. None if path is None.
419 @raises error.TestFail if the file is not found.
420 """
421 if path is None:
422 return None
423
424 if not os.path.isabs(path) and base_dir is not None:
425 # Assume the relative path is based in autotest directory.
426 path = os.path.join(base_dir, path)
427 if not os.path.isfile(path):
428 raise error.TestFail('ERROR: Unable to find %s' % path)
429 return path
430
431
432def matrix_to_string(matrix, header=None):
433 """
434 Return a pretty, aligned string representation of a nxm matrix.
435
436 This representation can be used to print any tabular data, such as
437 database results. It works by scanning the lengths of each element
438 in each column, and determining the format string dynamically.
439
440 @param matrix: Matrix representation (list with n rows of m elements).
441 @param header: Optional tuple or list with header elements to be displayed.
442 """
443 if type(header) is list:
444 header = tuple(header)
445 lengths = []
446 if header:
447 for column in header:
448 lengths.append(len(column))
449 for row in matrix:
450 for i, column in enumerate(row):
451 column = unicode(column).encode("utf-8")
452 cl = len(column)
453 try:
454 ml = lengths[i]
455 if cl > ml:
456 lengths[i] = cl
457 except IndexError:
458 lengths.append(cl)
459
460 lengths = tuple(lengths)
461 format_string = ""
462 for length in lengths:
463 format_string += "%-" + str(length) + "s "
464 format_string += "\n"
465
466 matrix_str = ""
467 if header:
468 matrix_str += format_string % header
469 for row in matrix:
470 matrix_str += format_string % tuple(row)
471
472 return matrix_str
473
474
475def read_keyval(path, type_tag=None):
476 """
477 Read a key-value pair format file into a dictionary, and return it.
478 Takes either a filename or directory name as input. If it's a
479 directory name, we assume you want the file to be called keyval.
480
481 @param path: Full path of the file to read from.
482 @param type_tag: If not None, only keyvals with key ending
483 in a suffix {type_tag} will be collected.
484 """
485 if os.path.isdir(path):
486 path = os.path.join(path, 'keyval')
487 if not os.path.exists(path):
488 return {}
489
490 if type_tag:
491 pattern = r'^([-\.\w]+)\{%s\}=(.*)$' % type_tag
492 else:
493 pattern = r'^([-\.\w]+)=(.*)$'
494
495 keyval = {}
496 f = open(path)
497 for line in f:
498 line = re.sub('#.*', '', line).rstrip()
499 if not line:
500 continue
501 match = re.match(pattern, line)
502 if match:
503 key = match.group(1)
504 value = match.group(2)
505 if re.search('^\d+$', value):
506 value = int(value)
507 elif re.search('^(\d+\.)?\d+$', value):
508 value = float(value)
509 keyval[key] = value
510 else:
511 raise ValueError('Invalid format line: %s' % line)
512 f.close()
513 return keyval
514
515
Aviv Keshetf0c82242017-05-18 22:06:40 -0700516def write_keyval(path, dictionary, type_tag=None):
Allen Li5ed7e632017-02-03 16:31:33 -0800517 """
518 Write a key-value pair format file out to a file. This uses append
519 mode to open the file, so existing text will not be overwritten or
520 reparsed.
521
522 If type_tag is None, then the key must be composed of alphanumeric
523 characters (or dashes+underscores). However, if type-tag is not
524 null then the keys must also have "{type_tag}" as a suffix. At
525 the moment the only valid values of type_tag are "attr" and "perf".
526
527 @param path: full path of the file to be written
528 @param dictionary: the items to write
529 @param type_tag: see text above
530 """
531 if os.path.isdir(path):
532 path = os.path.join(path, 'keyval')
533 keyval = open(path, 'a')
534
535 if type_tag is None:
536 key_regex = re.compile(r'^[-\.\w]+$')
537 else:
538 if type_tag not in ('attr', 'perf'):
539 raise ValueError('Invalid type tag: %s' % type_tag)
540 escaped_tag = re.escape(type_tag)
541 key_regex = re.compile(r'^[-\.\w]+\{%s\}$' % escaped_tag)
542 try:
543 for key in sorted(dictionary.keys()):
544 if not key_regex.search(key):
545 raise ValueError('Invalid key: %s' % key)
546 keyval.write('%s=%s\n' % (key, dictionary[key]))
547 finally:
548 keyval.close()
549
Allen Li5ed7e632017-02-03 16:31:33 -0800550
551def is_url(path):
552 """Return true if path looks like a URL"""
553 # for now, just handle http and ftp
554 url_parts = urlparse.urlparse(path)
555 return (url_parts[0] in ('http', 'ftp'))
556
557
558def urlopen(url, data=None, timeout=5):
559 """Wrapper to urllib2.urlopen with timeout addition."""
560
561 # Save old timeout
562 old_timeout = socket.getdefaulttimeout()
563 socket.setdefaulttimeout(timeout)
564 try:
565 return urllib2.urlopen(url, data=data)
566 finally:
567 socket.setdefaulttimeout(old_timeout)
568
569
570def urlretrieve(url, filename, data=None, timeout=300):
571 """Retrieve a file from given url."""
572 logging.debug('Fetching %s -> %s', url, filename)
573
574 src_file = urlopen(url, data=data, timeout=timeout)
575 try:
576 dest_file = open(filename, 'wb')
577 try:
578 shutil.copyfileobj(src_file, dest_file)
579 finally:
580 dest_file.close()
581 finally:
582 src_file.close()
583
584
Kirtika Ruchandani7f10b272018-05-31 20:39:03 -0700585def hash(hashtype, input=None):
Allen Li5ed7e632017-02-03 16:31:33 -0800586 """
587 Returns an hash object of type md5 or sha1. This function is implemented in
588 order to encapsulate hash objects in a way that is compatible with python
589 2.4 and python 2.6 without warnings.
590
591 Note that even though python 2.6 hashlib supports hash types other than
592 md5 and sha1, we are artificially limiting the input values in order to
593 make the function to behave exactly the same among both python
594 implementations.
595
596 @param input: Optional input string that will be used to update the hash.
597 """
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -0700598 # pylint: disable=redefined-builtin
Kirtika Ruchandani7f10b272018-05-31 20:39:03 -0700599 if hashtype not in ['md5', 'sha1']:
600 raise ValueError("Unsupported hash type: %s" % hashtype)
Allen Li5ed7e632017-02-03 16:31:33 -0800601
602 try:
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -0700603 computed_hash = hashlib.new(hashtype)
Allen Li5ed7e632017-02-03 16:31:33 -0800604 except NameError:
Kirtika Ruchandani7f10b272018-05-31 20:39:03 -0700605 if hashtype == 'md5':
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -0700606 computed_hash = md5.new()
Kirtika Ruchandani7f10b272018-05-31 20:39:03 -0700607 elif hashtype == 'sha1':
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -0700608 computed_hash = sha.new()
Allen Li5ed7e632017-02-03 16:31:33 -0800609
610 if input:
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -0700611 computed_hash.update(input)
Allen Li5ed7e632017-02-03 16:31:33 -0800612
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -0700613 return computed_hash
Allen Li5ed7e632017-02-03 16:31:33 -0800614
615
616def get_file(src, dest, permissions=None):
617 """Get a file from src, which can be local or a remote URL"""
618 if src == dest:
619 return
620
621 if is_url(src):
622 urlretrieve(src, dest)
623 else:
624 shutil.copyfile(src, dest)
625
626 if permissions:
627 os.chmod(dest, permissions)
628 return dest
629
630
631def unmap_url(srcdir, src, destdir='.'):
632 """
633 Receives either a path to a local file or a URL.
634 returns either the path to the local file, or the fetched URL
635
636 unmap_url('/usr/src', 'foo.tar', '/tmp')
637 = '/usr/src/foo.tar'
638 unmap_url('/usr/src', 'http://site/file', '/tmp')
639 = '/tmp/file'
640 (after retrieving it)
641 """
642 if is_url(src):
643 url_parts = urlparse.urlparse(src)
644 filename = os.path.basename(url_parts[2])
645 dest = os.path.join(destdir, filename)
646 return get_file(src, dest)
647 else:
648 return os.path.join(srcdir, src)
649
650
651def update_version(srcdir, preserve_srcdir, new_version, install,
652 *args, **dargs):
653 """
654 Make sure srcdir is version new_version
655
656 If not, delete it and install() the new version.
657
658 In the preserve_srcdir case, we just check it's up to date,
659 and if not, we rerun install, without removing srcdir
660 """
661 versionfile = os.path.join(srcdir, '.version')
662 install_needed = True
663
664 if os.path.exists(versionfile):
665 old_version = pickle.load(open(versionfile))
666 if old_version == new_version:
667 install_needed = False
668
669 if install_needed:
670 if not preserve_srcdir and os.path.exists(srcdir):
671 shutil.rmtree(srcdir)
672 install(*args, **dargs)
673 if os.path.exists(srcdir):
674 pickle.dump(new_version, open(versionfile, 'w'))
675
676
Daniel Erat3300f6e2018-01-09 17:34:31 -0800677def get_stderr_level(stderr_is_expected, stdout_level=DEFAULT_STDOUT_LEVEL):
Allen Li5ed7e632017-02-03 16:31:33 -0800678 if stderr_is_expected:
Daniel Erat3300f6e2018-01-09 17:34:31 -0800679 return stdout_level
Allen Li5ed7e632017-02-03 16:31:33 -0800680 return DEFAULT_STDERR_LEVEL
681
682
Daniel Erat3300f6e2018-01-09 17:34:31 -0800683def run(command, timeout=None, ignore_status=False, stdout_tee=None,
684 stderr_tee=None, verbose=True, stdin=None, stderr_is_expected=None,
685 stdout_level=None, stderr_level=None, args=(), nickname=None,
686 ignore_timeout=False, env=None, extra_paths=None):
Allen Li5ed7e632017-02-03 16:31:33 -0800687 """
688 Run a command on the host.
689
690 @param command: the command line string.
691 @param timeout: time limit in seconds before attempting to kill the
692 running process. The run() function will take a few seconds
693 longer than 'timeout' to complete if it has to kill the process.
694 @param ignore_status: do not raise an exception, no matter what the exit
695 code of the command is.
696 @param stdout_tee: optional file-like object to which stdout data
697 will be written as it is generated (data will still be stored
Daniel Erat9ac588d2018-04-16 08:45:53 -0700698 in result.stdout unless this is DEVNULL).
Allen Li5ed7e632017-02-03 16:31:33 -0800699 @param stderr_tee: likewise for stderr.
700 @param verbose: if True, log the command being run.
701 @param stdin: stdin to pass to the executed process (can be a file
702 descriptor, a file object of a real file or a string).
703 @param stderr_is_expected: if True, stderr will be logged at the same level
704 as stdout
Daniel Erat3300f6e2018-01-09 17:34:31 -0800705 @param stdout_level: logging level used if stdout_tee is TEE_TO_LOGS;
706 if None, a default is used.
707 @param stderr_level: like stdout_level but for stderr.
Allen Li5ed7e632017-02-03 16:31:33 -0800708 @param args: sequence of strings of arguments to be given to the command
709 inside " quotes after they have been escaped for that; each
710 element in the sequence will be given as a separate command
711 argument
712 @param nickname: Short string that will appear in logging messages
713 associated with this command.
714 @param ignore_timeout: If True, timeouts are ignored otherwise if a
715 timeout occurs it will raise CmdTimeoutError.
716 @param env: Dict containing environment variables used in a subprocess.
717 @param extra_paths: Optional string list, to be prepended to the PATH
718 env variable in env (or os.environ dict if env is
719 not specified).
720
721 @return a CmdResult object or None if the command timed out and
722 ignore_timeout is True
Dana Goyette4dc0adc2019-05-06 14:51:53 -0700723 @rtype: CmdResult
Allen Li5ed7e632017-02-03 16:31:33 -0800724
725 @raise CmdError: the exit code of the command execution was not 0
726 @raise CmdTimeoutError: the command timed out and ignore_timeout is False.
727 """
728 if isinstance(args, basestring):
729 raise TypeError('Got a string for the "args" keyword argument, '
730 'need a sequence.')
731
732 # In some cases, command will actually be a list
733 # (For example, see get_user_hash in client/cros/cryptohome.py.)
734 # So, to cover that case, detect if it's a string or not and convert it
735 # into one if necessary.
736 if not isinstance(command, basestring):
737 command = ' '.join([sh_quote_word(arg) for arg in command])
738
739 command = ' '.join([command] + [sh_quote_word(arg) for arg in args])
Daniel Erat3300f6e2018-01-09 17:34:31 -0800740
Allen Li5ed7e632017-02-03 16:31:33 -0800741 if stderr_is_expected is None:
742 stderr_is_expected = ignore_status
Daniel Erat3300f6e2018-01-09 17:34:31 -0800743 if stdout_level is None:
744 stdout_level = DEFAULT_STDOUT_LEVEL
745 if stderr_level is None:
746 stderr_level = get_stderr_level(stderr_is_expected, stdout_level)
Allen Li5ed7e632017-02-03 16:31:33 -0800747
748 try:
749 bg_job = join_bg_jobs(
750 (BgJob(command, stdout_tee, stderr_tee, verbose, stdin=stdin,
Daniel Erat3300f6e2018-01-09 17:34:31 -0800751 stdout_level=stdout_level, stderr_level=stderr_level,
Allen Li5ed7e632017-02-03 16:31:33 -0800752 nickname=nickname, env=env, extra_paths=extra_paths),),
753 timeout)[0]
754 except error.CmdTimeoutError:
755 if not ignore_timeout:
756 raise
757 return None
758
759 if not ignore_status and bg_job.result.exit_status:
760 raise error.CmdError(command, bg_job.result,
761 "Command returned non-zero exit status")
762
763 return bg_job.result
764
765
766def run_parallel(commands, timeout=None, ignore_status=False,
767 stdout_tee=None, stderr_tee=None,
Kirtika Ruchandanif798f5e2018-07-08 18:34:20 -0700768 nicknames=None):
Allen Li5ed7e632017-02-03 16:31:33 -0800769 """
770 Behaves the same as run() with the following exceptions:
771
772 - commands is a list of commands to run in parallel.
773 - ignore_status toggles whether or not an exception should be raised
774 on any error.
775
776 @return: a list of CmdResult objects
777 """
778 bg_jobs = []
Kirtika Ruchandanif798f5e2018-07-08 18:34:20 -0700779 if nicknames is None:
780 nicknames = []
Allen Li5ed7e632017-02-03 16:31:33 -0800781 for (command, nickname) in itertools.izip_longest(commands, nicknames):
782 bg_jobs.append(BgJob(command, stdout_tee, stderr_tee,
783 stderr_level=get_stderr_level(ignore_status),
784 nickname=nickname))
785
786 # Updates objects in bg_jobs list with their process information
787 join_bg_jobs(bg_jobs, timeout)
788
789 for bg_job in bg_jobs:
790 if not ignore_status and bg_job.result.exit_status:
791 raise error.CmdError(command, bg_job.result,
792 "Command returned non-zero exit status")
793
794 return [bg_job.result for bg_job in bg_jobs]
795
796
797@deprecated
798def run_bg(command):
799 """Function deprecated. Please use BgJob class instead."""
800 bg_job = BgJob(command)
801 return bg_job.sp, bg_job.result
802
803
804def join_bg_jobs(bg_jobs, timeout=None):
805 """Joins the bg_jobs with the current thread.
806
807 Returns the same list of bg_jobs objects that was passed in.
808 """
Hidehiko Abed939d662017-06-13 20:25:03 +0900809 if any(bg_job.unjoinable for bg_job in bg_jobs):
810 raise error.InvalidBgJobCall(
811 'join_bg_jobs cannot be called for unjoinable bg_job')
Allen Li5ed7e632017-02-03 16:31:33 -0800812
Hidehiko Abed939d662017-06-13 20:25:03 +0900813 timeout_error = False
Allen Li5ed7e632017-02-03 16:31:33 -0800814 try:
815 # We are holding ends to stdin, stdout pipes
816 # hence we need to be sure to close those fds no mater what
817 start_time = time.time()
818 timeout_error = _wait_for_commands(bg_jobs, start_time, timeout)
819
820 for bg_job in bg_jobs:
821 # Process stdout and stderr
822 bg_job.process_output(stdout=True,final_read=True)
823 bg_job.process_output(stdout=False,final_read=True)
824 finally:
825 # close our ends of the pipes to the sp no matter what
826 for bg_job in bg_jobs:
827 bg_job.cleanup()
828
829 if timeout_error:
830 # TODO: This needs to be fixed to better represent what happens when
831 # running in parallel. However this is backwards compatable, so it will
832 # do for the time being.
833 raise error.CmdTimeoutError(
834 bg_jobs[0].command, bg_jobs[0].result,
835 "Command(s) did not complete within %d seconds" % timeout)
836
837
838 return bg_jobs
839
840
841def _wait_for_commands(bg_jobs, start_time, timeout):
842 """Waits for background jobs by select polling their stdout/stderr.
843
844 @param bg_jobs: A list of background jobs to wait on.
845 @param start_time: Time used to calculate the timeout lifetime of a job.
846 @param timeout: The timeout of the list of bg_jobs.
847
848 @return: True if the return was due to a timeout, False otherwise.
849 """
850
851 # To check for processes which terminate without producing any output
852 # a 1 second timeout is used in select.
853 SELECT_TIMEOUT = 1
854
855 read_list = []
856 write_list = []
857 reverse_dict = {}
858
859 for bg_job in bg_jobs:
Hidehiko Abed939d662017-06-13 20:25:03 +0900860 if bg_job.sp.stdout:
861 read_list.append(bg_job.sp.stdout)
862 reverse_dict[bg_job.sp.stdout] = (bg_job, True)
863 if bg_job.sp.stderr:
864 read_list.append(bg_job.sp.stderr)
865 reverse_dict[bg_job.sp.stderr] = (bg_job, False)
Allen Li5ed7e632017-02-03 16:31:33 -0800866 if bg_job.string_stdin is not None:
867 write_list.append(bg_job.sp.stdin)
868 reverse_dict[bg_job.sp.stdin] = bg_job
869
870 if timeout:
871 stop_time = start_time + timeout
872 time_left = stop_time - time.time()
873 else:
874 time_left = None # so that select never times out
875
876 while not timeout or time_left > 0:
877 # select will return when we may write to stdin, when there is
878 # stdout/stderr output we can read (including when it is
879 # EOF, that is the process has terminated) or when a non-fatal
880 # signal was sent to the process. In the last case the select returns
881 # EINTR, and we continue waiting for the job if the signal handler for
882 # the signal that interrupted the call allows us to.
883 try:
884 read_ready, write_ready, _ = select.select(read_list, write_list,
885 [], SELECT_TIMEOUT)
886 except select.error as v:
887 if v[0] == errno.EINTR:
888 logging.warning(v)
889 continue
890 else:
891 raise
892 # os.read() has to be used instead of
893 # subproc.stdout.read() which will otherwise block
894 for file_obj in read_ready:
895 bg_job, is_stdout = reverse_dict[file_obj]
896 bg_job.process_output(is_stdout)
897
898 for file_obj in write_ready:
899 # we can write PIPE_BUF bytes without blocking
900 # POSIX requires PIPE_BUF is >= 512
901 bg_job = reverse_dict[file_obj]
902 file_obj.write(bg_job.string_stdin[:512])
903 bg_job.string_stdin = bg_job.string_stdin[512:]
904 # no more input data, close stdin, remove it from the select set
905 if not bg_job.string_stdin:
906 file_obj.close()
907 write_list.remove(file_obj)
908 del reverse_dict[file_obj]
909
910 all_jobs_finished = True
911 for bg_job in bg_jobs:
912 if bg_job.result.exit_status is not None:
913 continue
914
915 bg_job.result.exit_status = bg_job.sp.poll()
916 if bg_job.result.exit_status is not None:
917 # process exited, remove its stdout/stdin from the select set
918 bg_job.result.duration = time.time() - start_time
Hidehiko Abed939d662017-06-13 20:25:03 +0900919 if bg_job.sp.stdout:
920 read_list.remove(bg_job.sp.stdout)
921 del reverse_dict[bg_job.sp.stdout]
922 if bg_job.sp.stderr:
923 read_list.remove(bg_job.sp.stderr)
924 del reverse_dict[bg_job.sp.stderr]
Allen Li5ed7e632017-02-03 16:31:33 -0800925 else:
926 all_jobs_finished = False
927
928 if all_jobs_finished:
929 return False
930
931 if timeout:
932 time_left = stop_time - time.time()
933
934 # Kill all processes which did not complete prior to timeout
935 for bg_job in bg_jobs:
936 if bg_job.result.exit_status is not None:
937 continue
938
939 logging.warning('run process timeout (%s) fired on: %s', timeout,
940 bg_job.command)
941 if nuke_subprocess(bg_job.sp) is None:
942 # If process could not be SIGKILL'd, log kernel stack.
943 logging.warning(read_file('/proc/%d/stack' % bg_job.sp.pid))
944 bg_job.result.exit_status = bg_job.sp.poll()
945 bg_job.result.duration = time.time() - start_time
946
947 return True
948
949
950def pid_is_alive(pid):
951 """
952 True if process pid exists and is not yet stuck in Zombie state.
953 Zombies are impossible to move between cgroups, etc.
954 pid can be integer, or text of integer.
955 """
956 path = '/proc/%s/stat' % pid
957
958 try:
959 stat = read_one_line(path)
960 except IOError:
961 if not os.path.exists(path):
962 # file went away
963 return False
964 raise
965
966 return stat.split()[2] != 'Z'
967
968
969def signal_pid(pid, sig):
970 """
971 Sends a signal to a process id. Returns True if the process terminated
972 successfully, False otherwise.
973 """
974 try:
975 os.kill(pid, sig)
976 except OSError:
977 # The process may have died before we could kill it.
978 pass
979
Kirtika Ruchandani17f3a382018-07-08 01:59:05 -0700980 for _ in range(5):
Allen Li5ed7e632017-02-03 16:31:33 -0800981 if not pid_is_alive(pid):
982 return True
983 time.sleep(1)
984
985 # The process is still alive
986 return False
987
988
989def nuke_subprocess(subproc):
990 # check if the subprocess is still alive, first
991 if subproc.poll() is not None:
992 return subproc.poll()
993
994 # the process has not terminated within timeout,
995 # kill it via an escalating series of signals.
996 signal_queue = [signal.SIGTERM, signal.SIGKILL]
997 for sig in signal_queue:
998 signal_pid(subproc.pid, sig)
999 if subproc.poll() is not None:
1000 return subproc.poll()
1001
1002
1003def nuke_pid(pid, signal_queue=(signal.SIGTERM, signal.SIGKILL)):
1004 # the process has not terminated within timeout,
1005 # kill it via an escalating series of signals.
1006 pid_path = '/proc/%d/'
1007 if not os.path.exists(pid_path % pid):
1008 # Assume that if the pid does not exist in proc it is already dead.
1009 logging.error('No listing in /proc for pid:%d.', pid)
1010 raise error.AutoservPidAlreadyDeadError('Could not kill nonexistant '
1011 'pid: %s.', pid)
1012 for sig in signal_queue:
1013 if signal_pid(pid, sig):
1014 return
1015
1016 # no signal successfully terminated the process
1017 raise error.AutoservRunError('Could not kill %d for process name: %s' % (
1018 pid, get_process_name(pid)), None)
1019
1020
1021def system(command, timeout=None, ignore_status=False):
1022 """
1023 Run a command
1024
1025 @param timeout: timeout in seconds
1026 @param ignore_status: if ignore_status=False, throw an exception if the
1027 command's exit code is non-zero
1028 if ignore_stauts=True, return the exit code.
1029
1030 @return exit status of command
1031 (note, this will always be zero unless ignore_status=True)
1032 """
1033 return run(command, timeout=timeout, ignore_status=ignore_status,
1034 stdout_tee=TEE_TO_LOGS, stderr_tee=TEE_TO_LOGS).exit_status
1035
1036
1037def system_parallel(commands, timeout=None, ignore_status=False):
1038 """This function returns a list of exit statuses for the respective
1039 list of commands."""
1040 return [bg_jobs.exit_status for bg_jobs in
1041 run_parallel(commands, timeout=timeout, ignore_status=ignore_status,
1042 stdout_tee=TEE_TO_LOGS, stderr_tee=TEE_TO_LOGS)]
1043
1044
1045def system_output(command, timeout=None, ignore_status=False,
1046 retain_output=False, args=()):
1047 """
1048 Run a command and return the stdout output.
1049
1050 @param command: command string to execute.
1051 @param timeout: time limit in seconds before attempting to kill the
1052 running process. The function will take a few seconds longer
1053 than 'timeout' to complete if it has to kill the process.
1054 @param ignore_status: do not raise an exception, no matter what the exit
1055 code of the command is.
1056 @param retain_output: set to True to make stdout/stderr of the command
1057 output to be also sent to the logging system
1058 @param args: sequence of strings of arguments to be given to the command
1059 inside " quotes after they have been escaped for that; each
1060 element in the sequence will be given as a separate command
1061 argument
1062
1063 @return a string with the stdout output of the command.
1064 """
1065 if retain_output:
1066 out = run(command, timeout=timeout, ignore_status=ignore_status,
1067 stdout_tee=TEE_TO_LOGS, stderr_tee=TEE_TO_LOGS,
1068 args=args).stdout
1069 else:
1070 out = run(command, timeout=timeout, ignore_status=ignore_status,
1071 args=args).stdout
1072 if out[-1:] == '\n':
1073 out = out[:-1]
1074 return out
1075
1076
1077def system_output_parallel(commands, timeout=None, ignore_status=False,
1078 retain_output=False):
1079 if retain_output:
1080 out = [bg_job.stdout for bg_job
1081 in run_parallel(commands, timeout=timeout,
1082 ignore_status=ignore_status,
1083 stdout_tee=TEE_TO_LOGS, stderr_tee=TEE_TO_LOGS)]
1084 else:
1085 out = [bg_job.stdout for bg_job in run_parallel(commands,
1086 timeout=timeout, ignore_status=ignore_status)]
Kirtika Ruchandani17f3a382018-07-08 01:59:05 -07001087 for _ in out:
Kirtika Ruchandani5c2b4c42018-08-25 14:31:42 -07001088 if out[-1:] == '\n':
1089 out = out[:-1]
Allen Li5ed7e632017-02-03 16:31:33 -08001090 return out
1091
1092
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -07001093def strip_unicode(input_obj):
1094 if type(input_obj) == list:
1095 return [strip_unicode(i) for i in input_obj]
1096 elif type(input_obj) == dict:
Allen Li5ed7e632017-02-03 16:31:33 -08001097 output = {}
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -07001098 for key in input_obj.keys():
1099 output[str(key)] = strip_unicode(input_obj[key])
Allen Li5ed7e632017-02-03 16:31:33 -08001100 return output
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -07001101 elif type(input_obj) == unicode:
1102 return str(input_obj)
Allen Li5ed7e632017-02-03 16:31:33 -08001103 else:
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -07001104 return input_obj
Allen Li5ed7e632017-02-03 16:31:33 -08001105
1106
1107def get_cpu_percentage(function, *args, **dargs):
1108 """Returns a tuple containing the CPU% and return value from function call.
1109
1110 This function calculates the usage time by taking the difference of
1111 the user and system times both before and after the function call.
1112 """
1113 child_pre = resource.getrusage(resource.RUSAGE_CHILDREN)
1114 self_pre = resource.getrusage(resource.RUSAGE_SELF)
1115 start = time.time()
1116 to_return = function(*args, **dargs)
1117 elapsed = time.time() - start
1118 self_post = resource.getrusage(resource.RUSAGE_SELF)
1119 child_post = resource.getrusage(resource.RUSAGE_CHILDREN)
1120
1121 # Calculate CPU Percentage
1122 s_user, s_system = [a - b for a, b in zip(self_post, self_pre)[:2]]
1123 c_user, c_system = [a - b for a, b in zip(child_post, child_pre)[:2]]
1124 cpu_percent = (s_user + c_user + s_system + c_system) / elapsed
1125
1126 return cpu_percent, to_return
1127
1128
1129def get_arch(run_function=run):
1130 """
1131 Get the hardware architecture of the machine.
1132 If specified, run_function should return a CmdResult object and throw a
1133 CmdError exception.
1134 If run_function is anything other than utils.run(), it is used to
1135 execute the commands. By default (when set to utils.run()) this will
1136 just examine os.uname()[4].
1137 """
1138
1139 # Short circuit from the common case.
1140 if run_function == run:
1141 return re.sub(r'i\d86$', 'i386', os.uname()[4])
1142
1143 # Otherwise, use the run_function in case it hits a remote machine.
1144 arch = run_function('/bin/uname -m').stdout.rstrip()
1145 if re.match(r'i\d86$', arch):
1146 arch = 'i386'
1147 return arch
1148
1149def get_arch_userspace(run_function=run):
1150 """
1151 Get the architecture by userspace (possibly different from kernel).
1152 """
1153 archs = {
1154 'arm': 'ELF 32-bit.*, ARM,',
Adam Kallai77754052018-07-12 10:02:40 +02001155 'arm64': 'ELF 64-bit.*, ARM aarch64,',
Allen Li5ed7e632017-02-03 16:31:33 -08001156 'i386': 'ELF 32-bit.*, Intel 80386,',
1157 'x86_64': 'ELF 64-bit.*, x86-64,',
1158 }
1159
1160 cmd = 'file --brief --dereference /bin/sh'
1161 filestr = run_function(cmd).stdout.rstrip()
1162 for a, regex in archs.iteritems():
1163 if re.match(regex, filestr):
1164 return a
1165
1166 return get_arch()
1167
1168
1169def get_num_logical_cpus_per_socket(run_function=run):
1170 """
1171 Get the number of cores (including hyperthreading) per cpu.
1172 run_function is used to execute the commands. It defaults to
1173 utils.run() but a custom method (if provided) should be of the
1174 same schema as utils.run. It should return a CmdResult object and
1175 throw a CmdError exception.
1176 """
1177 siblings = run_function('grep "^siblings" /proc/cpuinfo').stdout.rstrip()
Billy Zhao8c107882019-05-31 14:46:03 -07001178 num_siblings = [int(x) for x in
1179 re.findall(r'^siblings\s*:\s*(\d+)\s*$', siblings, re.M)]
Allen Li5ed7e632017-02-03 16:31:33 -08001180 if len(num_siblings) == 0:
1181 raise error.TestError('Unable to find siblings info in /proc/cpuinfo')
1182 if min(num_siblings) != max(num_siblings):
1183 raise error.TestError('Number of siblings differ %r' %
1184 num_siblings)
1185 return num_siblings[0]
1186
1187
Alex Khouderchah19e0ab32018-07-06 17:03:21 -07001188def set_high_performance_mode(host=None):
1189 """
1190 Sets the kernel governor mode to the highest setting.
1191 Returns previous governor state.
1192 """
1193 original_governors = get_scaling_governor_states(host)
1194 set_scaling_governors('performance', host)
1195 return original_governors
1196
1197
1198def set_scaling_governors(value, host=None):
1199 """
1200 Sets all scaling governor to string value.
1201 Sample values: 'performance', 'interactive', 'ondemand', 'powersave'.
1202 """
1203 paths = _get_cpufreq_paths('scaling_governor', host)
1204 if not paths:
1205 logging.info("Could not set governor states, as no files of the form "
1206 "'/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor' "
1207 "were found.")
1208 run_func = host.run if host else system
1209 for path in paths:
1210 cmd = 'echo %s > %s' % (value, path)
1211 logging.info('Writing scaling governor mode \'%s\' -> %s', value, path)
1212 # On Tegra CPUs can be dynamically enabled/disabled. Ignore failures.
1213 run_func(cmd, ignore_status=True)
1214
1215
1216def _get_cpufreq_paths(filename, host=None):
1217 """
1218 Returns a list of paths to the governors.
1219 """
1220 run_func = host.run if host else run
Brian Norris951f3572019-01-16 10:13:03 -08001221 glob = '/sys/devices/system/cpu/cpu*/cpufreq/' + filename
1222 # Simple glob expansion; note that CPUs may come and go, causing these
1223 # paths to change at any time.
1224 cmd = 'echo ' + glob
Alex Khouderchah19e0ab32018-07-06 17:03:21 -07001225 try:
Brian Norris951f3572019-01-16 10:13:03 -08001226 paths = run_func(cmd, verbose=False).stdout.split()
Alex Khouderchah19e0ab32018-07-06 17:03:21 -07001227 except error.CmdError:
1228 return []
Brian Norris951f3572019-01-16 10:13:03 -08001229 # If the glob result equals itself, then we likely didn't match any real
1230 # paths (assuming 'cpu*' is not a real path).
1231 if paths == [glob]:
1232 return []
Alex Khouderchah19e0ab32018-07-06 17:03:21 -07001233 return paths
1234
1235
1236def get_scaling_governor_states(host=None):
1237 """
1238 Returns a list of (performance governor path, current state) tuples.
1239 """
1240 paths = _get_cpufreq_paths('scaling_governor', host)
1241 path_value_list = []
1242 run_func = host.run if host else run
1243 for path in paths:
1244 value = run_func('head -n 1 %s' % path, verbose=False).stdout
1245 path_value_list.append((path, value))
1246 return path_value_list
1247
1248
1249def restore_scaling_governor_states(path_value_list, host=None):
1250 """
1251 Restores governor states. Inverse operation to get_scaling_governor_states.
1252 """
1253 run_func = host.run if host else system
1254 for (path, value) in path_value_list:
1255 cmd = 'echo %s > %s' % (value.rstrip('\n'), path)
1256 # On Tegra CPUs can be dynamically enabled/disabled. Ignore failures.
1257 run_func(cmd, ignore_status=True)
1258
1259
Allen Li5ed7e632017-02-03 16:31:33 -08001260def merge_trees(src, dest):
1261 """
1262 Merges a source directory tree at 'src' into a destination tree at
1263 'dest'. If a path is a file in both trees than the file in the source
1264 tree is APPENDED to the one in the destination tree. If a path is
1265 a directory in both trees then the directories are recursively merged
1266 with this function. In any other case, the function will skip the
1267 paths that cannot be merged (instead of failing).
1268 """
1269 if not os.path.exists(src):
1270 return # exists only in dest
1271 elif not os.path.exists(dest):
1272 if os.path.isfile(src):
1273 shutil.copy2(src, dest) # file only in src
1274 else:
1275 shutil.copytree(src, dest, symlinks=True) # dir only in src
1276 return
1277 elif os.path.isfile(src) and os.path.isfile(dest):
1278 # src & dest are files in both trees, append src to dest
1279 destfile = open(dest, "a")
1280 try:
1281 srcfile = open(src)
1282 try:
1283 destfile.write(srcfile.read())
1284 finally:
1285 srcfile.close()
1286 finally:
1287 destfile.close()
1288 elif os.path.isdir(src) and os.path.isdir(dest):
1289 # src & dest are directories in both trees, so recursively merge
1290 for name in os.listdir(src):
1291 merge_trees(os.path.join(src, name), os.path.join(dest, name))
1292 else:
1293 # src & dest both exist, but are incompatible
1294 return
1295
1296
1297class CmdResult(object):
1298 """
1299 Command execution result.
1300
1301 command: String containing the command line itself
1302 exit_status: Integer exit code of the process
1303 stdout: String containing stdout of the process
1304 stderr: String containing stderr of the process
1305 duration: Elapsed wall clock time running the process
1306 """
1307
1308
1309 def __init__(self, command="", stdout="", stderr="",
1310 exit_status=None, duration=0):
1311 self.command = command
1312 self.exit_status = exit_status
1313 self.stdout = stdout
1314 self.stderr = stderr
1315 self.duration = duration
1316
1317
Prathmesh Prabhu54905412017-07-11 17:08:38 -07001318 def __eq__(self, other):
1319 if type(self) == type(other):
1320 return (self.command == other.command
1321 and self.exit_status == other.exit_status
1322 and self.stdout == other.stdout
1323 and self.stderr == other.stderr
1324 and self.duration == other.duration)
1325 else:
1326 return NotImplemented
1327
1328
Allen Li5ed7e632017-02-03 16:31:33 -08001329 def __repr__(self):
1330 wrapper = textwrap.TextWrapper(width = 78,
1331 initial_indent="\n ",
1332 subsequent_indent=" ")
1333
1334 stdout = self.stdout.rstrip()
1335 if stdout:
1336 stdout = "\nstdout:\n%s" % stdout
1337
1338 stderr = self.stderr.rstrip()
1339 if stderr:
1340 stderr = "\nstderr:\n%s" % stderr
1341
1342 return ("* Command: %s\n"
1343 "Exit status: %s\n"
1344 "Duration: %s\n"
1345 "%s"
1346 "%s"
1347 % (wrapper.fill(str(self.command)), self.exit_status,
1348 self.duration, stdout, stderr))
1349
1350
1351class run_randomly:
1352 def __init__(self, run_sequentially=False):
1353 # Run sequentially is for debugging control files
1354 self.test_list = []
1355 self.run_sequentially = run_sequentially
1356
1357
1358 def add(self, *args, **dargs):
1359 test = (args, dargs)
1360 self.test_list.append(test)
1361
1362
1363 def run(self, fn):
1364 while self.test_list:
1365 test_index = random.randint(0, len(self.test_list)-1)
1366 if self.run_sequentially:
1367 test_index = 0
1368 (args, dargs) = self.test_list.pop(test_index)
1369 fn(*args, **dargs)
1370
1371
1372def import_site_module(path, module, dummy=None, modulefile=None):
1373 """
1374 Try to import the site specific module if it exists.
1375
1376 @param path full filename of the source file calling this (ie __file__)
1377 @param module full module name
1378 @param dummy dummy value to return in case there is no symbol to import
1379 @param modulefile module filename
1380
1381 @return site specific module or dummy
1382
1383 @raises ImportError if the site file exists but imports fails
1384 """
1385 short_module = module[module.rfind(".") + 1:]
1386
1387 if not modulefile:
1388 modulefile = short_module + ".py"
1389
1390 if os.path.exists(os.path.join(os.path.dirname(path), modulefile)):
1391 return __import__(module, {}, {}, [short_module])
1392 return dummy
1393
1394
1395def import_site_symbol(path, module, name, dummy=None, modulefile=None):
1396 """
1397 Try to import site specific symbol from site specific file if it exists
1398
1399 @param path full filename of the source file calling this (ie __file__)
1400 @param module full module name
1401 @param name symbol name to be imported from the site file
1402 @param dummy dummy value to return in case there is no symbol to import
1403 @param modulefile module filename
1404
1405 @return site specific symbol or dummy
1406
1407 @raises ImportError if the site file exists but imports fails
1408 """
1409 module = import_site_module(path, module, modulefile=modulefile)
1410 if not module:
1411 return dummy
1412
1413 # special unique value to tell us if the symbol can't be imported
1414 cant_import = object()
1415
1416 obj = getattr(module, name, cant_import)
1417 if obj is cant_import:
1418 return dummy
1419
1420 return obj
1421
1422
1423def import_site_class(path, module, classname, baseclass, modulefile=None):
1424 """
1425 Try to import site specific class from site specific file if it exists
1426
1427 Args:
1428 path: full filename of the source file calling this (ie __file__)
1429 module: full module name
1430 classname: class name to be loaded from site file
1431 baseclass: base class object to return when no site file present or
1432 to mixin when site class exists but is not inherited from baseclass
1433 modulefile: module filename
1434
1435 Returns: baseclass if site specific class does not exist, the site specific
1436 class if it exists and is inherited from baseclass or a mixin of the
1437 site specific class and baseclass when the site specific class exists
1438 and is not inherited from baseclass
1439
1440 Raises: ImportError if the site file exists but imports fails
1441 """
1442
1443 res = import_site_symbol(path, module, classname, None, modulefile)
1444 if res:
1445 if not issubclass(res, baseclass):
1446 # if not a subclass of baseclass then mix in baseclass with the
1447 # site specific class object and return the result
1448 res = type(classname, (res, baseclass), {})
1449 else:
1450 res = baseclass
1451
1452 return res
1453
1454
1455def import_site_function(path, module, funcname, dummy, modulefile=None):
1456 """
1457 Try to import site specific function from site specific file if it exists
1458
1459 Args:
1460 path: full filename of the source file calling this (ie __file__)
1461 module: full module name
1462 funcname: function name to be imported from site file
1463 dummy: dummy function to return in case there is no function to import
1464 modulefile: module filename
1465
1466 Returns: site specific function object or dummy
1467
1468 Raises: ImportError if the site file exists but imports fails
1469 """
1470
1471 return import_site_symbol(path, module, funcname, dummy, modulefile)
1472
1473
1474def _get_pid_path(program_name):
1475 my_path = os.path.dirname(__file__)
1476 return os.path.abspath(os.path.join(my_path, "..", "..",
1477 "%s.pid" % program_name))
1478
1479
1480def write_pid(program_name):
1481 """
1482 Try to drop <program_name>.pid in the main autotest directory.
1483
1484 Args:
1485 program_name: prefix for file name
1486 """
1487 pidfile = open(_get_pid_path(program_name), "w")
1488 try:
1489 pidfile.write("%s\n" % os.getpid())
1490 finally:
1491 pidfile.close()
1492
1493
1494def delete_pid_file_if_exists(program_name):
1495 """
1496 Tries to remove <program_name>.pid from the main autotest directory.
1497 """
1498 pidfile_path = _get_pid_path(program_name)
1499
1500 try:
1501 os.remove(pidfile_path)
1502 except OSError:
1503 if not os.path.exists(pidfile_path):
1504 return
1505 raise
1506
1507
1508def get_pid_from_file(program_name):
1509 """
1510 Reads the pid from <program_name>.pid in the autotest directory.
1511
1512 @param program_name the name of the program
1513 @return the pid if the file exists, None otherwise.
1514 """
1515 pidfile_path = _get_pid_path(program_name)
1516 if not os.path.exists(pidfile_path):
1517 return None
1518
1519 pidfile = open(_get_pid_path(program_name), 'r')
1520
1521 try:
1522 try:
1523 pid = int(pidfile.readline())
1524 except IOError:
1525 if not os.path.exists(pidfile_path):
1526 return None
1527 raise
1528 finally:
1529 pidfile.close()
1530
1531 return pid
1532
1533
1534def get_process_name(pid):
1535 """
1536 Get process name from PID.
1537 @param pid: PID of process.
1538 @return: Process name if PID stat file exists or 'Dead PID' if it does not.
1539 """
1540 pid_stat_path = "/proc/%d/stat"
1541 if not os.path.exists(pid_stat_path % pid):
1542 return "Dead Pid"
1543 return get_field(read_file(pid_stat_path % pid), 1)[1:-1]
1544
1545
1546def program_is_alive(program_name):
1547 """
1548 Checks if the process is alive and not in Zombie state.
1549
1550 @param program_name the name of the program
1551 @return True if still alive, False otherwise
1552 """
1553 pid = get_pid_from_file(program_name)
1554 if pid is None:
1555 return False
1556 return pid_is_alive(pid)
1557
1558
1559def signal_program(program_name, sig=signal.SIGTERM):
1560 """
1561 Sends a signal to the process listed in <program_name>.pid
1562
1563 @param program_name the name of the program
1564 @param sig signal to send
1565 """
1566 pid = get_pid_from_file(program_name)
1567 if pid:
1568 signal_pid(pid, sig)
1569
1570
1571def get_relative_path(path, reference):
1572 """Given 2 absolute paths "path" and "reference", compute the path of
1573 "path" as relative to the directory "reference".
1574
1575 @param path the absolute path to convert to a relative path
1576 @param reference an absolute directory path to which the relative
1577 path will be computed
1578 """
1579 # normalize the paths (remove double slashes, etc)
1580 assert(os.path.isabs(path))
1581 assert(os.path.isabs(reference))
1582
1583 path = os.path.normpath(path)
1584 reference = os.path.normpath(reference)
1585
1586 # we could use os.path.split() but it splits from the end
1587 path_list = path.split(os.path.sep)[1:]
1588 ref_list = reference.split(os.path.sep)[1:]
1589
1590 # find the longest leading common path
1591 for i in xrange(min(len(path_list), len(ref_list))):
1592 if path_list[i] != ref_list[i]:
1593 # decrement i so when exiting this loop either by no match or by
1594 # end of range we are one step behind
1595 i -= 1
1596 break
1597 i += 1
1598 # drop the common part of the paths, not interested in that anymore
1599 del path_list[:i]
1600
1601 # for each uncommon component in the reference prepend a ".."
1602 path_list[:0] = ['..'] * (len(ref_list) - i)
1603
1604 return os.path.join(*path_list)
1605
1606
1607def sh_escape(command):
1608 """
1609 Escape special characters from a command so that it can be passed
1610 as a double quoted (" ") string in a (ba)sh command.
1611
1612 Args:
1613 command: the command string to escape.
1614
1615 Returns:
1616 The escaped command string. The required englobing double
1617 quotes are NOT added and so should be added at some point by
1618 the caller.
1619
1620 See also: http://www.tldp.org/LDP/abs/html/escapingsection.html
1621 """
1622 command = command.replace("\\", "\\\\")
1623 command = command.replace("$", r'\$')
1624 command = command.replace('"', r'\"')
1625 command = command.replace('`', r'\`')
1626 return command
1627
1628
1629def sh_quote_word(text, whitelist=SHELL_QUOTING_WHITELIST):
1630 r"""Quote a string to make it safe as a single word in a shell command.
1631
1632 POSIX shell syntax recognizes no escape characters inside a single-quoted
1633 string. So, single quotes can safely quote any string of characters except
1634 a string with a single quote character. A single quote character must be
1635 quoted with the sequence '\'' which translates to:
1636 ' -> close current quote
1637 \' -> insert a literal single quote
1638 ' -> reopen quoting again.
1639
1640 This is safe for all combinations of characters, including embedded and
1641 trailing backslashes in odd or even numbers.
1642
1643 This is also safe for nesting, e.g. the following is a valid use:
1644
1645 adb_command = 'adb shell %s' % (
1646 sh_quote_word('echo %s' % sh_quote_word('hello world')))
1647
1648 @param text: The string to be quoted into a single word for the shell.
1649 @param whitelist: Optional list of characters that do not need quoting.
1650 Defaults to a known good list of characters.
1651
1652 @return A string, possibly quoted, safe as a single word for a shell.
1653 """
1654 if all(c in whitelist for c in text):
1655 return text
1656 return "'" + text.replace("'", r"'\''") + "'"
1657
1658
1659def configure(extra=None, configure='./configure'):
1660 """
1661 Run configure passing in the correct host, build, and target options.
1662
1663 @param extra: extra command line arguments to pass to configure
1664 @param configure: which configure script to use
1665 """
1666 args = []
1667 if 'CHOST' in os.environ:
1668 args.append('--host=' + os.environ['CHOST'])
1669 if 'CBUILD' in os.environ:
1670 args.append('--build=' + os.environ['CBUILD'])
1671 if 'CTARGET' in os.environ:
1672 args.append('--target=' + os.environ['CTARGET'])
1673 if extra:
1674 args.append(extra)
1675
1676 system('%s %s' % (configure, ' '.join(args)))
1677
1678
1679def make(extra='', make='make', timeout=None, ignore_status=False):
1680 """
1681 Run make, adding MAKEOPTS to the list of options.
1682
1683 @param extra: extra command line arguments to pass to make.
1684 """
1685 cmd = '%s %s %s' % (make, os.environ.get('MAKEOPTS', ''), extra)
1686 return system(cmd, timeout=timeout, ignore_status=ignore_status)
1687
1688
1689def compare_versions(ver1, ver2):
1690 """Version number comparison between ver1 and ver2 strings.
1691
1692 >>> compare_tuple("1", "2")
1693 -1
1694 >>> compare_tuple("foo-1.1", "foo-1.2")
1695 -1
1696 >>> compare_tuple("1.2", "1.2a")
1697 -1
1698 >>> compare_tuple("1.2b", "1.2a")
1699 1
1700 >>> compare_tuple("1.3.5.3a", "1.3.5.3b")
1701 -1
1702
1703 Args:
1704 ver1: version string
1705 ver2: version string
1706
1707 Returns:
1708 int: 1 if ver1 > ver2
1709 0 if ver1 == ver2
1710 -1 if ver1 < ver2
1711 """
1712 ax = re.split('[.-]', ver1)
1713 ay = re.split('[.-]', ver2)
1714 while len(ax) > 0 and len(ay) > 0:
1715 cx = ax.pop(0)
1716 cy = ay.pop(0)
1717 maxlen = max(len(cx), len(cy))
1718 c = cmp(cx.zfill(maxlen), cy.zfill(maxlen))
1719 if c != 0:
1720 return c
1721 return cmp(len(ax), len(ay))
1722
1723
1724def args_to_dict(args):
1725 """Convert autoserv extra arguments in the form of key=val or key:val to a
1726 dictionary. Each argument key is converted to lowercase dictionary key.
1727
1728 Args:
1729 args - list of autoserv extra arguments.
1730
1731 Returns:
1732 dictionary
1733 """
1734 arg_re = re.compile(r'(\w+)[:=](.*)$')
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -07001735 args_dict = {}
Allen Li5ed7e632017-02-03 16:31:33 -08001736 for arg in args:
1737 match = arg_re.match(arg)
1738 if match:
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -07001739 args_dict[match.group(1).lower()] = match.group(2)
Allen Li5ed7e632017-02-03 16:31:33 -08001740 else:
1741 logging.warning("args_to_dict: argument '%s' doesn't match "
1742 "'%s' pattern. Ignored.", arg, arg_re.pattern)
Kirtika Ruchandani82a18c42018-07-08 19:16:46 -07001743 return args_dict
Allen Li5ed7e632017-02-03 16:31:33 -08001744
1745
1746def get_unused_port():
1747 """
1748 Finds a semi-random available port. A race condition is still
1749 possible after the port number is returned, if another process
1750 happens to bind it.
1751
1752 Returns:
1753 A port number that is unused on both TCP and UDP.
1754 """
1755
1756 def try_bind(port, socket_type, socket_proto):
1757 s = socket.socket(socket.AF_INET, socket_type, socket_proto)
1758 try:
1759 try:
1760 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1761 s.bind(('', port))
1762 return s.getsockname()[1]
1763 except socket.error:
1764 return None
1765 finally:
1766 s.close()
1767
1768 # On the 2.6 kernel, calling try_bind() on UDP socket returns the
1769 # same port over and over. So always try TCP first.
1770 while True:
1771 # Ask the OS for an unused port.
1772 port = try_bind(0, socket.SOCK_STREAM, socket.IPPROTO_TCP)
1773 # Check if this port is unused on the other protocol.
1774 if port and try_bind(port, socket.SOCK_DGRAM, socket.IPPROTO_UDP):
1775 return port
1776
1777
1778def ask(question, auto=False):
1779 """
1780 Raw input with a prompt that emulates logging.
1781
1782 @param question: Question to be asked
1783 @param auto: Whether to return "y" instead of asking the question
1784 """
1785 if auto:
1786 logging.info("%s (y/n) y", question)
1787 return "y"
1788 return raw_input("%s INFO | %s (y/n) " %
1789 (time.strftime("%H:%M:%S", time.localtime()), question))
1790
1791
1792def rdmsr(address, cpu=0):
1793 """
1794 Reads an x86 MSR from the specified CPU, returns as long integer.
1795 """
1796 with open('/dev/cpu/%s/msr' % cpu, 'r', 0) as fd:
1797 fd.seek(address)
1798 return struct.unpack('=Q', fd.read(8))[0]
1799
1800
1801def wait_for_value(func,
1802 expected_value=None,
1803 min_threshold=None,
1804 max_threshold=None,
1805 timeout_sec=10):
1806 """
1807 Returns the value of func(). If |expected_value|, |min_threshold|, and
1808 |max_threshold| are not set, returns immediately.
1809
1810 If |expected_value| is set, polls the return value until |expected_value| is
1811 reached, and returns that value.
1812
1813 If either |max_threshold| or |min_threshold| is set, this function will
1814 will repeatedly call func() until the return value reaches or exceeds one of
1815 these thresholds.
1816
1817 Polling will stop after |timeout_sec| regardless of these thresholds.
1818
1819 @param func: function whose return value is to be waited on.
1820 @param expected_value: wait for func to return this value.
1821 @param min_threshold: wait for func value to reach or fall below this value.
1822 @param max_threshold: wait for func value to reach or rise above this value.
1823 @param timeout_sec: Number of seconds to wait before giving up and
1824 returning whatever value func() last returned.
1825
1826 Return value:
1827 The most recent return value of func().
1828 """
1829 value = None
1830 start_time_sec = time.time()
1831 while True:
1832 value = func()
1833 if (expected_value is None and \
1834 min_threshold is None and \
1835 max_threshold is None) or \
1836 (expected_value is not None and value == expected_value) or \
1837 (min_threshold is not None and value <= min_threshold) or \
1838 (max_threshold is not None and value >= max_threshold):
1839 break
1840
1841 if time.time() - start_time_sec >= timeout_sec:
1842 break
1843 time.sleep(0.1)
1844
1845 return value
1846
1847
1848def wait_for_value_changed(func,
1849 old_value=None,
1850 timeout_sec=10):
1851 """
1852 Returns the value of func().
1853
1854 The function polls the return value until it is different from |old_value|,
1855 and returns that value.
1856
1857 Polling will stop after |timeout_sec|.
1858
1859 @param func: function whose return value is to be waited on.
1860 @param old_value: wait for func to return a value different from this.
1861 @param timeout_sec: Number of seconds to wait before giving up and
1862 returning whatever value func() last returned.
1863
1864 @returns The most recent return value of func().
1865 """
1866 value = None
1867 start_time_sec = time.time()
1868 while True:
1869 value = func()
1870 if value != old_value:
1871 break
1872
1873 if time.time() - start_time_sec >= timeout_sec:
1874 break
1875 time.sleep(0.1)
1876
1877 return value
1878
1879
1880CONFIG = global_config.global_config
1881
1882# Keep checking if the pid is alive every second until the timeout (in seconds)
1883CHECK_PID_IS_ALIVE_TIMEOUT = 6
1884
1885_LOCAL_HOST_LIST = ('localhost', '127.0.0.1')
1886
1887# The default address of a vm gateway.
1888DEFAULT_VM_GATEWAY = '10.0.2.2'
1889
1890# Google Storage bucket URI to store results in.
1891DEFAULT_OFFLOAD_GSURI = CONFIG.get_config_value(
1892 'CROS', 'results_storage_server', default=None)
1893
1894# Default Moblab Ethernet Interface.
1895_MOBLAB_ETH_0 = 'eth0'
1896_MOBLAB_ETH_1 = 'eth1'
1897
1898# A list of subnets that requires dedicated devserver and drone in the same
1899# subnet. Each item is a tuple of (subnet_ip, mask_bits), e.g.,
1900# ('192.168.0.0', 24))
1901RESTRICTED_SUBNETS = []
1902
1903def _setup_restricted_subnets():
1904 restricted_subnets_list = CONFIG.get_config_value(
1905 'CROS', 'restricted_subnets', type=list, default=[])
1906 # TODO(dshi): Remove the code to split subnet with `:` after R51 is
1907 # off stable channel, and update shadow config to use `/` as
1908 # delimiter for consistency.
1909 for subnet in restricted_subnets_list:
1910 ip, mask_bits = subnet.split('/') if '/' in subnet \
1911 else subnet.split(':')
1912 RESTRICTED_SUBNETS.append((ip, int(mask_bits)))
1913
1914_setup_restricted_subnets()
1915
1916# regex pattern for CLIENT/wireless_ssid_ config. For example, global config
1917# can have following config in CLIENT section to indicate that hosts in subnet
1918# 192.168.0.1/24 should use wireless ssid of `ssid_1`
1919# wireless_ssid_192.168.0.1/24: ssid_1
1920WIRELESS_SSID_PATTERN = 'wireless_ssid_(.*)/(\d+)'
1921
1922
Keith Haddowa4b55dd2018-02-28 14:34:59 -08001923def get_moblab_serial_number():
Keith Haddowd2798fe2018-07-24 10:06:50 -07001924 """Gets a unique identifier for the moblab.
Allen Li5ed7e632017-02-03 16:31:33 -08001925
Keith Haddowd2798fe2018-07-24 10:06:50 -07001926 Serial number is the prefered identifier, use it if
1927 present, however fallback is the ethernet mac address.
Allen Li5ed7e632017-02-03 16:31:33 -08001928 """
Keith Haddowd2798fe2018-07-24 10:06:50 -07001929 for vpd_key in ['serial_number', 'ethernet_mac']:
1930 try:
1931 cmd_result = run('sudo vpd -g %s' % vpd_key)
1932 if cmd_result and cmd_result.stdout:
1933 return cmd_result.stdout
1934 except error.CmdError as e:
1935 logging.error(str(e))
1936 logging.info(vpd_key)
Keith Haddowa4b55dd2018-02-28 14:34:59 -08001937 return 'NoSerialNumber'
Allen Li5ed7e632017-02-03 16:31:33 -08001938
1939
Kevin Cernekee0f709a62018-02-16 15:13:13 -08001940def ping(host, deadline=None, tries=None, timeout=60, user=None):
Allen Li5ed7e632017-02-03 16:31:33 -08001941 """Attempt to ping |host|.
1942
1943 Shell out to 'ping' if host is an IPv4 addres or 'ping6' if host is an
1944 IPv6 address to try to reach |host| for |timeout| seconds.
1945 Returns exit code of ping.
1946
1947 Per 'man ping', if you specify BOTH |deadline| and |tries|, ping only
1948 returns 0 if we get responses to |tries| pings within |deadline| seconds.
1949
1950 Specifying |deadline| or |count| alone should return 0 as long as
1951 some packets receive responses.
1952
1953 Note that while this works with literal IPv6 addresses it will not work
1954 with hostnames that resolve to IPv6 only.
1955
1956 @param host: the host to ping.
1957 @param deadline: seconds within which |tries| pings must succeed.
1958 @param tries: number of pings to send.
1959 @param timeout: number of seconds after which to kill 'ping' command.
1960 @return exit code of ping command.
1961 """
1962 args = [host]
Kevin Cernekee0f709a62018-02-16 15:13:13 -08001963 cmd = 'ping6' if re.search(r':.*:', host) else 'ping'
Allen Li5ed7e632017-02-03 16:31:33 -08001964
1965 if deadline:
1966 args.append('-w%d' % deadline)
1967 if tries:
1968 args.append('-c%d' % tries)
1969
Kevin Cernekee0f709a62018-02-16 15:13:13 -08001970 if user != None:
1971 args = [user, '-c', ' '.join([cmd] + args)]
1972 cmd = 'su'
1973
Dana Goyette6005f362019-09-27 16:06:40 -07001974 result = run(cmd, args=args, verbose=True,
1975 ignore_status=True, timeout=timeout,
1976 stderr_tee=TEE_TO_LOGS)
1977
1978 rc = result.exit_status
1979 lines = result.stdout.splitlines()
1980
1981 # rc=0: host reachable
1982 # rc=1: host unreachable
1983 # other: an error (do not abbreviate)
1984 if rc in (0, 1):
1985 # Report the two stats lines, as a single line.
1986 # [-2]: packets transmitted, 1 received, 0% packet loss, time 0ms
1987 # [-1]: rtt min/avg/max/mdev = 0.497/0.497/0.497/0.000 ms
1988 stats = lines[-2:]
1989 while '' in stats:
1990 stats.remove('')
1991
1992 if stats or len(lines) < 2:
1993 logging.debug('[rc=%s] %s', rc, '; '.join(stats))
1994 else:
1995 logging.debug('[rc=%s] Ping output:\n%s',
1996 rc, result.stdout)
1997 else:
1998 output = result.stdout.rstrip()
1999 if output:
2000 logging.debug('Unusual ping result (rc=%s):\n%s', rc, output)
2001 else:
2002 logging.debug('Unusual ping result (rc=%s).', rc)
2003 return rc
Allen Li5ed7e632017-02-03 16:31:33 -08002004
2005
2006def host_is_in_lab_zone(hostname):
2007 """Check if the host is in the CLIENT.dns_zone.
2008
2009 @param hostname: The hostname to check.
2010 @returns True if hostname.dns_zone resolves, otherwise False.
2011 """
2012 host_parts = hostname.split('.')
2013 dns_zone = CONFIG.get_config_value('CLIENT', 'dns_zone', default=None)
2014 fqdn = '%s.%s' % (host_parts[0], dns_zone)
Xixuan Wud227ac82019-01-07 14:04:51 -08002015 logging.debug('Checking if host %s is in lab zone.', fqdn)
Allen Li5ed7e632017-02-03 16:31:33 -08002016 try:
2017 socket.gethostbyname(fqdn)
2018 return True
2019 except socket.gaierror:
2020 return False
2021
2022
Puthikorn Voravootivata272b7f2020-02-05 14:27:33 -08002023def host_is_in_power_lab(hostname):
2024 """Check if the hostname is in power lab.
2025
2026 Example: chromeos1-power-host2.cros
2027
2028 @param hostname: The hostname to check.
2029 @returns True if hostname match power lab hostname, otherwise False.
2030 """
2031 pattern = r'chromeos\d+-power-host\d+(\.cros(\.corp(\.google\.com)?)?)?$'
2032 return re.match(pattern, hostname) is not None
2033
2034
2035def get_power_lab_wlan_hostname(hostname):
2036 """Return wlan hostname for host in power lab.
2037
2038 Example: chromeos1-power-host2.cros -> chromeos1-power-host2-wlan.cros
2039
2040 @param hostname: The hostname in power lab.
2041 @returns wlan hostname.
2042 """
2043 split_host = hostname.split('.')
2044 split_host[0] += '-wlan'
2045 return '.'.join(split_host)
2046
2047
Prathmesh Prabhucbd5ebb2018-08-28 17:04:50 -07002048def in_moblab_ssp():
2049 """Detects if this execution is inside an SSP container on moblab."""
2050 config_is_moblab = CONFIG.get_config_value('SSP', 'is_moblab', type=bool,
2051 default=False)
2052 return is_in_container() and config_is_moblab
2053
2054
Allen Li5ed7e632017-02-03 16:31:33 -08002055def get_chrome_version(job_views):
2056 """
2057 Retrieves the version of the chrome binary associated with a job.
2058
2059 When a test runs we query the chrome binary for it's version and drop
2060 that value into a client keyval. To retrieve the chrome version we get all
2061 the views associated with a test from the db, including those of the
2062 server and client jobs, and parse the version out of the first test view
2063 that has it. If we never ran a single test in the suite the job_views
2064 dictionary will not contain a chrome version.
2065
2066 This method cannot retrieve the chrome version from a dictionary that
2067 does not conform to the structure of an autotest tko view.
2068
2069 @param job_views: a list of a job's result views, as returned by
2070 the get_detailed_test_views method in rpc_interface.
2071 @return: The chrome version string, or None if one can't be found.
2072 """
2073
2074 # Aborted jobs have no views.
2075 if not job_views:
2076 return None
2077
2078 for view in job_views:
2079 if (view.get('attributes')
2080 and constants.CHROME_VERSION in view['attributes'].keys()):
2081
2082 return view['attributes'].get(constants.CHROME_VERSION)
2083
2084 logging.warning('Could not find chrome version for failure.')
2085 return None
2086
2087
Allen Li5ed7e632017-02-03 16:31:33 -08002088def get_moblab_id():
2089 """Gets the moblab random id.
2090
2091 The random id file is cached on disk. If it does not exist, a new file is
2092 created the first time.
2093
2094 @returns the moblab random id.
2095 """
2096 moblab_id_filepath = '/home/moblab/.moblab_id'
Keith Haddowfa0412a2017-05-18 09:17:22 -07002097 try:
2098 if os.path.exists(moblab_id_filepath):
2099 with open(moblab_id_filepath, 'r') as moblab_id_file:
2100 random_id = moblab_id_file.read()
2101 else:
2102 random_id = uuid.uuid1().hex
2103 with open(moblab_id_filepath, 'w') as moblab_id_file:
2104 moblab_id_file.write('%s' % random_id)
2105 except IOError as e:
2106 # Possible race condition, another process has created the file.
2107 # Sleep a second to make sure the file gets closed.
2108 logging.info(e)
2109 time.sleep(1)
Allen Li5ed7e632017-02-03 16:31:33 -08002110 with open(moblab_id_filepath, 'r') as moblab_id_file:
2111 random_id = moblab_id_file.read()
Allen Li5ed7e632017-02-03 16:31:33 -08002112 return random_id
2113
2114
2115def get_offload_gsuri():
2116 """Return the GSURI to offload test results to.
2117
2118 For the normal use case this is the results_storage_server in the
2119 global_config.
2120
2121 However partners using Moblab will be offloading their results to a
2122 subdirectory of their image storage buckets. The subdirectory is
2123 determined by the MAC Address of the Moblab device.
2124
2125 @returns gsuri to offload test results to.
2126 """
2127 # For non-moblab, use results_storage_server or default.
Dean Liao34be76d2018-07-19 00:48:47 +08002128 if not is_moblab(): # pylint: disable=undefined-variable
Allen Li5ed7e632017-02-03 16:31:33 -08002129 return DEFAULT_OFFLOAD_GSURI
2130
2131 # For moblab, use results_storage_server or image_storage_server as bucket
2132 # name and mac-address/moblab_id as path.
2133 gsuri = DEFAULT_OFFLOAD_GSURI
2134 if not gsuri:
Dean Liao34be76d2018-07-19 00:48:47 +08002135 gsuri = "%sresults/" % CONFIG.get_config_value('CROS',
2136 'image_storage_server')
Allen Li5ed7e632017-02-03 16:31:33 -08002137
Keith Haddowa4b55dd2018-02-28 14:34:59 -08002138 return '%s%s/%s/' % (gsuri, get_moblab_serial_number(), get_moblab_id())
Allen Li5ed7e632017-02-03 16:31:33 -08002139
2140
2141# TODO(petermayo): crosbug.com/31826 Share this with _GsUpload in
2142# //chromite.git/buildbot/prebuilt.py somewhere/somehow
2143def gs_upload(local_file, remote_file, acl, result_dir=None,
2144 transfer_timeout=300, acl_timeout=300):
2145 """Upload to GS bucket.
2146
2147 @param local_file: Local file to upload
2148 @param remote_file: Remote location to upload the local_file to.
2149 @param acl: name or file used for controlling access to the uploaded
2150 file.
2151 @param result_dir: Result directory if you want to add tracing to the
2152 upload.
2153 @param transfer_timeout: Timeout for this upload call.
2154 @param acl_timeout: Timeout for the acl call needed to confirm that
2155 the uploader has permissions to execute the upload.
2156
2157 @raise CmdError: the exit code of the gsutil call was not 0.
2158
2159 @returns True/False - depending on if the upload succeeded or failed.
2160 """
2161 # https://developers.google.com/storage/docs/accesscontrol#extension
2162 CANNED_ACLS = ['project-private', 'private', 'public-read',
2163 'public-read-write', 'authenticated-read',
2164 'bucket-owner-read', 'bucket-owner-full-control']
2165 _GSUTIL_BIN = 'gsutil'
2166 acl_cmd = None
2167 if acl in CANNED_ACLS:
2168 cmd = '%s cp -a %s %s %s' % (_GSUTIL_BIN, acl, local_file, remote_file)
2169 else:
2170 # For private uploads we assume that the overlay board is set up
2171 # properly and a googlestore_acl.xml is present, if not this script
2172 # errors
2173 cmd = '%s cp -a private %s %s' % (_GSUTIL_BIN, local_file, remote_file)
2174 if not os.path.exists(acl):
2175 logging.error('Unable to find ACL File %s.', acl)
2176 return False
2177 acl_cmd = '%s setacl %s %s' % (_GSUTIL_BIN, acl, remote_file)
2178 if not result_dir:
2179 run(cmd, timeout=transfer_timeout, verbose=True)
2180 if acl_cmd:
2181 run(acl_cmd, timeout=acl_timeout, verbose=True)
2182 return True
2183 with open(os.path.join(result_dir, 'tracing'), 'w') as ftrace:
2184 ftrace.write('Preamble\n')
2185 run(cmd, timeout=transfer_timeout, verbose=True,
2186 stdout_tee=ftrace, stderr_tee=ftrace)
2187 if acl_cmd:
2188 ftrace.write('\nACL setting\n')
2189 # Apply the passed in ACL xml file to the uploaded object.
2190 run(acl_cmd, timeout=acl_timeout, verbose=True,
2191 stdout_tee=ftrace, stderr_tee=ftrace)
2192 ftrace.write('Postamble\n')
2193 return True
2194
2195
2196def gs_ls(uri_pattern):
2197 """Returns a list of URIs that match a given pattern.
2198
2199 @param uri_pattern: a GS URI pattern, may contain wildcards
2200
2201 @return A list of URIs matching the given pattern.
2202
2203 @raise CmdError: the gsutil command failed.
2204
2205 """
2206 gs_cmd = ' '.join(['gsutil', 'ls', uri_pattern])
2207 result = system_output(gs_cmd).splitlines()
2208 return [path.rstrip() for path in result if path]
2209
2210
Kirtika Ruchandanif798f5e2018-07-08 18:34:20 -07002211def nuke_pids(pid_list, signal_queue=None):
Allen Li5ed7e632017-02-03 16:31:33 -08002212 """
2213 Given a list of pid's, kill them via an esclating series of signals.
2214
2215 @param pid_list: List of PID's to kill.
2216 @param signal_queue: Queue of signals to send the PID's to terminate them.
2217
2218 @return: A mapping of the signal name to the number of processes it
2219 was sent to.
2220 """
Kirtika Ruchandanif798f5e2018-07-08 18:34:20 -07002221 if signal_queue is None:
2222 signal_queue = [signal.SIGTERM, signal.SIGKILL]
Allen Li5ed7e632017-02-03 16:31:33 -08002223 sig_count = {}
2224 # Though this is slightly hacky it beats hardcoding names anyday.
2225 sig_names = dict((k, v) for v, k in signal.__dict__.iteritems()
2226 if v.startswith('SIG'))
2227 for sig in signal_queue:
2228 logging.debug('Sending signal %s to the following pids:', sig)
2229 sig_count[sig_names.get(sig, 'unknown_signal')] = len(pid_list)
2230 for pid in pid_list:
2231 logging.debug('Pid %d', pid)
2232 try:
2233 os.kill(pid, sig)
2234 except OSError:
2235 # The process may have died from a previous signal before we
2236 # could kill it.
2237 pass
2238 if sig == signal.SIGKILL:
2239 return sig_count
2240 pid_list = [pid for pid in pid_list if pid_is_alive(pid)]
2241 if not pid_list:
2242 break
2243 time.sleep(CHECK_PID_IS_ALIVE_TIMEOUT)
2244 failed_list = []
2245 for pid in pid_list:
2246 if pid_is_alive(pid):
2247 failed_list.append('Could not kill %d for process name: %s.' % pid,
2248 get_process_name(pid))
2249 if failed_list:
2250 raise error.AutoservRunError('Following errors occured: %s' %
2251 failed_list, None)
2252 return sig_count
2253
2254
2255def externalize_host(host):
2256 """Returns an externally accessible host name.
2257
2258 @param host: a host name or address (string)
2259
2260 @return An externally visible host name or address
2261
2262 """
2263 return socket.gethostname() if host in _LOCAL_HOST_LIST else host
2264
2265
2266def urlopen_socket_timeout(url, data=None, timeout=5):
2267 """
2268 Wrapper to urllib2.urlopen with a socket timeout.
2269
2270 This method will convert all socket timeouts to
2271 TimeoutExceptions, so we can use it in conjunction
2272 with the rpc retry decorator and continue to handle
2273 other URLErrors as we see fit.
2274
2275 @param url: The url to open.
2276 @param data: The data to send to the url (eg: the urlencoded dictionary
2277 used with a POST call).
2278 @param timeout: The timeout for this urlopen call.
2279
2280 @return: The response of the urlopen call.
2281
2282 @raises: error.TimeoutException when a socket timeout occurs.
2283 urllib2.URLError for errors that not caused by timeout.
2284 urllib2.HTTPError for errors like 404 url not found.
2285 """
2286 old_timeout = socket.getdefaulttimeout()
2287 socket.setdefaulttimeout(timeout)
2288 try:
2289 return urllib2.urlopen(url, data=data)
2290 except urllib2.URLError as e:
2291 if type(e.reason) is socket.timeout:
2292 raise error.TimeoutException(str(e))
2293 raise
2294 finally:
2295 socket.setdefaulttimeout(old_timeout)
2296
2297
2298def parse_chrome_version(version_string):
2299 """
2300 Parse a chrome version string and return version and milestone.
2301
2302 Given a chrome version of the form "W.X.Y.Z", return "W.X.Y.Z" as
2303 the version and "W" as the milestone.
2304
2305 @param version_string: Chrome version string.
2306 @return: a tuple (chrome_version, milestone). If the incoming version
2307 string is not of the form "W.X.Y.Z", chrome_version will
2308 be set to the incoming "version_string" argument and the
2309 milestone will be set to the empty string.
2310 """
2311 match = re.search('(\d+)\.\d+\.\d+\.\d+', version_string)
2312 ver = match.group(0) if match else version_string
2313 milestone = match.group(1) if match else ''
2314 return ver, milestone
2315
2316
Jon Flatley5fd0a272019-02-20 09:42:55 -08002317def parse_gs_uri_version(uri):
2318 """Pull out major.minor.sub from image URI
2319
2320 @param uri: A GS URI for a bucket containing ChromeOS build artifacts
2321 @return: The build version as a string in the form 'major.minor.sub'
2322
2323 """
2324 return re.sub('.*(R[0-9]+|LATEST)-', '', uri).strip('/')
2325
2326
2327def compare_gs_uri_build_versions(x, y):
2328 """Compares two bucket URIs by their version string
2329
2330 @param x: A GS URI for a bucket containing ChromeOS build artifacts
2331 @param y: Another GS URI for a bucket containing ChromeOS build artifacts
2332 @return: 1 if x > y, -1 if x < y, and 0 if x == y
2333
2334 """
2335 # Converts a gs uri 'gs://.../R75-<major>.<minor>.<sub>' to
2336 # [major, minor, sub]
Billy Zhao8c107882019-05-31 14:46:03 -07002337 split_version = lambda v: [int(x) for x in
2338 parse_gs_uri_version(v).split('.')]
Jon Flatley5fd0a272019-02-20 09:42:55 -08002339
2340 x_version = split_version(x)
2341 y_version = split_version(y)
2342
2343 for a, b in zip(x_version, y_version):
2344 if a > b:
2345 return 1
2346 elif b > a:
2347 return -1
2348
2349 return 0
2350
2351
Allen Li5ed7e632017-02-03 16:31:33 -08002352def is_localhost(server):
2353 """Check if server is equivalent to localhost.
2354
2355 @param server: Name of the server to check.
2356
2357 @return: True if given server is equivalent to localhost.
2358
2359 @raise socket.gaierror: If server name failed to be resolved.
2360 """
2361 if server in _LOCAL_HOST_LIST:
2362 return True
2363 try:
2364 return (socket.gethostbyname(socket.gethostname()) ==
2365 socket.gethostbyname(server))
2366 except socket.gaierror:
2367 logging.error('Failed to resolve server name %s.', server)
2368 return False
2369
2370
Allen Li5ed7e632017-02-03 16:31:33 -08002371def get_function_arg_value(func, arg_name, args, kwargs):
2372 """Get the value of the given argument for the function.
2373
2374 @param func: Function being called with given arguments.
2375 @param arg_name: Name of the argument to look for value.
2376 @param args: arguments for function to be called.
2377 @param kwargs: keyword arguments for function to be called.
2378
2379 @return: The value of the given argument for the function.
2380
2381 @raise ValueError: If the argument is not listed function arguemnts.
2382 @raise KeyError: If no value is found for the given argument.
2383 """
2384 if arg_name in kwargs:
2385 return kwargs[arg_name]
2386
2387 argspec = inspect.getargspec(func)
2388 index = argspec.args.index(arg_name)
2389 try:
2390 return args[index]
2391 except IndexError:
2392 try:
2393 # The argument can use a default value. Reverse the default value
2394 # so argument with default value can be counted from the last to
2395 # the first.
2396 return argspec.defaults[::-1][len(argspec.args) - index - 1]
2397 except IndexError:
2398 raise KeyError('Argument %s is not given a value. argspec: %s, '
2399 'args:%s, kwargs:%s' %
2400 (arg_name, argspec, args, kwargs))
2401
2402
2403def has_systemd():
2404 """Check if the host is running systemd.
2405
2406 @return: True if the host uses systemd, otherwise returns False.
2407 """
2408 return os.path.basename(os.readlink('/proc/1/exe')) == 'systemd'
2409
2410
Allen Li5ed7e632017-02-03 16:31:33 -08002411def get_real_user():
2412 """Get the real user that runs the script.
2413
2414 The function check environment variable SUDO_USER for the user if the
2415 script is run with sudo. Otherwise, it returns the value of environment
2416 variable USER.
2417
2418 @return: The user name that runs the script.
2419
2420 """
2421 user = os.environ.get('SUDO_USER')
2422 if not user:
2423 user = os.environ.get('USER')
2424 return user
2425
2426
2427def get_service_pid(service_name):
2428 """Return pid of service.
2429
2430 @param service_name: string name of service.
2431
2432 @return: pid or 0 if service is not running.
2433 """
2434 if has_systemd():
2435 # systemctl show prints 'MainPID=0' if the service is not running.
2436 cmd_result = run('systemctl show -p MainPID %s' %
2437 service_name, ignore_status=True)
2438 return int(cmd_result.stdout.split('=')[1])
2439 else:
2440 cmd_result = run('status %s' % service_name,
2441 ignore_status=True)
2442 if 'start/running' in cmd_result.stdout:
2443 return int(cmd_result.stdout.split()[3])
2444 return 0
2445
2446
2447def control_service(service_name, action='start', ignore_status=True):
2448 """Controls a service. It can be used to start, stop or restart
2449 a service.
2450
2451 @param service_name: string service to be restarted.
2452
2453 @param action: string choice of action to control command.
2454
2455 @param ignore_status: boolean ignore if system command fails.
2456
2457 @return: status code of the executed command.
2458 """
2459 if action not in ('start', 'stop', 'restart'):
2460 raise ValueError('Unknown action supplied as parameter.')
2461
2462 control_cmd = action + ' ' + service_name
2463 if has_systemd():
2464 control_cmd = 'systemctl ' + control_cmd
2465 return system(control_cmd, ignore_status=ignore_status)
2466
2467
2468def restart_service(service_name, ignore_status=True):
2469 """Restarts a service
2470
2471 @param service_name: string service to be restarted.
2472
2473 @param ignore_status: boolean ignore if system command fails.
2474
2475 @return: status code of the executed command.
2476 """
Dean Liao34be76d2018-07-19 00:48:47 +08002477 return control_service(service_name, action='restart',
2478 ignore_status=ignore_status)
Allen Li5ed7e632017-02-03 16:31:33 -08002479
2480
2481def start_service(service_name, ignore_status=True):
2482 """Starts a service
2483
2484 @param service_name: string service to be started.
2485
2486 @param ignore_status: boolean ignore if system command fails.
2487
2488 @return: status code of the executed command.
2489 """
Dean Liao34be76d2018-07-19 00:48:47 +08002490 return control_service(service_name, action='start',
2491 ignore_status=ignore_status)
Allen Li5ed7e632017-02-03 16:31:33 -08002492
2493
2494def stop_service(service_name, ignore_status=True):
2495 """Stops a service
2496
2497 @param service_name: string service to be stopped.
2498
2499 @param ignore_status: boolean ignore if system command fails.
2500
2501 @return: status code of the executed command.
2502 """
Dean Liao34be76d2018-07-19 00:48:47 +08002503 return control_service(service_name, action='stop',
2504 ignore_status=ignore_status)
Allen Li5ed7e632017-02-03 16:31:33 -08002505
2506
2507def sudo_require_password():
2508 """Test if the process can run sudo command without using password.
2509
2510 @return: True if the process needs password to run sudo command.
2511
2512 """
2513 try:
2514 run('sudo -n true')
2515 return False
2516 except error.CmdError:
2517 logging.warn('sudo command requires password.')
2518 return True
2519
2520
2521def is_in_container():
2522 """Check if the process is running inside a container.
2523
2524 @return: True if the process is running inside a container, otherwise False.
2525 """
2526 result = run('grep -q "/lxc/" /proc/1/cgroup',
2527 verbose=False, ignore_status=True)
Dan Shi30a90de2017-04-24 12:46:19 -07002528 if result.exit_status == 0:
2529 return True
2530
2531 # Check "container" environment variable for lxd/lxc containers.
Dan Shi8e312ec2017-04-24 14:23:36 -07002532 if os.environ.get('container') == 'lxc':
Dan Shi30a90de2017-04-24 12:46:19 -07002533 return True
2534
2535 return False
Allen Li5ed7e632017-02-03 16:31:33 -08002536
2537
2538def is_flash_installed():
2539 """
2540 The Adobe Flash binary is only distributed with internal builds.
2541 """
2542 return (os.path.exists('/opt/google/chrome/pepper/libpepflashplayer.so')
2543 and os.path.exists('/opt/google/chrome/pepper/pepper-flash.info'))
2544
2545
2546def verify_flash_installed():
2547 """
2548 The Adobe Flash binary is only distributed with internal builds.
2549 Warn users of public builds of the extra dependency.
2550 """
2551 if not is_flash_installed():
2552 raise error.TestNAError('No Adobe Flash binary installed.')
2553
2554
2555def is_in_same_subnet(ip_1, ip_2, mask_bits=24):
2556 """Check if two IP addresses are in the same subnet with given mask bits.
2557
2558 The two IP addresses are string of IPv4, e.g., '192.168.0.3'.
2559
2560 @param ip_1: First IP address to compare.
2561 @param ip_2: Second IP address to compare.
2562 @param mask_bits: Number of mask bits for subnet comparison. Default to 24.
2563
2564 @return: True if the two IP addresses are in the same subnet.
2565
2566 """
2567 mask = ((2L<<mask_bits-1) -1)<<(32-mask_bits)
2568 ip_1_num = struct.unpack('!I', socket.inet_aton(ip_1))[0]
2569 ip_2_num = struct.unpack('!I', socket.inet_aton(ip_2))[0]
2570 return ip_1_num & mask == ip_2_num & mask
2571
2572
Vinayak Suleyd5e648f2019-05-22 20:06:24 -07002573def get_ip_address(hostname=None):
2574 """Get the IP address of given hostname or current machine.
Allen Li5ed7e632017-02-03 16:31:33 -08002575
Vinayak Suleyd5e648f2019-05-22 20:06:24 -07002576 @param hostname: Hostname of a DUT, default value is None.
Allen Li5ed7e632017-02-03 16:31:33 -08002577
Vinayak Suleyd5e648f2019-05-22 20:06:24 -07002578 @return: The IP address of given hostname. If hostname is not given then
2579 we'll try to query the IP address of the current machine and
2580 return.
Allen Li5ed7e632017-02-03 16:31:33 -08002581 """
Vinayak Suleyd5e648f2019-05-22 20:06:24 -07002582 if hostname:
2583 try:
Allen Li5ed7e632017-02-03 16:31:33 -08002584 return socket.gethostbyname(hostname)
Vinayak Suleyd5e648f2019-05-22 20:06:24 -07002585 except socket.gaierror as e:
2586 logging.error(
2587 'Failed to get IP address of %s, error: %s.', hostname, e)
2588 else:
2589 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
2590 s.connect(("8.8.8.8", 80))
2591 ip = s.getsockname()[0]
2592 s.close()
2593 return ip
Allen Li5ed7e632017-02-03 16:31:33 -08002594
2595
2596def get_servers_in_same_subnet(host_ip, mask_bits, servers=None,
2597 server_ip_map=None):
2598 """Get the servers in the same subnet of the given host ip.
2599
2600 @param host_ip: The IP address of a dut to look for devserver.
2601 @param mask_bits: Number of mask bits.
2602 @param servers: A list of servers to be filtered by subnet specified by
2603 host_ip and mask_bits.
2604 @param server_ip_map: A map between the server name and its IP address.
2605 The map can be pre-built for better performance, e.g., when
2606 allocating a drone for an agent task.
2607
2608 @return: A list of servers in the same subnet of the given host ip.
2609
2610 """
2611 matched_servers = []
2612 if not servers and not server_ip_map:
2613 raise ValueError('Either `servers` or `server_ip_map` must be given.')
2614 if not servers:
2615 servers = server_ip_map.keys()
2616 # Make sure server_ip_map is an empty dict if it's not set.
2617 if not server_ip_map:
2618 server_ip_map = {}
2619 for server in servers:
2620 server_ip = server_ip_map.get(server, get_ip_address(server))
2621 if server_ip and is_in_same_subnet(server_ip, host_ip, mask_bits):
2622 matched_servers.append(server)
2623 return matched_servers
2624
2625
Kirtika Ruchandanif798f5e2018-07-08 18:34:20 -07002626def get_restricted_subnet(hostname, restricted_subnets=None):
Allen Li5ed7e632017-02-03 16:31:33 -08002627 """Get the restricted subnet of given hostname.
2628
2629 @param hostname: Name of the host to look for matched restricted subnet.
2630 @param restricted_subnets: A list of restricted subnets, default is set to
2631 RESTRICTED_SUBNETS.
2632
2633 @return: A tuple of (subnet_ip, mask_bits), which defines a restricted
2634 subnet.
2635 """
Kirtika Ruchandanif798f5e2018-07-08 18:34:20 -07002636 if restricted_subnets is None:
2637 restricted_subnets=RESTRICTED_SUBNETS
Allen Li5ed7e632017-02-03 16:31:33 -08002638 host_ip = get_ip_address(hostname)
2639 if not host_ip:
2640 return
2641 for subnet_ip, mask_bits in restricted_subnets:
2642 if is_in_same_subnet(subnet_ip, host_ip, mask_bits):
2643 return subnet_ip, mask_bits
2644
2645
2646def get_wireless_ssid(hostname):
2647 """Get the wireless ssid based on given hostname.
2648
2649 The method tries to locate the wireless ssid in the same subnet of given
2650 hostname first. If none is found, it returns the default setting in
2651 CLIENT/wireless_ssid.
2652
2653 @param hostname: Hostname of the test device.
2654
2655 @return: wireless ssid for the test device.
2656 """
2657 default_ssid = CONFIG.get_config_value('CLIENT', 'wireless_ssid',
2658 default=None)
2659 host_ip = get_ip_address(hostname)
2660 if not host_ip:
2661 return default_ssid
2662
2663 # Get all wireless ssid in the global config.
2664 ssids = CONFIG.get_config_value_regex('CLIENT', WIRELESS_SSID_PATTERN)
2665
2666 # There could be multiple subnet matches, pick the one with most strict
2667 # match, i.e., the one with highest maskbit.
2668 matched_ssid = default_ssid
2669 matched_maskbit = -1
2670 for key, value in ssids.items():
2671 # The config key filtered by regex WIRELESS_SSID_PATTERN has a format of
2672 # wireless_ssid_[subnet_ip]/[maskbit], for example:
2673 # wireless_ssid_192.168.0.1/24
2674 # Following line extract the subnet ip and mask bit from the key name.
2675 match = re.match(WIRELESS_SSID_PATTERN, key)
2676 subnet_ip, maskbit = match.groups()
2677 maskbit = int(maskbit)
2678 if (is_in_same_subnet(subnet_ip, host_ip, maskbit) and
2679 maskbit > matched_maskbit):
2680 matched_ssid = value
2681 matched_maskbit = maskbit
2682 return matched_ssid
2683
2684
2685def parse_launch_control_build(build_name):
2686 """Get branch, target, build_id from the given Launch Control build_name.
2687
2688 @param build_name: Name of a Launch Control build, should be formated as
2689 branch/target/build_id
2690
2691 @return: Tuple of branch, target, build_id
2692 @raise ValueError: If the build_name is not correctly formated.
2693 """
2694 branch, target, build_id = build_name.split('/')
2695 return branch, target, build_id
2696
2697
2698def parse_android_target(target):
2699 """Get board and build type from the given target.
2700
2701 @param target: Name of an Android build target, e.g., shamu-eng.
2702
2703 @return: Tuple of board, build_type
2704 @raise ValueError: If the target is not correctly formated.
2705 """
2706 board, build_type = target.split('-')
2707 return board, build_type
2708
2709
2710def parse_launch_control_target(target):
2711 """Parse the build target and type from a Launch Control target.
2712
2713 The Launch Control target has the format of build_target-build_type, e.g.,
2714 shamu-eng or dragonboard-userdebug. This method extracts the build target
2715 and type from the target name.
2716
2717 @param target: Name of a Launch Control target, e.g., shamu-eng.
2718
2719 @return: (build_target, build_type), e.g., ('shamu', 'userdebug')
2720 """
2721 match = re.match('(?P<build_target>.+)-(?P<build_type>[^-]+)', target)
2722 if match:
2723 return match.group('build_target'), match.group('build_type')
2724 else:
2725 return None, None
2726
2727
2728def is_launch_control_build(build):
2729 """Check if a given build is a Launch Control build.
2730
2731 @param build: Name of a build, e.g.,
2732 ChromeOS build: daisy-release/R50-1234.0.0
2733 Launch Control build: git_mnc_release/shamu-eng
2734
2735 @return: True if the build name matches the pattern of a Launch Control
2736 build, False otherwise.
2737 """
2738 try:
2739 _, target, _ = parse_launch_control_build(build)
2740 build_target, _ = parse_launch_control_target(target)
2741 if build_target:
2742 return True
2743 except ValueError:
2744 # parse_launch_control_build or parse_launch_control_target failed.
2745 pass
2746 return False
2747
2748
2749def which(exec_file):
2750 """Finds an executable file.
2751
2752 If the file name contains a path component, it is checked as-is.
2753 Otherwise, we check with each of the path components found in the system
2754 PATH prepended. This behavior is similar to the 'which' command-line tool.
2755
2756 @param exec_file: Name or path to desired executable.
2757
2758 @return: An actual path to the executable, or None if not found.
2759 """
2760 if os.path.dirname(exec_file):
2761 return exec_file if os.access(exec_file, os.X_OK) else None
2762 sys_path = os.environ.get('PATH')
2763 prefix_list = sys_path.split(os.pathsep) if sys_path else []
2764 for prefix in prefix_list:
2765 path = os.path.join(prefix, exec_file)
2766 if os.access(path, os.X_OK):
2767 return path
2768
2769
2770class TimeoutError(error.TestError):
Dean Liao7d51db02018-07-16 17:21:42 +08002771 """Error raised when poll_for_condition() failed to poll within time.
2772
2773 It may embed a reason (either a string or an exception object) so that
2774 the caller of poll_for_condition() can handle failure better.
2775 """
2776
2777 def __init__(self, message=None, reason=None):
2778 """Constructor.
2779
2780 It supports three invocations:
2781 1) TimeoutError()
2782 2) TimeoutError(message): with customized message.
2783 3) TimeoutError(message, reason): with message and reason for timeout.
2784 """
2785 self.reason = reason
2786 if self.reason:
2787 reason_str = 'Reason: ' + repr(self.reason)
2788 if message:
2789 message += '. ' + reason_str
2790 else:
2791 message = reason_str
2792
2793 if message:
2794 super(TimeoutError, self).__init__(message)
2795 else:
2796 super(TimeoutError, self).__init__()
2797
2798
2799class Timer(object):
2800 """A synchronous timer to evaluate if timout is reached.
2801
2802 Usage:
2803 timer = Timer(timeout_sec)
2804 while timer.sleep(sleep_interval):
2805 # do something...
2806 """
2807 def __init__(self, timeout):
2808 """Constructor.
2809
2810 Note that timer won't start until next() is called.
2811
2812 @param timeout: timer timeout in seconds.
2813 """
2814 self.timeout = timeout
2815 self.deadline = 0
2816
2817 def sleep(self, interval):
2818 """Checks if it has sufficient time to sleep; sleeps if so.
2819
2820 It blocks for |interval| seconds if it has time to sleep.
2821 If timer is not ticked yet, kicks it off and returns True without
2822 sleep.
2823
2824 @param interval: sleep interval in seconds.
2825 @return True if it has sleeped or just kicked off the timer. False
2826 otherwise.
2827 """
2828 now = time.time()
2829 if not self.deadline:
2830 self.deadline = now + self.timeout
2831 return True
2832 if now + interval < self.deadline:
2833 time.sleep(interval)
2834 return True
2835 return False
Allen Li5ed7e632017-02-03 16:31:33 -08002836
2837
2838def poll_for_condition(condition,
2839 exception=None,
2840 timeout=10,
2841 sleep_interval=0.1,
2842 desc=None):
Zachary Marcusfe67b652018-04-11 15:22:53 -07002843 """Polls until a condition is evaluated to true.
Allen Li5ed7e632017-02-03 16:31:33 -08002844
Zachary Marcusfe67b652018-04-11 15:22:53 -07002845 @param condition: function taking no args and returning anything that will
2846 evaluate to True in a conditional check
2847 @param exception: exception to throw if condition doesn't evaluate to true
Allen Li5ed7e632017-02-03 16:31:33 -08002848 @param timeout: maximum number of seconds to wait
2849 @param sleep_interval: time to sleep between polls
2850 @param desc: description of default TimeoutError used if 'exception' is
2851 None
2852
Zachary Marcusfe67b652018-04-11 15:22:53 -07002853 @return The evaluated value that caused the poll loop to terminate.
Allen Li5ed7e632017-02-03 16:31:33 -08002854
2855 @raise 'exception' arg if supplied; TimeoutError otherwise
2856 """
2857 start_time = time.time()
2858 while True:
2859 value = condition()
2860 if value:
2861 return value
2862 if time.time() + sleep_interval - start_time > timeout:
2863 if exception:
Xixuan Wue3dfe002017-09-26 10:48:00 -07002864 logging.error('Will raise error %r due to unexpected return: '
2865 '%r', exception, value)
Kirtika Ruchandani2af5b2d2018-08-25 14:39:54 -07002866 raise exception # pylint: disable=raising-bad-type
Allen Li5ed7e632017-02-03 16:31:33 -08002867
2868 if desc:
2869 desc = 'Timed out waiting for condition: ' + desc
2870 else:
2871 desc = 'Timed out waiting for unnamed condition'
2872 logging.error(desc)
Dean Liao7d51db02018-07-16 17:21:42 +08002873 raise TimeoutError(message=desc)
Allen Li5ed7e632017-02-03 16:31:33 -08002874
2875 time.sleep(sleep_interval)
2876
2877
Dean Liao7d51db02018-07-16 17:21:42 +08002878def poll_for_condition_ex(condition, timeout=10, sleep_interval=0.1, desc=None):
2879 """Polls until a condition is evaluated to true or until timeout.
2880
2881 Similiar to poll_for_condition, except that it handles exceptions
2882 condition() raises. If timeout is not reached, the exception is dropped and
2883 poll for condition after a sleep; otherwise, the exception is embedded into
2884 TimeoutError to raise.
2885
2886 @param condition: function taking no args and returning anything that will
2887 evaluate to True in a conditional check
2888 @param timeout: maximum number of seconds to wait
2889 @param sleep_interval: time to sleep between polls
2890 @param desc: description of the condition
2891
2892 @return The evaluated value that caused the poll loop to terminate.
2893
2894 @raise TimeoutError. If condition() raised exception, it is embedded in
2895 raised TimeoutError.
2896 """
2897 timer = Timer(timeout)
2898 while timer.sleep(sleep_interval):
2899 reason = None
2900 try:
2901 value = condition()
2902 if value:
2903 return value
2904 except BaseException as e:
2905 reason = e
2906
2907 if desc is None:
2908 desc = 'unamed condition'
2909 if reason is None:
2910 reason = 'condition evaluted as false'
2911 to_raise = TimeoutError(message='Timed out waiting for ' + desc,
2912 reason=reason)
2913 logging.error(str(to_raise))
2914 raise to_raise
2915
2916
Shijin Abraham923c70e2020-02-05 14:31:55 -08002917def poll_till_condition_holds(condition,
2918 exception=None,
2919 timeout=10,
2920 sleep_interval=0.1,
2921 hold_interval=5,
2922 desc=None):
2923 """Polls until a condition is evaluated to true for a period of time
2924
2925 This function checks that a condition remains true for the 'hold_interval'
2926 seconds after it first becomes true. If the condition becomes false
2927 subsequently, the timer is reset. This function will not detect if
2928 condition becomes false for any period of time less than the sleep_interval.
2929
2930 @param condition: function taking no args and returning anything that will
2931 evaluate to True in a conditional check
2932 @param exception: exception to throw if condition doesn't evaluate to true
2933 @param timeout: maximum number of seconds to wait
2934 @param sleep_interval: time to sleep between polls
2935 @param hold_interval: time period for which the condition should hold true
2936 @param desc: description of default TimeoutError used if 'exception' is
2937 None
2938
2939 @return The evaluated value that caused the poll loop to terminate.
2940
2941 @raise 'exception' arg if supplied; TimeoutError otherwise
2942 """
2943 start_time = time.time()
2944 cond_is_held = False
2945 cond_hold_start_time = None
2946
2947 while True:
2948 value = condition()
2949 if value:
2950 if cond_is_held:
2951 if time.time() - cond_hold_start_time > hold_interval:
2952 return value
2953 else:
2954 cond_is_held = True
2955 cond_hold_start_time = time.time()
2956 else:
2957 cond_is_held = False
2958
2959 time_remaining = timeout - (time.time() - start_time)
2960 if time_remaining < hold_interval:
2961 if exception:
2962 logging.error('Will raise error %r due to unexpected return: '
2963 '%r', exception, value)
2964 raise exception # pylint: disable=raising-bad-type
2965
2966 if desc:
2967 desc = 'Timed out waiting for condition: ' + desc
2968 else:
2969 desc = 'Timed out waiting for unnamed condition'
2970 logging.error(desc)
2971 raise TimeoutError(message=desc)
2972
2973 time.sleep(sleep_interval)
2974
2975
Ruben Zakariane88b4e92019-04-16 10:29:51 -07002976def shadowroot_query(element, action):
2977 """Recursively queries shadowRoot.
2978
2979 @param element: element to query for.
2980 @param action: action to be performed on the element.
2981
2982 @return JS functions to execute.
2983
2984 """
2985 # /deep/ CSS query has been removed from ShadowDOM. The only way to access
2986 # elements now is to recursively query in each shadowRoot.
2987 shadowroot_script = """
2988 function deepQuerySelectorAll(root, targetQuery) {
2989 const elems = Array.prototype.slice.call(
2990 root.querySelectorAll(targetQuery[0]));
2991 const remaining = targetQuery.slice(1);
2992 if (remaining.length === 0) {
2993 return elems;
2994 }
2995
2996 let res = [];
2997 for (let i = 0; i < elems.length; i++) {
2998 if (elems[i].shadowRoot) {
2999 res = res.concat(
3000 deepQuerySelectorAll(elems[i].shadowRoot, remaining));
3001 }
3002 }
3003 return res;
3004 };
3005 var testing_element = deepQuerySelectorAll(document, %s);
3006 testing_element[0].%s;
3007 """
3008 script_to_execute = shadowroot_script % (element, action)
3009 return script_to_execute
3010
3011
Zachary Marcus4e4cd7b2018-05-24 15:07:16 -07003012def threaded_return(function):
3013 """
3014 Decorator to add to a function to get that function to return a thread
3015 object, but with the added benefit of storing its return value.
3016
3017 @param function: function object to be run in the thread
3018
3019 @return a threading.Thread object, that has already been started, is
3020 recording its result, and can be completed and its result
3021 fetched by calling .finish()
3022 """
3023 def wrapped_t(queue, *args, **kwargs):
3024 """
3025 Calls the decorated function as normal, but appends the output into
3026 the passed-in threadsafe queue.
3027 """
3028 ret = function(*args, **kwargs)
3029 queue.put(ret)
3030
3031 def wrapped_finish(threaded_object):
3032 """
3033 Provides a utility to this thread object, getting its result while
3034 simultaneously joining the thread.
3035 """
3036 ret = threaded_object.get()
3037 threaded_object.join()
3038 return ret
3039
3040 def wrapper(*args, **kwargs):
3041 """
3042 Creates the queue and starts the thread, then assigns extra attributes
3043 to the thread to give it result-storing capability.
3044 """
3045 q = Queue.Queue()
3046 t = threading.Thread(target=wrapped_t, args=(q,) + args, kwargs=kwargs)
3047 t.start()
3048 t.result_queue = q
3049 t.get = t.result_queue.get
3050 t.finish = lambda: wrapped_finish(t)
3051 return t
3052
3053 # for the decorator
3054 return wrapper
3055
3056
3057@threaded_return
3058def background_sample_until_condition(
3059 function,
3060 condition=lambda: True,
3061 timeout=10,
3062 sleep_interval=1):
3063 """
3064 Records the value of the function until the condition is False or the
3065 timeout is reached. Runs as a background thread, so it's nonblocking.
3066 Usage might look something like:
3067
3068 def function():
3069 return get_value()
3070 def condition():
3071 return self._keep_sampling
3072
3073 # main thread
3074 sample_thread = utils.background_sample_until_condition(
3075 function=function,condition=condition)
3076 # do other work
3077 # ...
3078 self._keep_sampling = False
3079 # blocking call to get result and join the thread
3080 result = sample_thread.finish()
3081
3082 @param function: function object, 0 args, to be continually polled
3083 @param condition: function object, 0 args, to say when to stop polling
3084 @param timeout: maximum number of seconds to wait
3085 @param number of seconds to wait in between polls
3086
3087 @return a thread object that has already been started and is running in
3088 the background, whose run must be stopped with .finish(), which
3089 also returns a list of the results from the sample function
3090 """
3091 log = []
3092
3093 end_time = datetime.datetime.now() + datetime.timedelta(
3094 seconds = timeout + sleep_interval)
3095
3096 while condition() and datetime.datetime.now() < end_time:
3097 log.append(function())
3098 time.sleep(sleep_interval)
3099 return log
3100
3101
Aviv Keshet98b179e2017-07-18 16:22:23 -07003102class metrics_mock(metrics_mock_class.mock_class_base):
Allen Li5ed7e632017-02-03 16:31:33 -08003103 """mock class for metrics in case chromite is not installed."""
3104 pass
Luis Hector Chavez5473ee32018-05-15 10:12:50 -07003105
3106
3107MountInfo = collections.namedtuple('MountInfo', ['root', 'mount_point', 'tags'])
3108
3109
3110def get_mount_info(process='self', mount_point=None):
3111 """Retrieves information about currently mounted file systems.
3112
3113 @param mount_point: (optional) The mount point (a path). If this is
3114 provided, only information about the given mount point
3115 is returned. If this is omitted, info about all mount
3116 points is returned.
3117 @param process: (optional) The process id (or the string 'self') of the
3118 process whose mountinfo will be obtained. If this is
3119 omitted, info about the current process is returned.
3120
3121 @return A generator yielding one MountInfo object for each relevant mount
3122 found in /proc/PID/mountinfo.
3123 """
3124 with open('/proc/{}/mountinfo'.format(process)) as f:
3125 for line in f.readlines():
3126 # These lines are formatted according to the proc(5) manpage.
3127 # Sample line:
3128 # 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root \
3129 # rw,errors=continue
3130 # Fields (descriptions omitted for fields we don't care about)
3131 # 3: the root of the mount.
3132 # 4: the mount point.
3133 # 5: mount options.
3134 # 6: tags. There can be more than one of these. This is where
3135 # shared mounts are indicated.
3136 # 7: a dash separator marking the end of the tags.
3137 mountinfo = line.split()
3138 if mount_point is None or mountinfo[4] == mount_point:
3139 tags = []
3140 for field in mountinfo[6:]:
3141 if field == '-':
3142 break
3143 tags.append(field.split(':')[0])
3144 yield MountInfo(root = mountinfo[3],
3145 mount_point = mountinfo[4],
3146 tags = tags)
Kuo Jen Weice9f5562018-11-02 12:34:55 +08003147
3148
3149# Appended suffix for chart tablet naming convention in test lab
3150CHART_ADDRESS_SUFFIX = '-tablet'
3151
3152
3153def get_lab_chart_address(hostname):
3154 """Convert lab DUT hostname to address of camera box chart tablet"""
3155 return hostname + CHART_ADDRESS_SUFFIX if is_in_container() else None