blob: da556bb0bc83de34330e8d9feec307b8b9b99668 [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
Fang Deng18699fe2015-12-04 16:40:27 -08006import grp
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -08007import httplib
8import json
Alex Millerdadc2c22013-07-08 15:21:21 -07009import logging
MK Ryu35d661e2014-09-25 17:44:10 -070010import os
beeps023afc62014-02-04 16:59:22 -080011import random
Alex Millerdadc2c22013-07-08 15:21:21 -070012import re
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080013import time
Paul Drewsbef578d2013-09-24 15:10:36 -070014import urllib2
Alex Millerdadc2c22013-07-08 15:21:21 -070015
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080016import common
Dan Shief31f032016-05-13 15:51:39 -070017from autotest_lib.client.common_lib import utils
beeps023afc62014-02-04 16:59:22 -080018from autotest_lib.client.common_lib import error
19from autotest_lib.client.common_lib import global_config
MK Ryu0c1a37d2015-04-30 12:00:55 -070020from autotest_lib.client.common_lib import host_queue_entry_states
Simran Basi7756a0b2016-03-16 13:10:07 -070021from autotest_lib.server.cros import provision
Dan Shia1ecd5c2013-06-06 11:21:31 -070022from autotest_lib.server.cros.dynamic_suite import constants
Dan Shi7e04fa82013-07-25 15:08:48 -070023from autotest_lib.server.cros.dynamic_suite import job_status
Dan Shia1ecd5c2013-06-06 11:21:31 -070024
Dan Shi82997b92015-05-06 12:08:02 -070025try:
26 from chromite.lib import cros_build_lib
27except ImportError:
28 logging.warn('Unable to import chromite.')
29 # Init the module variable to None. Access to this module can check if it
30 # is not None before making calls.
31 cros_build_lib = None
32
Dan Shia1ecd5c2013-06-06 11:21:31 -070033
Dan Shid37736b2016-07-06 15:10:29 -070034CONFIG = global_config.global_config
35
36_SHERIFF_JS = CONFIG.get_config_value('NOTIFICATIONS', 'sheriffs', default='')
37_LAB_SHERIFF_JS = CONFIG.get_config_value(
38 'NOTIFICATIONS', 'lab_sheriffs', default='')
39_CHROMIUM_BUILD_URL = CONFIG.get_config_value(
40 'NOTIFICATIONS', 'chromium_build_url', default='')
Alex Millerdadc2c22013-07-08 15:21:21 -070041
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080042LAB_GOOD_STATES = ('open', 'throttled')
43
Dan Shid37736b2016-07-06 15:10:29 -070044ENABLE_DRONE_IN_RESTRICTED_SUBNET = CONFIG.get_config_value(
45 'CROS', 'enable_drone_in_restricted_subnet', type=bool,
46 default=False)
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080047
J. Richard Barnetteabbe0962013-12-10 18:15:44 -080048class TestLabException(Exception):
49 """Exception raised when the Test Lab blocks a test or suite."""
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080050 pass
51
52
53class ParseBuildNameException(Exception):
54 """Raised when ParseBuildName() cannot parse a build name."""
55 pass
56
57
Fang Dengf08814a2015-08-03 18:12:18 +000058class Singleton(type):
59 """Enforce that only one client class is instantiated per process."""
60 _instances = {}
61
62 def __call__(cls, *args, **kwargs):
63 """Fetch the instance of a class to use for subsequent calls."""
64 if cls not in cls._instances:
65 cls._instances[cls] = super(Singleton, cls).__call__(
66 *args, **kwargs)
67 return cls._instances[cls]
68
Kevin Cheng05ae2a42016-06-06 10:12:48 -070069class EmptyAFEHost(object):
70 """Object to represent an AFE host object when there is no AFE."""
71
72 def __init__(self):
73 """
74 We'll be setting the instance attributes as we use them. Right now
75 we only use attributes and labels but as time goes by and other
76 attributes are used from an actual AFE Host object (check
77 rpc_interfaces.get_hosts()), we'll add them in here so users won't be
78 perplexed why their host's afe_host object complains that attribute
79 doesn't exist.
80 """
81 self.attributes = {}
82 self.labels = []
83
Fang Dengf08814a2015-08-03 18:12:18 +000084
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080085def ParseBuildName(name):
86 """Format a build name, given board, type, milestone, and manifest num.
87
Simran Basib7d21162014-05-21 15:26:16 -070088 @param name: a build name, e.g. 'x86-alex-release/R20-2015.0.0' or a
89 relative build name, e.g. 'x86-alex-release/LATEST'
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080090
91 @return board: board the manifest is for, e.g. x86-alex.
92 @return type: one of 'release', 'factory', or 'firmware'
93 @return milestone: (numeric) milestone the manifest was associated with.
Simran Basib7d21162014-05-21 15:26:16 -070094 Will be None for relative build names.
95 @return manifest: manifest number, e.g. '2015.0.0'.
96 Will be None for relative build names.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080097
98 """
Simran Basif8f648e2014-09-09 11:40:03 -070099 match = re.match(r'(trybot-)?(?P<board>[\w-]+)-(?P<type>\w+)/'
100 r'(R(?P<milestone>\d+)-(?P<manifest>[\d.ab-]+)|LATEST)',
101 name)
102 if match and len(match.groups()) >= 5:
Simran Basib7d21162014-05-21 15:26:16 -0700103 return (match.group('board'), match.group('type'),
104 match.group('milestone'), match.group('manifest'))
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800105 raise ParseBuildNameException('%s is a malformed build name.' % name)
106
Alex Millerdadc2c22013-07-08 15:21:21 -0700107
Dan Shi3d7a0e12015-10-12 11:55:45 -0700108def get_labels_from_afe(hostname, label_prefix, afe):
109 """Retrieve a host's specific labels from the AFE.
110
111 Looks for the host labels that have the form <label_prefix>:<value>
112 and returns the "<value>" part of the label. None is returned
113 if there is not a label matching the pattern
114
115 @param hostname: hostname of given DUT.
116 @param label_prefix: prefix of label to be matched, e.g., |board:|
117 @param afe: afe instance.
118
119 @returns A list of labels that match the prefix or 'None'
120
121 """
122 labels = afe.get_labels(name__startswith=label_prefix,
123 host__hostname__in=[hostname])
124 if labels:
125 return [l.name.split(label_prefix, 1)[1] for l in labels]
126
127
Dan Shia1ecd5c2013-06-06 11:21:31 -0700128def get_label_from_afe(hostname, label_prefix, afe):
129 """Retrieve a host's specific label from the AFE.
130
131 Looks for a host label that has the form <label_prefix>:<value>
132 and returns the "<value>" part of the label. None is returned
133 if there is not a label matching the pattern
134
135 @param hostname: hostname of given DUT.
136 @param label_prefix: prefix of label to be matched, e.g., |board:|
137 @param afe: afe instance.
138 @returns the label that matches the prefix or 'None'
139
140 """
Dan Shi3d7a0e12015-10-12 11:55:45 -0700141 labels = get_labels_from_afe(hostname, label_prefix, afe)
Dan Shia1ecd5c2013-06-06 11:21:31 -0700142 if labels and len(labels) == 1:
Dan Shi3d7a0e12015-10-12 11:55:45 -0700143 return labels[0]
Dan Shia1ecd5c2013-06-06 11:21:31 -0700144
145
146def get_board_from_afe(hostname, afe):
147 """Retrieve given host's board from its labels in the AFE.
148
149 Looks for a host label of the form "board:<board>", and
150 returns the "<board>" part of the label. `None` is returned
151 if there is not a single, unique label matching the pattern.
152
153 @param hostname: hostname of given DUT.
154 @param afe: afe instance.
155 @returns board from label, or `None`.
156
157 """
158 return get_label_from_afe(hostname, constants.BOARD_PREFIX, afe)
159
160
161def get_build_from_afe(hostname, afe):
162 """Retrieve the current build for given host from the AFE.
163
164 Looks through the host's labels in the AFE to determine its build.
165
166 @param hostname: hostname of given DUT.
167 @param afe: afe instance.
168 @returns The current build or None if it could not find it or if there
169 were multiple build labels assigned to this host.
170
171 """
Simran Basi7756a0b2016-03-16 13:10:07 -0700172 for prefix in [provision.CROS_VERSION_PREFIX,
173 provision.ANDROID_BUILD_VERSION_PREFIX]:
174 build = get_label_from_afe(hostname, prefix + ':', afe)
175 if build:
176 return build
177 return None
Dan Shia1ecd5c2013-06-06 11:21:31 -0700178
179
Fang Deng3197b392013-06-26 11:42:02 -0700180def get_sheriffs(lab_only=False):
Alex Millerdadc2c22013-07-08 15:21:21 -0700181 """
182 Polls the javascript file that holds the identity of the sheriff and
183 parses it's output to return a list of chromium sheriff email addresses.
184 The javascript file can contain the ldap of more than one sheriff, eg:
185 document.write('sheriff_one, sheriff_two').
186
Fang Deng3197b392013-06-26 11:42:02 -0700187 @param lab_only: if True, only pulls lab sheriff.
188 @return: A list of chroium.org sheriff email addresses to cc on the bug.
189 An empty list if failed to parse the javascript.
Alex Millerdadc2c22013-07-08 15:21:21 -0700190 """
191 sheriff_ids = []
Fang Deng3197b392013-06-26 11:42:02 -0700192 sheriff_js_list = _LAB_SHERIFF_JS.split(',')
193 if not lab_only:
194 sheriff_js_list.extend(_SHERIFF_JS.split(','))
195
196 for sheriff_js in sheriff_js_list:
Alex Millerdadc2c22013-07-08 15:21:21 -0700197 try:
Dan Shief31f032016-05-13 15:51:39 -0700198 url_content = utils.urlopen('%s%s'% (
Alex Millerdadc2c22013-07-08 15:21:21 -0700199 _CHROMIUM_BUILD_URL, sheriff_js)).read()
200 except (ValueError, IOError) as e:
beeps4efdf032013-09-17 11:27:14 -0700201 logging.warning('could not parse sheriff from url %s%s: %s',
202 _CHROMIUM_BUILD_URL, sheriff_js, str(e))
Paul Drewsbef578d2013-09-24 15:10:36 -0700203 except (urllib2.URLError, httplib.HTTPException) as e:
204 logging.warning('unexpected error reading from url "%s%s": %s',
205 _CHROMIUM_BUILD_URL, sheriff_js, str(e))
Alex Millerdadc2c22013-07-08 15:21:21 -0700206 else:
207 ldaps = re.search(r"document.write\('(.*)'\)", url_content)
208 if not ldaps:
beeps4efdf032013-09-17 11:27:14 -0700209 logging.warning('Could not retrieve sheriff ldaps for: %s',
210 url_content)
Alex Millerdadc2c22013-07-08 15:21:21 -0700211 continue
212 sheriff_ids += ['%s@chromium.org' % alias.replace(' ', '')
213 for alias in ldaps.group(1).split(',')]
214 return sheriff_ids
beeps46dadc92013-11-07 14:07:10 -0800215
216
217def remote_wget(source_url, dest_path, ssh_cmd):
218 """wget source_url from localhost to dest_path on remote host using ssh.
219
220 @param source_url: The complete url of the source of the package to send.
221 @param dest_path: The path on the remote host's file system where we would
222 like to store the package.
223 @param ssh_cmd: The ssh command to use in performing the remote wget.
224 """
225 wget_cmd = ("wget -O - %s | %s 'cat >%s'" %
226 (source_url, ssh_cmd, dest_path))
Dan Shief31f032016-05-13 15:51:39 -0700227 utils.run(wget_cmd)
beeps46dadc92013-11-07 14:07:10 -0800228
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800229
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800230_MAX_LAB_STATUS_ATTEMPTS = 5
231def _get_lab_status(status_url):
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800232 """Grabs the current lab status and message.
233
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800234 @returns The JSON object obtained from the given URL.
235
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800236 """
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800237 retry_waittime = 1
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800238 for _ in range(_MAX_LAB_STATUS_ATTEMPTS):
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800239 try:
240 response = urllib2.urlopen(status_url)
241 except IOError as e:
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800242 logging.debug('Error occurred when grabbing the lab status: %s.',
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800243 e)
244 time.sleep(retry_waittime)
245 continue
246 # Check for successful response code.
247 if response.getcode() == 200:
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800248 return json.load(response)
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800249 time.sleep(retry_waittime)
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800250 return None
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800251
252
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800253def _decode_lab_status(lab_status, build):
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800254 """Decode lab status, and report exceptions as needed.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800255
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800256 Take a deserialized JSON object from the lab status page, and
257 interpret it to determine the actual lab status. Raise
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800258 exceptions as required to report when the lab is down.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800259
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800260 @param build: build name that we want to check the status of.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800261
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800262 @raises TestLabException Raised if a request to test for the given
263 status and build should be blocked.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800264 """
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800265 # First check if the lab is up.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800266 if not lab_status['general_state'] in LAB_GOOD_STATES:
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800267 raise TestLabException('Chromium OS Test Lab is closed: '
268 '%s.' % lab_status['message'])
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800269
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800270 # Check if the build we wish to use is disabled.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800271 # Lab messages should be in the format of:
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800272 # Lab is 'status' [regex ...] (comment)
273 # If the build name matches any regex, it will be blocked.
274 build_exceptions = re.search('\[(.*)\]', lab_status['message'])
Prashanth Balasubramanianae437212014-10-27 11:17:26 -0700275 if not build_exceptions or not build:
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800276 return
277 for build_pattern in build_exceptions.group(1).split():
J. Richard Barnette7f215d32015-06-19 12:44:38 -0700278 if re.match(build_pattern, build):
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800279 raise TestLabException('Chromium OS Test Lab is closed: '
280 '%s matches %s.' % (
281 build, build_pattern))
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800282 return
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800283
284
Dan Shi94234cb2014-05-23 20:04:31 -0700285def is_in_lab():
286 """Check if current Autotest instance is in lab
287
288 @return: True if the Autotest instance is in lab.
289 """
Dan Shid37736b2016-07-06 15:10:29 -0700290 test_server_name = CONFIG.get_config_value('SERVER', 'hostname')
Dan Shi94234cb2014-05-23 20:04:31 -0700291 return test_server_name.startswith('cautotest')
292
293
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800294def check_lab_status(build):
295 """Check if the lab status allows us to schedule for a build.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800296
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800297 Checks if the lab is down, or if testing for the requested build
298 should be blocked.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800299
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800300 @param build: Name of the build to be scheduled for testing.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800301
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800302 @raises TestLabException Raised if a request to test for the given
303 status and build should be blocked.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800304
305 """
306 # Ensure we are trying to schedule on the actual lab.
Dan Shi94234cb2014-05-23 20:04:31 -0700307 if not is_in_lab():
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800308 return
309
310 # Download the lab status from its home on the web.
Dan Shid37736b2016-07-06 15:10:29 -0700311 status_url = CONFIG.get_config_value('CROS', 'lab_status_url')
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800312 json_status = _get_lab_status(status_url)
313 if json_status is None:
314 # We go ahead and say the lab is open if we can't get the status.
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700315 logging.warning('Could not get a status from %s', status_url)
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800316 return
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800317 _decode_lab_status(json_status, build)
beeps023afc62014-02-04 16:59:22 -0800318
319
320def lock_host_with_labels(afe, lock_manager, labels):
321 """Lookup and lock one host that matches the list of input labels.
322
323 @param afe: An instance of the afe class, as defined in server.frontend.
324 @param lock_manager: A lock manager capable of locking hosts, eg the
325 one defined in server.cros.host_lock_manager.
326 @param labels: A list of labels to look for on hosts.
327
328 @return: The hostname of a host matching all labels, and locked through the
329 lock_manager. The hostname will be as specified in the database the afe
330 object is associated with, i.e if it exists in afe_hosts with a .cros
331 suffix, the hostname returned will contain a .cros suffix.
332
333 @raises: error.NoEligibleHostException: If no hosts matching the list of
334 input labels are available.
335 @raises: error.TestError: If unable to lock a host matching the labels.
336 """
337 potential_hosts = afe.get_hosts(multiple_labels=labels)
338 if not potential_hosts:
339 raise error.NoEligibleHostException(
340 'No devices found with labels %s.' % labels)
341
342 # This prevents errors where a fault might seem repeatable
343 # because we lock, say, the same packet capturer for each test run.
344 random.shuffle(potential_hosts)
345 for host in potential_hosts:
346 if lock_manager.lock([host.hostname]):
347 logging.info('Locked device %s with labels %s.',
348 host.hostname, labels)
349 return host.hostname
350 else:
351 logging.info('Unable to lock device %s with labels %s.',
352 host.hostname, labels)
353
354 raise error.TestError('Could not lock a device with labels %s' % labels)
Dan Shi7e04fa82013-07-25 15:08:48 -0700355
356
357def get_test_views_from_tko(suite_job_id, tko):
358 """Get test name and result for given suite job ID.
359
360 @param suite_job_id: ID of suite job.
361 @param tko: an instance of TKO as defined in server/frontend.py.
362 @return: A dictionary of test status keyed by test name, e.g.,
363 {'dummy_Fail.Error': 'ERROR', 'dummy_Fail.NAError': 'TEST_NA'}
364 @raise: Exception when there is no test view found.
365
366 """
367 views = tko.run('get_detailed_test_views', afe_job_id=suite_job_id)
368 relevant_views = filter(job_status.view_is_relevant, views)
369 if not relevant_views:
370 raise Exception('Failed to retrieve job results.')
371
372 test_views = {}
373 for view in relevant_views:
374 test_views[view['test_name']] = view['status']
375
376 return test_views
MK Ryu35d661e2014-09-25 17:44:10 -0700377
378
MK Ryuc9c0c3f2014-10-27 14:36:01 -0700379def get_data_key(prefix, suite, build, board):
380 """
381 Constructs a key string from parameters.
382
383 @param prefix: Prefix for the generating key.
384 @param suite: a suite name. e.g., bvt-cq, bvt-inline, dummy
385 @param build: The build string. This string should have a consistent
386 format eg: x86-mario-release/R26-3570.0.0. If the format of this
387 string changes such that we can't determine build_type or branch
388 we give up and use the parametes we're sure of instead (suite,
389 board). eg:
390 1. build = x86-alex-pgo-release/R26-3570.0.0
391 branch = 26
392 build_type = pgo-release
393 2. build = lumpy-paladin/R28-3993.0.0-rc5
394 branch = 28
395 build_type = paladin
396 @param board: The board that this suite ran on.
397 @return: The key string used for a dictionary.
398 """
399 try:
400 _board, build_type, branch = ParseBuildName(build)[:3]
401 except ParseBuildNameException as e:
402 logging.error(str(e))
403 branch = 'Unknown'
404 build_type = 'Unknown'
405 else:
406 embedded_str = re.search(r'x86-\w+-(.*)', _board)
407 if embedded_str:
408 build_type = embedded_str.group(1) + '-' + build_type
409
410 data_key_dict = {
411 'prefix': prefix,
412 'board': board,
413 'branch': branch,
414 'build_type': build_type,
415 'suite': suite,
416 }
417 return ('%(prefix)s.%(board)s.%(build_type)s.%(branch)s.%(suite)s'
418 % data_key_dict)
MK Ryu83184352014-12-10 14:59:40 -0800419
420
MK Ryu2d0a3642015-01-07 15:11:19 -0800421def setup_logging(logfile=None, prefix=False):
MK Ryu83184352014-12-10 14:59:40 -0800422 """Setup basic logging with all logging info stripped.
423
424 Calls to logging will only show the message. No severity is logged.
425
426 @param logfile: If specified dump output to a file as well.
MK Ryu2d0a3642015-01-07 15:11:19 -0800427 @param prefix: Flag for log prefix. Set to True to add prefix to log
428 entries to include timestamp and log level. Default is False.
MK Ryu83184352014-12-10 14:59:40 -0800429 """
430 # Remove all existing handlers. client/common_lib/logging_config adds
431 # a StreamHandler to logger when modules are imported, e.g.,
432 # autotest_lib.client.bin.utils. A new StreamHandler will be added here to
433 # log only messages, not severity.
434 logging.getLogger().handlers = []
435
MK Ryu2d0a3642015-01-07 15:11:19 -0800436 if prefix:
437 log_format = '%(asctime)s %(levelname)-5s| %(message)s'
438 else:
439 log_format = '%(message)s'
440
MK Ryu83184352014-12-10 14:59:40 -0800441 screen_handler = logging.StreamHandler()
MK Ryu2d0a3642015-01-07 15:11:19 -0800442 screen_handler.setFormatter(logging.Formatter(log_format))
MK Ryu83184352014-12-10 14:59:40 -0800443 logging.getLogger().addHandler(screen_handler)
444 logging.getLogger().setLevel(logging.INFO)
445 if logfile:
446 file_handler = logging.FileHandler(logfile)
MK Ryu2d0a3642015-01-07 15:11:19 -0800447 file_handler.setFormatter(logging.Formatter(log_format))
MK Ryu83184352014-12-10 14:59:40 -0800448 file_handler.setLevel(logging.DEBUG)
449 logging.getLogger().addHandler(file_handler)
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -0800450
451
452def is_shard():
453 """Determines if this instance is running as a shard.
454
455 Reads the global_config value shard_hostname in the section SHARD.
456
457 @return True, if shard_hostname is set, False otherwise.
458 """
Dan Shid37736b2016-07-06 15:10:29 -0700459 hostname = CONFIG.get_config_value('SHARD', 'shard_hostname', default=None)
MK Ryu0c1a37d2015-04-30 12:00:55 -0700460 return bool(hostname)
461
462
Fang Deng0cb2a3b2015-12-10 17:59:00 -0800463def get_global_afe_hostname():
464 """Read the hostname of the global AFE from the global configuration."""
Dan Shid37736b2016-07-06 15:10:29 -0700465 return CONFIG.get_config_value('SERVER', 'global_afe_hostname')
Fang Deng0cb2a3b2015-12-10 17:59:00 -0800466
467
Fang Deng18699fe2015-12-04 16:40:27 -0800468def is_restricted_user(username):
469 """Determines if a user is in a restricted group.
470
471 User in restricted group only have access to master.
472
473 @param username: A string, representing a username.
474
475 @returns: True if the user is in a restricted group.
476 """
477 if not username:
478 return False
479
Dan Shid37736b2016-07-06 15:10:29 -0700480 restricted_groups = CONFIG.get_config_value(
Fang Deng18699fe2015-12-04 16:40:27 -0800481 'AUTOTEST_WEB', 'restricted_groups', default='').split(',')
482 for group in restricted_groups:
Fang Deng5229c852016-02-09 13:30:31 -0800483 try:
484 if group and username in grp.getgrnam(group).gr_mem:
485 return True
486 except KeyError as e:
487 logging.debug("%s is not a valid group.", group)
Fang Deng18699fe2015-12-04 16:40:27 -0800488 return False
489
490
MK Ryu0c1a37d2015-04-30 12:00:55 -0700491def get_special_task_status(is_complete, success, is_active):
492 """Get the status of a special task.
493
494 Emulate a host queue entry status for a special task
495 Although SpecialTasks are not HostQueueEntries, it is helpful to
496 the user to present similar statuses.
497
498 @param is_complete Boolean if the task is completed.
499 @param success Boolean if the task succeeded.
500 @param is_active Boolean if the task is active.
501
502 @return The status of a special task.
503 """
504 if is_complete:
505 if success:
506 return host_queue_entry_states.Status.COMPLETED
507 return host_queue_entry_states.Status.FAILED
508 if is_active:
509 return host_queue_entry_states.Status.RUNNING
510 return host_queue_entry_states.Status.QUEUED
511
512
513def get_special_task_exec_path(hostname, task_id, task_name, time_requested):
514 """Get the execution path of the SpecialTask.
515
516 This method returns different paths depending on where a
517 the task ran:
518 * Master: hosts/hostname/task_id-task_type
519 * Shard: Master_path/time_created
520 This is to work around the fact that a shard can fail independent
521 of the master, and be replaced by another shard that has the same
522 hosts. Without the time_created stamp the logs of the tasks running
523 on the second shard will clobber the logs from the first in google
524 storage, because task ids are not globally unique.
525
526 @param hostname Hostname
527 @param task_id Special task id
528 @param task_name Special task name (e.g., Verify, Repair, etc)
529 @param time_requested Special task requested time.
530
531 @return An execution path for the task.
532 """
533 results_path = 'hosts/%s/%s-%s' % (hostname, task_id, task_name.lower())
534
535 # If we do this on the master it will break backward compatibility,
536 # as there are tasks that currently don't have timestamps. If a host
537 # or job has been sent to a shard, the rpc for that host/job will
538 # be redirected to the shard, so this global_config check will happen
539 # on the shard the logs are on.
540 if not is_shard():
541 return results_path
542
543 # Generate a uid to disambiguate special task result directories
544 # in case this shard fails. The simplest uid is the job_id, however
545 # in rare cases tasks do not have jobs associated with them (eg:
546 # frontend verify), so just use the creation timestamp. The clocks
547 # between a shard and master should always be in sync. Any discrepancies
548 # will be brought to our attention in the form of job timeouts.
549 uid = time_requested.strftime('%Y%d%m%H%M%S')
550
551 # TODO: This is a hack, however it is the easiest way to achieve
552 # correctness. There is currently some debate over the future of
553 # tasks in our infrastructure and refactoring everything right
554 # now isn't worth the time.
555 return '%s/%s' % (results_path, uid)
556
557
558def get_job_tag(id, owner):
559 """Returns a string tag for a job.
560
561 @param id Job id
562 @param owner Job owner
563
564 """
565 return '%s-%s' % (id, owner)
566
567
568def get_hqe_exec_path(tag, execution_subdir):
569 """Returns a execution path to a HQE's results.
570
571 @param tag Tag string for a job associated with a HQE.
572 @param execution_subdir Execution sub-directory string of a HQE.
573
574 """
575 return os.path.join(tag, execution_subdir)
Dan Shi82997b92015-05-06 12:08:02 -0700576
577
578def is_inside_chroot():
579 """Check if the process is running inside chroot.
580
581 This is a wrapper around chromite.lib.cros_build_lib.IsInsideChroot(). The
582 method checks if cros_build_lib can be imported first.
583
584 @return: True if the process is running inside chroot or cros_build_lib
585 cannot be imported.
586
587 """
J. Richard Barnette7f215d32015-06-19 12:44:38 -0700588 return not cros_build_lib or cros_build_lib.IsInsideChroot()
Dan Shi70647ca2015-07-16 22:52:35 -0700589
590
591def parse_job_name(name):
592 """Parse job name to get information including build, board and suite etc.
593
594 Suite job created by run_suite follows the naming convention of:
595 [build]-test_suites/control.[suite]
596 For example: lumpy-release/R46-7272.0.0-test_suites/control.bvt
597 The naming convention is defined in site_rpc_interface.create_suite_job.
598
599 Test job created by suite job follows the naming convention of:
600 [build]/[suite]/[test name]
601 For example: lumpy-release/R46-7272.0.0/bvt/login_LoginSuccess
602 The naming convention is defined in
603 server/cros/dynamic_suite/tools.create_job_name
604
605 Note that pgo and chrome-perf builds will fail the method. Since lab does
606 not run test for these builds, they can be ignored.
Dan Shief31f032016-05-13 15:51:39 -0700607 Also, tests for Launch Control builds have different naming convention.
608 The build ID will be used as build_version.
Dan Shi70647ca2015-07-16 22:52:35 -0700609
610 @param name: Name of the job.
611
612 @return: A dictionary containing the test information. The keyvals include:
613 build: Name of the build, e.g., lumpy-release/R46-7272.0.0
614 build_version: The version of the build, e.g., R46-7272.0.0
615 board: Name of the board, e.g., lumpy
616 suite: Name of the test suite, e.g., bvt
617
618 """
619 info = {}
Dan Shief31f032016-05-13 15:51:39 -0700620 suite_job_regex = '([^/]*/[^/]*(?:/\d+)?)-test_suites/control\.(.*)'
621 test_job_regex = '([^/]*/[^/]*(?:/\d+)?)/([^/]+)/.*'
Dan Shi70647ca2015-07-16 22:52:35 -0700622 match = re.match(suite_job_regex, name)
623 if not match:
624 match = re.match(test_job_regex, name)
625 if match:
626 info['build'] = match.groups()[0]
627 info['suite'] = match.groups()[1]
628 info['build_version'] = info['build'].split('/')[1]
629 try:
630 info['board'], _, _, _ = ParseBuildName(info['build'])
631 except ParseBuildNameException:
Dan Shief31f032016-05-13 15:51:39 -0700632 # Try to parse it as Launch Control build
633 # Launch Control builds have name format:
634 # branch/build_target-build_type/build_id.
635 try:
636 _, target, build_id = utils.parse_launch_control_build(
637 info['build'])
638 build_target, _ = utils.parse_launch_control_target(target)
639 if build_target:
640 info['board'] = build_target
641 info['build_version'] = build_id
642 except ValueError:
643 pass
Dan Shi70647ca2015-07-16 22:52:35 -0700644 return info
Kevin Cheng3a4a57a2015-09-30 12:09:50 -0700645
646
647def add_label_detector(label_function_list, label_list=None, label=None):
648 """Decorator used to group functions together into the provided list.
649
650 This is a helper function to automatically add label functions that have
651 the label decorator. This is to help populate the class list of label
652 functions to be retrieved by the get_labels class method.
653
654 @param label_function_list: List of label detecting functions to add
655 decorated function to.
656 @param label_list: List of detectable labels to add detectable labels to.
657 (Default: None)
658 @param label: Label string that is detectable by this detection function
659 (Default: None)
660 """
661 def add_func(func):
662 """
663 @param func: The function to be added as a detector.
664 """
665 label_function_list.append(func)
666 if label and label_list is not None:
667 label_list.append(label)
668 return func
669 return add_func
Simran Basi9f364a62015-12-07 14:15:19 -0800670
671
672def verify_not_root_user():
673 """Simple function to error out if running with uid == 0"""
674 if os.getuid() == 0:
Simran Basi1bf60eb2015-12-01 16:39:29 -0800675 raise error.IllegalUser('This script can not be ran as root.')
676
677
678def get_hostname_from_machine(machine):
679 """Lookup hostname from a machine string or dict.
680
681 @returns: Machine hostname in string format.
682 """
683 hostname, _ = get_host_info_from_machine(machine)
684 return hostname
685
686
687def get_host_info_from_machine(machine):
688 """Lookup host information from a machine string or dict.
689
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700690 @returns: Tuple of (hostname, afe_host)
Simran Basi1bf60eb2015-12-01 16:39:29 -0800691 """
692 if isinstance(machine, dict):
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700693 return (machine['hostname'], machine['afe_host'])
Simran Basi1bf60eb2015-12-01 16:39:29 -0800694 else:
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700695 return (machine, EmptyAFEHost())
696
697
698def get_afe_host_from_machine(machine):
699 """Return the afe_host from the machine dict if possible.
700
701 @returns: AFE host object.
702 """
703 _, afe_host = get_host_info_from_machine(machine)
704 return afe_host
Fang Dengf8a94e22015-12-07 13:39:13 -0800705
706
707def get_creds_abspath(creds_file):
708 """Returns the abspath of the credentials file.
709
710 If creds_file is already an absolute path, just return it.
711 Otherwise, assume it is located in the creds directory
712 specified in global_config and return the absolute path.
713
714 @param: creds_path, a path to the credentials.
715 @return: An absolute path to the credentials file.
716 """
717 if not creds_file:
718 return None
719 if os.path.isabs(creds_file):
720 return creds_file
Dan Shid37736b2016-07-06 15:10:29 -0700721 creds_dir = CONFIG.get_config_value('SERVER', 'creds_dir', default='')
Fang Dengf8a94e22015-12-07 13:39:13 -0800722 if not creds_dir or not os.path.exists(creds_dir):
723 creds_dir = common.autotest_dir
724 return os.path.join(creds_dir, creds_file)
Kevin Cheng3b111812015-12-15 11:52:08 -0800725
726
727def machine_is_testbed(machine):
728 """Checks if the machine is a testbed.
729
730 The signal we use to determine if the machine is a testbed
731 is if the host attributes contain more than 1 serial.
732
733 @param machine: is a list of dicts
734
735 @return: True if the machine is a testbed, False otherwise.
736 """
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700737 _, afe_host = get_host_info_from_machine(machine)
738 return len(afe_host.attributes.get('serials', '').split(',')) > 1