blob: de724edfbf9504225b96ed0cba969e656d567035 [file] [log] [blame]
Dan Shia1ecd5c2013-06-06 11:21:31 -07001# Copyright (c) 2013 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
Dan Shia1ecd5c2013-06-06 11:21:31 -07005
Paul Hobbs20cc72a2016-08-30 16:57:05 -07006import contextlib
Fang Deng18699fe2015-12-04 16:40:27 -08007import grp
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -08008import httplib
9import json
Alex Millerdadc2c22013-07-08 15:21:21 -070010import logging
MK Ryu35d661e2014-09-25 17:44:10 -070011import os
beeps023afc62014-02-04 16:59:22 -080012import random
Alex Millerdadc2c22013-07-08 15:21:21 -070013import re
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080014import time
Paul Drewsbef578d2013-09-24 15:10:36 -070015import urllib2
Alex Millerdadc2c22013-07-08 15:21:21 -070016
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080017import common
Dan Shief31f032016-05-13 15:51:39 -070018from autotest_lib.client.common_lib import utils
beeps023afc62014-02-04 16:59:22 -080019from autotest_lib.client.common_lib import error
20from autotest_lib.client.common_lib import global_config
MK Ryu0c1a37d2015-04-30 12:00:55 -070021from autotest_lib.client.common_lib import host_queue_entry_states
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -070022from autotest_lib.client.common_lib import host_states
Simran Basi7756a0b2016-03-16 13:10:07 -070023from autotest_lib.server.cros import provision
Dan Shia1ecd5c2013-06-06 11:21:31 -070024from autotest_lib.server.cros.dynamic_suite import constants
Dan Shi7e04fa82013-07-25 15:08:48 -070025from autotest_lib.server.cros.dynamic_suite import job_status
Dan Shia1ecd5c2013-06-06 11:21:31 -070026
Dan Shi82997b92015-05-06 12:08:02 -070027try:
28 from chromite.lib import cros_build_lib
Paul Hobbs20cc72a2016-08-30 16:57:05 -070029 from chromite.lib import ts_mon_config
Dan Shi82997b92015-05-06 12:08:02 -070030except ImportError:
Paul Hobbs20cc72a2016-08-30 16:57:05 -070031 logging.warn('Unable to import chromite. Monarch is disabled.')
Dan Shi82997b92015-05-06 12:08:02 -070032 # Init the module variable to None. Access to this module can check if it
33 # is not None before making calls.
34 cros_build_lib = None
Paul Hobbs20cc72a2016-08-30 16:57:05 -070035 ts_mon_config = None
Dan Shi82997b92015-05-06 12:08:02 -070036
Dan Shia1ecd5c2013-06-06 11:21:31 -070037
Dan Shid37736b2016-07-06 15:10:29 -070038CONFIG = global_config.global_config
39
40_SHERIFF_JS = CONFIG.get_config_value('NOTIFICATIONS', 'sheriffs', default='')
41_LAB_SHERIFF_JS = CONFIG.get_config_value(
42 'NOTIFICATIONS', 'lab_sheriffs', default='')
43_CHROMIUM_BUILD_URL = CONFIG.get_config_value(
44 'NOTIFICATIONS', 'chromium_build_url', default='')
Alex Millerdadc2c22013-07-08 15:21:21 -070045
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080046LAB_GOOD_STATES = ('open', 'throttled')
47
Dan Shid37736b2016-07-06 15:10:29 -070048ENABLE_DRONE_IN_RESTRICTED_SUBNET = CONFIG.get_config_value(
49 'CROS', 'enable_drone_in_restricted_subnet', type=bool,
50 default=False)
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080051
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -070052# Wait at most 10 mins for duts to go idle.
53IDLE_DUT_WAIT_TIMEOUT = 600
54
J. Richard Barnetteabbe0962013-12-10 18:15:44 -080055class TestLabException(Exception):
56 """Exception raised when the Test Lab blocks a test or suite."""
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080057 pass
58
59
60class ParseBuildNameException(Exception):
61 """Raised when ParseBuildName() cannot parse a build name."""
62 pass
63
64
Fang Dengf08814a2015-08-03 18:12:18 +000065class Singleton(type):
66 """Enforce that only one client class is instantiated per process."""
67 _instances = {}
68
69 def __call__(cls, *args, **kwargs):
70 """Fetch the instance of a class to use for subsequent calls."""
71 if cls not in cls._instances:
72 cls._instances[cls] = super(Singleton, cls).__call__(
73 *args, **kwargs)
74 return cls._instances[cls]
75
Kevin Cheng05ae2a42016-06-06 10:12:48 -070076class EmptyAFEHost(object):
77 """Object to represent an AFE host object when there is no AFE."""
78
79 def __init__(self):
80 """
81 We'll be setting the instance attributes as we use them. Right now
82 we only use attributes and labels but as time goes by and other
83 attributes are used from an actual AFE Host object (check
84 rpc_interfaces.get_hosts()), we'll add them in here so users won't be
85 perplexed why their host's afe_host object complains that attribute
86 doesn't exist.
87 """
88 self.attributes = {}
89 self.labels = []
90
Fang Dengf08814a2015-08-03 18:12:18 +000091
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080092def ParseBuildName(name):
93 """Format a build name, given board, type, milestone, and manifest num.
94
Simran Basib7d21162014-05-21 15:26:16 -070095 @param name: a build name, e.g. 'x86-alex-release/R20-2015.0.0' or a
96 relative build name, e.g. 'x86-alex-release/LATEST'
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080097
98 @return board: board the manifest is for, e.g. x86-alex.
99 @return type: one of 'release', 'factory', or 'firmware'
100 @return milestone: (numeric) milestone the manifest was associated with.
Simran Basib7d21162014-05-21 15:26:16 -0700101 Will be None for relative build names.
102 @return manifest: manifest number, e.g. '2015.0.0'.
103 Will be None for relative build names.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800104
105 """
Dan Shie02810d2016-08-25 09:44:57 -0700106 match = re.match(r'(trybot-)?(?P<board>[\w-]+?)(?:-chrome)?(?:-chromium)?'
107 r'-(?P<type>\w+)/(R(?P<milestone>\d+)-'
108 r'(?P<manifest>[\d.ab-]+)|LATEST)',
Simran Basif8f648e2014-09-09 11:40:03 -0700109 name)
110 if match and len(match.groups()) >= 5:
Simran Basib7d21162014-05-21 15:26:16 -0700111 return (match.group('board'), match.group('type'),
112 match.group('milestone'), match.group('manifest'))
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800113 raise ParseBuildNameException('%s is a malformed build name.' % name)
114
Alex Millerdadc2c22013-07-08 15:21:21 -0700115
Dan Shi3d7a0e12015-10-12 11:55:45 -0700116def get_labels_from_afe(hostname, label_prefix, afe):
117 """Retrieve a host's specific labels from the AFE.
118
119 Looks for the host labels that have the form <label_prefix>:<value>
120 and returns the "<value>" part of the label. None is returned
121 if there is not a label matching the pattern
122
123 @param hostname: hostname of given DUT.
124 @param label_prefix: prefix of label to be matched, e.g., |board:|
125 @param afe: afe instance.
126
127 @returns A list of labels that match the prefix or 'None'
128
129 """
130 labels = afe.get_labels(name__startswith=label_prefix,
131 host__hostname__in=[hostname])
132 if labels:
133 return [l.name.split(label_prefix, 1)[1] for l in labels]
134
135
Dan Shia1ecd5c2013-06-06 11:21:31 -0700136def get_label_from_afe(hostname, label_prefix, afe):
137 """Retrieve a host's specific label from the AFE.
138
139 Looks for a host label that has the form <label_prefix>:<value>
140 and returns the "<value>" part of the label. None is returned
141 if there is not a label matching the pattern
142
143 @param hostname: hostname of given DUT.
144 @param label_prefix: prefix of label to be matched, e.g., |board:|
145 @param afe: afe instance.
146 @returns the label that matches the prefix or 'None'
147
148 """
Dan Shi3d7a0e12015-10-12 11:55:45 -0700149 labels = get_labels_from_afe(hostname, label_prefix, afe)
Dan Shia1ecd5c2013-06-06 11:21:31 -0700150 if labels and len(labels) == 1:
Dan Shi3d7a0e12015-10-12 11:55:45 -0700151 return labels[0]
Dan Shia1ecd5c2013-06-06 11:21:31 -0700152
153
154def get_board_from_afe(hostname, afe):
155 """Retrieve given host's board from its labels in the AFE.
156
157 Looks for a host label of the form "board:<board>", and
158 returns the "<board>" part of the label. `None` is returned
159 if there is not a single, unique label matching the pattern.
160
161 @param hostname: hostname of given DUT.
162 @param afe: afe instance.
163 @returns board from label, or `None`.
164
165 """
166 return get_label_from_afe(hostname, constants.BOARD_PREFIX, afe)
167
168
169def get_build_from_afe(hostname, afe):
170 """Retrieve the current build for given host from the AFE.
171
172 Looks through the host's labels in the AFE to determine its build.
173
174 @param hostname: hostname of given DUT.
175 @param afe: afe instance.
176 @returns The current build or None if it could not find it or if there
177 were multiple build labels assigned to this host.
178
179 """
Simran Basi7756a0b2016-03-16 13:10:07 -0700180 for prefix in [provision.CROS_VERSION_PREFIX,
181 provision.ANDROID_BUILD_VERSION_PREFIX]:
182 build = get_label_from_afe(hostname, prefix + ':', afe)
183 if build:
184 return build
185 return None
Dan Shia1ecd5c2013-06-06 11:21:31 -0700186
187
Allen Li6a612392016-08-18 12:09:32 -0700188# TODO(fdeng): fix get_sheriffs crbug.com/483254
Fang Deng3197b392013-06-26 11:42:02 -0700189def get_sheriffs(lab_only=False):
Alex Millerdadc2c22013-07-08 15:21:21 -0700190 """
191 Polls the javascript file that holds the identity of the sheriff and
192 parses it's output to return a list of chromium sheriff email addresses.
193 The javascript file can contain the ldap of more than one sheriff, eg:
194 document.write('sheriff_one, sheriff_two').
195
Fang Deng3197b392013-06-26 11:42:02 -0700196 @param lab_only: if True, only pulls lab sheriff.
197 @return: A list of chroium.org sheriff email addresses to cc on the bug.
198 An empty list if failed to parse the javascript.
Alex Millerdadc2c22013-07-08 15:21:21 -0700199 """
200 sheriff_ids = []
Fang Deng3197b392013-06-26 11:42:02 -0700201 sheriff_js_list = _LAB_SHERIFF_JS.split(',')
202 if not lab_only:
203 sheriff_js_list.extend(_SHERIFF_JS.split(','))
204
205 for sheriff_js in sheriff_js_list:
Alex Millerdadc2c22013-07-08 15:21:21 -0700206 try:
Dan Shief31f032016-05-13 15:51:39 -0700207 url_content = utils.urlopen('%s%s'% (
Alex Millerdadc2c22013-07-08 15:21:21 -0700208 _CHROMIUM_BUILD_URL, sheriff_js)).read()
209 except (ValueError, IOError) as e:
beeps4efdf032013-09-17 11:27:14 -0700210 logging.warning('could not parse sheriff from url %s%s: %s',
211 _CHROMIUM_BUILD_URL, sheriff_js, str(e))
Paul Drewsbef578d2013-09-24 15:10:36 -0700212 except (urllib2.URLError, httplib.HTTPException) as e:
213 logging.warning('unexpected error reading from url "%s%s": %s',
214 _CHROMIUM_BUILD_URL, sheriff_js, str(e))
Alex Millerdadc2c22013-07-08 15:21:21 -0700215 else:
216 ldaps = re.search(r"document.write\('(.*)'\)", url_content)
217 if not ldaps:
beeps4efdf032013-09-17 11:27:14 -0700218 logging.warning('Could not retrieve sheriff ldaps for: %s',
219 url_content)
Alex Millerdadc2c22013-07-08 15:21:21 -0700220 continue
221 sheriff_ids += ['%s@chromium.org' % alias.replace(' ', '')
222 for alias in ldaps.group(1).split(',')]
223 return sheriff_ids
beeps46dadc92013-11-07 14:07:10 -0800224
225
226def remote_wget(source_url, dest_path, ssh_cmd):
227 """wget source_url from localhost to dest_path on remote host using ssh.
228
229 @param source_url: The complete url of the source of the package to send.
230 @param dest_path: The path on the remote host's file system where we would
231 like to store the package.
232 @param ssh_cmd: The ssh command to use in performing the remote wget.
233 """
234 wget_cmd = ("wget -O - %s | %s 'cat >%s'" %
235 (source_url, ssh_cmd, dest_path))
Dan Shief31f032016-05-13 15:51:39 -0700236 utils.run(wget_cmd)
beeps46dadc92013-11-07 14:07:10 -0800237
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800238
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800239_MAX_LAB_STATUS_ATTEMPTS = 5
240def _get_lab_status(status_url):
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800241 """Grabs the current lab status and message.
242
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800243 @returns The JSON object obtained from the given URL.
244
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800245 """
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800246 retry_waittime = 1
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800247 for _ in range(_MAX_LAB_STATUS_ATTEMPTS):
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800248 try:
249 response = urllib2.urlopen(status_url)
250 except IOError as e:
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800251 logging.debug('Error occurred when grabbing the lab status: %s.',
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800252 e)
253 time.sleep(retry_waittime)
254 continue
255 # Check for successful response code.
256 if response.getcode() == 200:
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800257 return json.load(response)
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800258 time.sleep(retry_waittime)
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800259 return None
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800260
261
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800262def _decode_lab_status(lab_status, build):
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800263 """Decode lab status, and report exceptions as needed.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800264
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800265 Take a deserialized JSON object from the lab status page, and
266 interpret it to determine the actual lab status. Raise
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800267 exceptions as required to report when the lab is down.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800268
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800269 @param build: build name that we want to check the status of.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800270
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800271 @raises TestLabException Raised if a request to test for the given
272 status and build should be blocked.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800273 """
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800274 # First check if the lab is up.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800275 if not lab_status['general_state'] in LAB_GOOD_STATES:
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800276 raise TestLabException('Chromium OS Test Lab is closed: '
277 '%s.' % lab_status['message'])
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800278
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800279 # Check if the build we wish to use is disabled.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800280 # Lab messages should be in the format of:
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800281 # Lab is 'status' [regex ...] (comment)
282 # If the build name matches any regex, it will be blocked.
283 build_exceptions = re.search('\[(.*)\]', lab_status['message'])
Prashanth Balasubramanianae437212014-10-27 11:17:26 -0700284 if not build_exceptions or not build:
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800285 return
286 for build_pattern in build_exceptions.group(1).split():
J. Richard Barnette7f215d32015-06-19 12:44:38 -0700287 if re.match(build_pattern, build):
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800288 raise TestLabException('Chromium OS Test Lab is closed: '
289 '%s matches %s.' % (
290 build, build_pattern))
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800291 return
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800292
293
Dan Shi94234cb2014-05-23 20:04:31 -0700294def is_in_lab():
295 """Check if current Autotest instance is in lab
296
297 @return: True if the Autotest instance is in lab.
298 """
Dan Shid37736b2016-07-06 15:10:29 -0700299 test_server_name = CONFIG.get_config_value('SERVER', 'hostname')
Dan Shi94234cb2014-05-23 20:04:31 -0700300 return test_server_name.startswith('cautotest')
301
302
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800303def check_lab_status(build):
304 """Check if the lab status allows us to schedule for a build.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800305
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800306 Checks if the lab is down, or if testing for the requested build
307 should be blocked.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800308
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800309 @param build: Name of the build to be scheduled for testing.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800310
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800311 @raises TestLabException Raised if a request to test for the given
312 status and build should be blocked.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800313
314 """
315 # Ensure we are trying to schedule on the actual lab.
Dan Shi94234cb2014-05-23 20:04:31 -0700316 if not is_in_lab():
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800317 return
318
319 # Download the lab status from its home on the web.
Dan Shid37736b2016-07-06 15:10:29 -0700320 status_url = CONFIG.get_config_value('CROS', 'lab_status_url')
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800321 json_status = _get_lab_status(status_url)
322 if json_status is None:
323 # We go ahead and say the lab is open if we can't get the status.
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700324 logging.warning('Could not get a status from %s', status_url)
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800325 return
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800326 _decode_lab_status(json_status, build)
beeps023afc62014-02-04 16:59:22 -0800327
328
329def lock_host_with_labels(afe, lock_manager, labels):
330 """Lookup and lock one host that matches the list of input labels.
331
332 @param afe: An instance of the afe class, as defined in server.frontend.
333 @param lock_manager: A lock manager capable of locking hosts, eg the
334 one defined in server.cros.host_lock_manager.
335 @param labels: A list of labels to look for on hosts.
336
337 @return: The hostname of a host matching all labels, and locked through the
338 lock_manager. The hostname will be as specified in the database the afe
339 object is associated with, i.e if it exists in afe_hosts with a .cros
340 suffix, the hostname returned will contain a .cros suffix.
341
342 @raises: error.NoEligibleHostException: If no hosts matching the list of
343 input labels are available.
344 @raises: error.TestError: If unable to lock a host matching the labels.
345 """
346 potential_hosts = afe.get_hosts(multiple_labels=labels)
347 if not potential_hosts:
348 raise error.NoEligibleHostException(
349 'No devices found with labels %s.' % labels)
350
351 # This prevents errors where a fault might seem repeatable
352 # because we lock, say, the same packet capturer for each test run.
353 random.shuffle(potential_hosts)
354 for host in potential_hosts:
355 if lock_manager.lock([host.hostname]):
356 logging.info('Locked device %s with labels %s.',
357 host.hostname, labels)
358 return host.hostname
359 else:
360 logging.info('Unable to lock device %s with labels %s.',
361 host.hostname, labels)
362
363 raise error.TestError('Could not lock a device with labels %s' % labels)
Dan Shi7e04fa82013-07-25 15:08:48 -0700364
365
366def get_test_views_from_tko(suite_job_id, tko):
367 """Get test name and result for given suite job ID.
368
369 @param suite_job_id: ID of suite job.
370 @param tko: an instance of TKO as defined in server/frontend.py.
371 @return: A dictionary of test status keyed by test name, e.g.,
372 {'dummy_Fail.Error': 'ERROR', 'dummy_Fail.NAError': 'TEST_NA'}
373 @raise: Exception when there is no test view found.
374
375 """
376 views = tko.run('get_detailed_test_views', afe_job_id=suite_job_id)
377 relevant_views = filter(job_status.view_is_relevant, views)
378 if not relevant_views:
379 raise Exception('Failed to retrieve job results.')
380
381 test_views = {}
382 for view in relevant_views:
383 test_views[view['test_name']] = view['status']
384
385 return test_views
MK Ryu35d661e2014-09-25 17:44:10 -0700386
387
MK Ryuc9c0c3f2014-10-27 14:36:01 -0700388def get_data_key(prefix, suite, build, board):
389 """
390 Constructs a key string from parameters.
391
392 @param prefix: Prefix for the generating key.
393 @param suite: a suite name. e.g., bvt-cq, bvt-inline, dummy
394 @param build: The build string. This string should have a consistent
395 format eg: x86-mario-release/R26-3570.0.0. If the format of this
396 string changes such that we can't determine build_type or branch
397 we give up and use the parametes we're sure of instead (suite,
398 board). eg:
399 1. build = x86-alex-pgo-release/R26-3570.0.0
400 branch = 26
401 build_type = pgo-release
402 2. build = lumpy-paladin/R28-3993.0.0-rc5
403 branch = 28
404 build_type = paladin
405 @param board: The board that this suite ran on.
406 @return: The key string used for a dictionary.
407 """
408 try:
409 _board, build_type, branch = ParseBuildName(build)[:3]
410 except ParseBuildNameException as e:
411 logging.error(str(e))
412 branch = 'Unknown'
413 build_type = 'Unknown'
414 else:
415 embedded_str = re.search(r'x86-\w+-(.*)', _board)
416 if embedded_str:
417 build_type = embedded_str.group(1) + '-' + build_type
418
419 data_key_dict = {
420 'prefix': prefix,
421 'board': board,
422 'branch': branch,
423 'build_type': build_type,
424 'suite': suite,
425 }
426 return ('%(prefix)s.%(board)s.%(build_type)s.%(branch)s.%(suite)s'
427 % data_key_dict)
MK Ryu83184352014-12-10 14:59:40 -0800428
429
MK Ryu2d0a3642015-01-07 15:11:19 -0800430def setup_logging(logfile=None, prefix=False):
MK Ryu83184352014-12-10 14:59:40 -0800431 """Setup basic logging with all logging info stripped.
432
433 Calls to logging will only show the message. No severity is logged.
434
435 @param logfile: If specified dump output to a file as well.
MK Ryu2d0a3642015-01-07 15:11:19 -0800436 @param prefix: Flag for log prefix. Set to True to add prefix to log
437 entries to include timestamp and log level. Default is False.
MK Ryu83184352014-12-10 14:59:40 -0800438 """
439 # Remove all existing handlers. client/common_lib/logging_config adds
440 # a StreamHandler to logger when modules are imported, e.g.,
441 # autotest_lib.client.bin.utils. A new StreamHandler will be added here to
442 # log only messages, not severity.
443 logging.getLogger().handlers = []
444
MK Ryu2d0a3642015-01-07 15:11:19 -0800445 if prefix:
446 log_format = '%(asctime)s %(levelname)-5s| %(message)s'
447 else:
448 log_format = '%(message)s'
449
MK Ryu83184352014-12-10 14:59:40 -0800450 screen_handler = logging.StreamHandler()
MK Ryu2d0a3642015-01-07 15:11:19 -0800451 screen_handler.setFormatter(logging.Formatter(log_format))
MK Ryu83184352014-12-10 14:59:40 -0800452 logging.getLogger().addHandler(screen_handler)
453 logging.getLogger().setLevel(logging.INFO)
454 if logfile:
455 file_handler = logging.FileHandler(logfile)
MK Ryu2d0a3642015-01-07 15:11:19 -0800456 file_handler.setFormatter(logging.Formatter(log_format))
MK Ryu83184352014-12-10 14:59:40 -0800457 file_handler.setLevel(logging.DEBUG)
458 logging.getLogger().addHandler(file_handler)
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -0800459
460
461def is_shard():
462 """Determines if this instance is running as a shard.
463
464 Reads the global_config value shard_hostname in the section SHARD.
465
466 @return True, if shard_hostname is set, False otherwise.
467 """
Dan Shid37736b2016-07-06 15:10:29 -0700468 hostname = CONFIG.get_config_value('SHARD', 'shard_hostname', default=None)
MK Ryu0c1a37d2015-04-30 12:00:55 -0700469 return bool(hostname)
470
471
Fang Deng0cb2a3b2015-12-10 17:59:00 -0800472def get_global_afe_hostname():
473 """Read the hostname of the global AFE from the global configuration."""
Dan Shid37736b2016-07-06 15:10:29 -0700474 return CONFIG.get_config_value('SERVER', 'global_afe_hostname')
Fang Deng0cb2a3b2015-12-10 17:59:00 -0800475
476
Fang Deng18699fe2015-12-04 16:40:27 -0800477def is_restricted_user(username):
478 """Determines if a user is in a restricted group.
479
480 User in restricted group only have access to master.
481
482 @param username: A string, representing a username.
483
484 @returns: True if the user is in a restricted group.
485 """
486 if not username:
487 return False
488
Dan Shid37736b2016-07-06 15:10:29 -0700489 restricted_groups = CONFIG.get_config_value(
Fang Deng18699fe2015-12-04 16:40:27 -0800490 'AUTOTEST_WEB', 'restricted_groups', default='').split(',')
491 for group in restricted_groups:
Fang Deng5229c852016-02-09 13:30:31 -0800492 try:
493 if group and username in grp.getgrnam(group).gr_mem:
494 return True
495 except KeyError as e:
496 logging.debug("%s is not a valid group.", group)
Fang Deng18699fe2015-12-04 16:40:27 -0800497 return False
498
499
MK Ryu0c1a37d2015-04-30 12:00:55 -0700500def get_special_task_status(is_complete, success, is_active):
501 """Get the status of a special task.
502
503 Emulate a host queue entry status for a special task
504 Although SpecialTasks are not HostQueueEntries, it is helpful to
505 the user to present similar statuses.
506
507 @param is_complete Boolean if the task is completed.
508 @param success Boolean if the task succeeded.
509 @param is_active Boolean if the task is active.
510
511 @return The status of a special task.
512 """
513 if is_complete:
514 if success:
515 return host_queue_entry_states.Status.COMPLETED
516 return host_queue_entry_states.Status.FAILED
517 if is_active:
518 return host_queue_entry_states.Status.RUNNING
519 return host_queue_entry_states.Status.QUEUED
520
521
522def get_special_task_exec_path(hostname, task_id, task_name, time_requested):
523 """Get the execution path of the SpecialTask.
524
525 This method returns different paths depending on where a
526 the task ran:
527 * Master: hosts/hostname/task_id-task_type
528 * Shard: Master_path/time_created
529 This is to work around the fact that a shard can fail independent
530 of the master, and be replaced by another shard that has the same
531 hosts. Without the time_created stamp the logs of the tasks running
532 on the second shard will clobber the logs from the first in google
533 storage, because task ids are not globally unique.
534
535 @param hostname Hostname
536 @param task_id Special task id
537 @param task_name Special task name (e.g., Verify, Repair, etc)
538 @param time_requested Special task requested time.
539
540 @return An execution path for the task.
541 """
542 results_path = 'hosts/%s/%s-%s' % (hostname, task_id, task_name.lower())
543
544 # If we do this on the master it will break backward compatibility,
545 # as there are tasks that currently don't have timestamps. If a host
546 # or job has been sent to a shard, the rpc for that host/job will
547 # be redirected to the shard, so this global_config check will happen
548 # on the shard the logs are on.
549 if not is_shard():
550 return results_path
551
552 # Generate a uid to disambiguate special task result directories
553 # in case this shard fails. The simplest uid is the job_id, however
554 # in rare cases tasks do not have jobs associated with them (eg:
555 # frontend verify), so just use the creation timestamp. The clocks
556 # between a shard and master should always be in sync. Any discrepancies
557 # will be brought to our attention in the form of job timeouts.
558 uid = time_requested.strftime('%Y%d%m%H%M%S')
559
560 # TODO: This is a hack, however it is the easiest way to achieve
561 # correctness. There is currently some debate over the future of
562 # tasks in our infrastructure and refactoring everything right
563 # now isn't worth the time.
564 return '%s/%s' % (results_path, uid)
565
566
567def get_job_tag(id, owner):
568 """Returns a string tag for a job.
569
570 @param id Job id
571 @param owner Job owner
572
573 """
574 return '%s-%s' % (id, owner)
575
576
577def get_hqe_exec_path(tag, execution_subdir):
578 """Returns a execution path to a HQE's results.
579
580 @param tag Tag string for a job associated with a HQE.
581 @param execution_subdir Execution sub-directory string of a HQE.
582
583 """
584 return os.path.join(tag, execution_subdir)
Dan Shi82997b92015-05-06 12:08:02 -0700585
586
587def is_inside_chroot():
588 """Check if the process is running inside chroot.
589
590 This is a wrapper around chromite.lib.cros_build_lib.IsInsideChroot(). The
591 method checks if cros_build_lib can be imported first.
592
593 @return: True if the process is running inside chroot or cros_build_lib
594 cannot be imported.
595
596 """
J. Richard Barnette7f215d32015-06-19 12:44:38 -0700597 return not cros_build_lib or cros_build_lib.IsInsideChroot()
Dan Shi70647ca2015-07-16 22:52:35 -0700598
599
600def parse_job_name(name):
601 """Parse job name to get information including build, board and suite etc.
602
603 Suite job created by run_suite follows the naming convention of:
604 [build]-test_suites/control.[suite]
605 For example: lumpy-release/R46-7272.0.0-test_suites/control.bvt
606 The naming convention is defined in site_rpc_interface.create_suite_job.
607
608 Test job created by suite job follows the naming convention of:
609 [build]/[suite]/[test name]
610 For example: lumpy-release/R46-7272.0.0/bvt/login_LoginSuccess
611 The naming convention is defined in
612 server/cros/dynamic_suite/tools.create_job_name
613
614 Note that pgo and chrome-perf builds will fail the method. Since lab does
615 not run test for these builds, they can be ignored.
Dan Shief31f032016-05-13 15:51:39 -0700616 Also, tests for Launch Control builds have different naming convention.
617 The build ID will be used as build_version.
Dan Shi70647ca2015-07-16 22:52:35 -0700618
619 @param name: Name of the job.
620
621 @return: A dictionary containing the test information. The keyvals include:
622 build: Name of the build, e.g., lumpy-release/R46-7272.0.0
623 build_version: The version of the build, e.g., R46-7272.0.0
624 board: Name of the board, e.g., lumpy
625 suite: Name of the test suite, e.g., bvt
626
627 """
628 info = {}
Dan Shief31f032016-05-13 15:51:39 -0700629 suite_job_regex = '([^/]*/[^/]*(?:/\d+)?)-test_suites/control\.(.*)'
630 test_job_regex = '([^/]*/[^/]*(?:/\d+)?)/([^/]+)/.*'
Dan Shi70647ca2015-07-16 22:52:35 -0700631 match = re.match(suite_job_regex, name)
632 if not match:
633 match = re.match(test_job_regex, name)
634 if match:
635 info['build'] = match.groups()[0]
636 info['suite'] = match.groups()[1]
637 info['build_version'] = info['build'].split('/')[1]
638 try:
639 info['board'], _, _, _ = ParseBuildName(info['build'])
640 except ParseBuildNameException:
Dan Shief31f032016-05-13 15:51:39 -0700641 # Try to parse it as Launch Control build
642 # Launch Control builds have name format:
643 # branch/build_target-build_type/build_id.
644 try:
645 _, target, build_id = utils.parse_launch_control_build(
646 info['build'])
647 build_target, _ = utils.parse_launch_control_target(target)
648 if build_target:
649 info['board'] = build_target
650 info['build_version'] = build_id
651 except ValueError:
652 pass
Dan Shi70647ca2015-07-16 22:52:35 -0700653 return info
Kevin Cheng3a4a57a2015-09-30 12:09:50 -0700654
655
656def add_label_detector(label_function_list, label_list=None, label=None):
657 """Decorator used to group functions together into the provided list.
658
659 This is a helper function to automatically add label functions that have
660 the label decorator. This is to help populate the class list of label
661 functions to be retrieved by the get_labels class method.
662
663 @param label_function_list: List of label detecting functions to add
664 decorated function to.
665 @param label_list: List of detectable labels to add detectable labels to.
666 (Default: None)
667 @param label: Label string that is detectable by this detection function
668 (Default: None)
669 """
670 def add_func(func):
671 """
672 @param func: The function to be added as a detector.
673 """
674 label_function_list.append(func)
675 if label and label_list is not None:
676 label_list.append(label)
677 return func
678 return add_func
Simran Basi9f364a62015-12-07 14:15:19 -0800679
680
681def verify_not_root_user():
682 """Simple function to error out if running with uid == 0"""
683 if os.getuid() == 0:
Simran Basi1bf60eb2015-12-01 16:39:29 -0800684 raise error.IllegalUser('This script can not be ran as root.')
685
686
687def get_hostname_from_machine(machine):
688 """Lookup hostname from a machine string or dict.
689
690 @returns: Machine hostname in string format.
691 """
692 hostname, _ = get_host_info_from_machine(machine)
693 return hostname
694
695
696def get_host_info_from_machine(machine):
697 """Lookup host information from a machine string or dict.
698
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700699 @returns: Tuple of (hostname, afe_host)
Simran Basi1bf60eb2015-12-01 16:39:29 -0800700 """
701 if isinstance(machine, dict):
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700702 return (machine['hostname'], machine['afe_host'])
Simran Basi1bf60eb2015-12-01 16:39:29 -0800703 else:
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700704 return (machine, EmptyAFEHost())
705
706
707def get_afe_host_from_machine(machine):
708 """Return the afe_host from the machine dict if possible.
709
710 @returns: AFE host object.
711 """
712 _, afe_host = get_host_info_from_machine(machine)
713 return afe_host
Fang Dengf8a94e22015-12-07 13:39:13 -0800714
715
716def get_creds_abspath(creds_file):
717 """Returns the abspath of the credentials file.
718
719 If creds_file is already an absolute path, just return it.
720 Otherwise, assume it is located in the creds directory
721 specified in global_config and return the absolute path.
722
723 @param: creds_path, a path to the credentials.
724 @return: An absolute path to the credentials file.
725 """
726 if not creds_file:
727 return None
728 if os.path.isabs(creds_file):
729 return creds_file
Dan Shid37736b2016-07-06 15:10:29 -0700730 creds_dir = CONFIG.get_config_value('SERVER', 'creds_dir', default='')
Fang Dengf8a94e22015-12-07 13:39:13 -0800731 if not creds_dir or not os.path.exists(creds_dir):
732 creds_dir = common.autotest_dir
733 return os.path.join(creds_dir, creds_file)
Kevin Cheng3b111812015-12-15 11:52:08 -0800734
735
736def machine_is_testbed(machine):
737 """Checks if the machine is a testbed.
738
739 The signal we use to determine if the machine is a testbed
740 is if the host attributes contain more than 1 serial.
741
742 @param machine: is a list of dicts
743
744 @return: True if the machine is a testbed, False otherwise.
745 """
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700746 _, afe_host = get_host_info_from_machine(machine)
747 return len(afe_host.attributes.get('serials', '').split(',')) > 1
Paul Hobbs20cc72a2016-08-30 16:57:05 -0700748
749
750def SetupTsMonGlobalState(*args, **kwargs):
751 """Import-safe wrap around chromite.lib.ts_mon_config's setup function.
752
753 @param *args: Args to pass through.
754 @param **kwargs: Kwargs to pass through.
755 """
756 if ts_mon_config:
Paul Hobbs604fc872016-09-29 16:41:55 -0700757 try:
758 context = ts_mon_config.SetupTsMonGlobalState(*args, **kwargs)
759 if hasattr(context, '__exit__'):
760 return context
761 except Exception as e:
762 logging.warning('Caught an exception trying to setup ts_mon, '
763 'monitoring is disabled: %s', e, exc_info=True)
764 return TrivialContextManager()
Paul Hobbs20cc72a2016-08-30 16:57:05 -0700765 else:
Paul Hobbs930ed9d2016-09-28 15:59:08 -0700766 return TrivialContextManager()
Paul Hobbs20cc72a2016-08-30 16:57:05 -0700767
768
769@contextlib.contextmanager
Paul Hobbs604fc872016-09-29 16:41:55 -0700770def TrivialContextManager(*args, **kwargs):
771 """Context manager that does nothing.
772
773 @param *args: Ignored args
774 @param **kwargs: Ignored kwargs.
775 """
Paul Hobbs20cc72a2016-08-30 16:57:05 -0700776 yield
Kevin Cheng5f2ba6c2016-09-28 10:20:05 -0700777
778
779def wait_for_idle_duts(duts, afe, max_wait=IDLE_DUT_WAIT_TIMEOUT):
780 """Wait for the hosts to all go idle.
781
782 @param duts: List of duts to check for idle state.
783 @param afe: afe instance.
784 @param max_wait: Max wait time in seconds.
785
786 @returns Boolean True if all hosts are idle or False if any hosts did not
787 go idle within max_wait.
788 """
789 start_time = time.time()
790 # We make a shallow copy since we're going to be modifying active_dut_list.
791 active_dut_list = duts[:]
792 while active_dut_list:
793 # Let's rate-limit how often we hit the AFE.
794 time.sleep(1)
795
796 # Check if we've waited too long.
797 if (time.time() - start_time) > max_wait:
798 return False
799
800 idle_duts = []
801 # Get the status for the duts and see if they're in the idle state.
802 afe_hosts = afe.get_hosts(active_dut_list)
803 idle_duts = [afe_host.hostname for afe_host in afe_hosts
804 if afe_host.status in host_states.IDLE_STATES]
805
806 # Take out idle duts so we don't needlessly check them
807 # next time around.
808 for idle_dut in idle_duts:
809 active_dut_list.remove(idle_dut)
810
811 logging.info('still waiting for following duts to go idle: %s',
812 active_dut_list)
813 return True
814
815
816@contextlib.contextmanager
817def lock_duts_and_wait(duts, afe, lock_msg='default lock message',
818 max_wait=IDLE_DUT_WAIT_TIMEOUT):
819 """Context manager to lock the duts and wait for them to go idle.
820
821 @param duts: List of duts to lock.
822 @param afe: afe instance.
823
824 @returns Boolean lock_success where True if all duts locked successfully or
825 False if we timed out waiting too long for hosts to go idle.
826 """
827 try:
828 locked_duts = []
829 duts.sort()
830 for dut in duts:
831 if afe.lock_host(dut, lock_msg, fail_if_locked=True):
832 locked_duts.append(dut)
833 else:
834 logging.info('%s already locked', dut)
835 yield wait_for_idle_duts(locked_duts, afe, max_wait)
836 finally:
837 afe.unlock_hosts(locked_duts)
Dan Shib5b8b4f2016-11-02 14:04:02 -0700838
839
840def board_labels_allowed(boards):
841 """Check if the list of board labels can be set to a single host.
842
843 The only case multiple board labels can be set to a single host is for
844 testbed, which may have a list of board labels like
845 board:angler-1, board:angler-2, board:angler-3, board:marlin-1'
846
847 @param boards: A list of board labels (may include platform label).
848
849 @returns True if the the list of boards can be set to a single host.
850 """
851 # Filter out any non-board labels
852 boards = [b for b in boards if re.match('board:.*', b)]
853 if len(boards) <= 1:
854 return True
855 for board in boards:
856 if not re.match('board:[^-]+-\d+', board):
857 return False
858 return True