blob: 2e4a66b8760f4df8651cd2ee3dca328613fb319d [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 """
Dan Shie02810d2016-08-25 09:44:57 -070099 match = re.match(r'(trybot-)?(?P<board>[\w-]+?)(?:-chrome)?(?:-chromium)?'
100 r'-(?P<type>\w+)/(R(?P<milestone>\d+)-'
101 r'(?P<manifest>[\d.ab-]+)|LATEST)',
Simran Basif8f648e2014-09-09 11:40:03 -0700102 name)
103 if match and len(match.groups()) >= 5:
Simran Basib7d21162014-05-21 15:26:16 -0700104 return (match.group('board'), match.group('type'),
105 match.group('milestone'), match.group('manifest'))
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800106 raise ParseBuildNameException('%s is a malformed build name.' % name)
107
Alex Millerdadc2c22013-07-08 15:21:21 -0700108
Dan Shi3d7a0e12015-10-12 11:55:45 -0700109def get_labels_from_afe(hostname, label_prefix, afe):
110 """Retrieve a host's specific labels from the AFE.
111
112 Looks for the host labels that have the form <label_prefix>:<value>
113 and returns the "<value>" part of the label. None is returned
114 if there is not a label matching the pattern
115
116 @param hostname: hostname of given DUT.
117 @param label_prefix: prefix of label to be matched, e.g., |board:|
118 @param afe: afe instance.
119
120 @returns A list of labels that match the prefix or 'None'
121
122 """
123 labels = afe.get_labels(name__startswith=label_prefix,
124 host__hostname__in=[hostname])
125 if labels:
126 return [l.name.split(label_prefix, 1)[1] for l in labels]
127
128
Dan Shia1ecd5c2013-06-06 11:21:31 -0700129def get_label_from_afe(hostname, label_prefix, afe):
130 """Retrieve a host's specific label from the AFE.
131
132 Looks for a host label that has the form <label_prefix>:<value>
133 and returns the "<value>" part of the label. None is returned
134 if there is not a label matching the pattern
135
136 @param hostname: hostname of given DUT.
137 @param label_prefix: prefix of label to be matched, e.g., |board:|
138 @param afe: afe instance.
139 @returns the label that matches the prefix or 'None'
140
141 """
Dan Shi3d7a0e12015-10-12 11:55:45 -0700142 labels = get_labels_from_afe(hostname, label_prefix, afe)
Dan Shia1ecd5c2013-06-06 11:21:31 -0700143 if labels and len(labels) == 1:
Dan Shi3d7a0e12015-10-12 11:55:45 -0700144 return labels[0]
Dan Shia1ecd5c2013-06-06 11:21:31 -0700145
146
147def get_board_from_afe(hostname, afe):
148 """Retrieve given host's board from its labels in the AFE.
149
150 Looks for a host label of the form "board:<board>", and
151 returns the "<board>" part of the label. `None` is returned
152 if there is not a single, unique label matching the pattern.
153
154 @param hostname: hostname of given DUT.
155 @param afe: afe instance.
156 @returns board from label, or `None`.
157
158 """
159 return get_label_from_afe(hostname, constants.BOARD_PREFIX, afe)
160
161
162def get_build_from_afe(hostname, afe):
163 """Retrieve the current build for given host from the AFE.
164
165 Looks through the host's labels in the AFE to determine its build.
166
167 @param hostname: hostname of given DUT.
168 @param afe: afe instance.
169 @returns The current build or None if it could not find it or if there
170 were multiple build labels assigned to this host.
171
172 """
Simran Basi7756a0b2016-03-16 13:10:07 -0700173 for prefix in [provision.CROS_VERSION_PREFIX,
174 provision.ANDROID_BUILD_VERSION_PREFIX]:
175 build = get_label_from_afe(hostname, prefix + ':', afe)
176 if build:
177 return build
178 return None
Dan Shia1ecd5c2013-06-06 11:21:31 -0700179
180
Allen Li6a612392016-08-18 12:09:32 -0700181# TODO(fdeng): fix get_sheriffs crbug.com/483254
Fang Deng3197b392013-06-26 11:42:02 -0700182def get_sheriffs(lab_only=False):
Alex Millerdadc2c22013-07-08 15:21:21 -0700183 """
184 Polls the javascript file that holds the identity of the sheriff and
185 parses it's output to return a list of chromium sheriff email addresses.
186 The javascript file can contain the ldap of more than one sheriff, eg:
187 document.write('sheriff_one, sheriff_two').
188
Fang Deng3197b392013-06-26 11:42:02 -0700189 @param lab_only: if True, only pulls lab sheriff.
190 @return: A list of chroium.org sheriff email addresses to cc on the bug.
191 An empty list if failed to parse the javascript.
Alex Millerdadc2c22013-07-08 15:21:21 -0700192 """
193 sheriff_ids = []
Fang Deng3197b392013-06-26 11:42:02 -0700194 sheriff_js_list = _LAB_SHERIFF_JS.split(',')
195 if not lab_only:
196 sheriff_js_list.extend(_SHERIFF_JS.split(','))
197
198 for sheriff_js in sheriff_js_list:
Alex Millerdadc2c22013-07-08 15:21:21 -0700199 try:
Dan Shief31f032016-05-13 15:51:39 -0700200 url_content = utils.urlopen('%s%s'% (
Alex Millerdadc2c22013-07-08 15:21:21 -0700201 _CHROMIUM_BUILD_URL, sheriff_js)).read()
202 except (ValueError, IOError) as e:
beeps4efdf032013-09-17 11:27:14 -0700203 logging.warning('could not parse sheriff from url %s%s: %s',
204 _CHROMIUM_BUILD_URL, sheriff_js, str(e))
Paul Drewsbef578d2013-09-24 15:10:36 -0700205 except (urllib2.URLError, httplib.HTTPException) as e:
206 logging.warning('unexpected error reading from url "%s%s": %s',
207 _CHROMIUM_BUILD_URL, sheriff_js, str(e))
Alex Millerdadc2c22013-07-08 15:21:21 -0700208 else:
209 ldaps = re.search(r"document.write\('(.*)'\)", url_content)
210 if not ldaps:
beeps4efdf032013-09-17 11:27:14 -0700211 logging.warning('Could not retrieve sheriff ldaps for: %s',
212 url_content)
Alex Millerdadc2c22013-07-08 15:21:21 -0700213 continue
214 sheriff_ids += ['%s@chromium.org' % alias.replace(' ', '')
215 for alias in ldaps.group(1).split(',')]
216 return sheriff_ids
beeps46dadc92013-11-07 14:07:10 -0800217
218
219def remote_wget(source_url, dest_path, ssh_cmd):
220 """wget source_url from localhost to dest_path on remote host using ssh.
221
222 @param source_url: The complete url of the source of the package to send.
223 @param dest_path: The path on the remote host's file system where we would
224 like to store the package.
225 @param ssh_cmd: The ssh command to use in performing the remote wget.
226 """
227 wget_cmd = ("wget -O - %s | %s 'cat >%s'" %
228 (source_url, ssh_cmd, dest_path))
Dan Shief31f032016-05-13 15:51:39 -0700229 utils.run(wget_cmd)
beeps46dadc92013-11-07 14:07:10 -0800230
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800231
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800232_MAX_LAB_STATUS_ATTEMPTS = 5
233def _get_lab_status(status_url):
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800234 """Grabs the current lab status and message.
235
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800236 @returns The JSON object obtained from the given URL.
237
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800238 """
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800239 retry_waittime = 1
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800240 for _ in range(_MAX_LAB_STATUS_ATTEMPTS):
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800241 try:
242 response = urllib2.urlopen(status_url)
243 except IOError as e:
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800244 logging.debug('Error occurred when grabbing the lab status: %s.',
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800245 e)
246 time.sleep(retry_waittime)
247 continue
248 # Check for successful response code.
249 if response.getcode() == 200:
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800250 return json.load(response)
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800251 time.sleep(retry_waittime)
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800252 return None
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800253
254
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800255def _decode_lab_status(lab_status, build):
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800256 """Decode lab status, and report exceptions as needed.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800257
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800258 Take a deserialized JSON object from the lab status page, and
259 interpret it to determine the actual lab status. Raise
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800260 exceptions as required to report when the lab is down.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800261
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800262 @param build: build name that we want to check the status of.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800263
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800264 @raises TestLabException Raised if a request to test for the given
265 status and build should be blocked.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800266 """
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800267 # First check if the lab is up.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800268 if not lab_status['general_state'] in LAB_GOOD_STATES:
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800269 raise TestLabException('Chromium OS Test Lab is closed: '
270 '%s.' % lab_status['message'])
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800271
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800272 # Check if the build we wish to use is disabled.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800273 # Lab messages should be in the format of:
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800274 # Lab is 'status' [regex ...] (comment)
275 # If the build name matches any regex, it will be blocked.
276 build_exceptions = re.search('\[(.*)\]', lab_status['message'])
Prashanth Balasubramanianae437212014-10-27 11:17:26 -0700277 if not build_exceptions or not build:
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800278 return
279 for build_pattern in build_exceptions.group(1).split():
J. Richard Barnette7f215d32015-06-19 12:44:38 -0700280 if re.match(build_pattern, build):
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800281 raise TestLabException('Chromium OS Test Lab is closed: '
282 '%s matches %s.' % (
283 build, build_pattern))
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800284 return
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800285
286
Dan Shi94234cb2014-05-23 20:04:31 -0700287def is_in_lab():
288 """Check if current Autotest instance is in lab
289
290 @return: True if the Autotest instance is in lab.
291 """
Dan Shid37736b2016-07-06 15:10:29 -0700292 test_server_name = CONFIG.get_config_value('SERVER', 'hostname')
Dan Shi94234cb2014-05-23 20:04:31 -0700293 return test_server_name.startswith('cautotest')
294
295
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800296def check_lab_status(build):
297 """Check if the lab status allows us to schedule for a build.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800298
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800299 Checks if the lab is down, or if testing for the requested build
300 should be blocked.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800301
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800302 @param build: Name of the build to be scheduled for testing.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800303
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800304 @raises TestLabException Raised if a request to test for the given
305 status and build should be blocked.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800306
307 """
308 # Ensure we are trying to schedule on the actual lab.
Dan Shi94234cb2014-05-23 20:04:31 -0700309 if not is_in_lab():
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800310 return
311
312 # Download the lab status from its home on the web.
Dan Shid37736b2016-07-06 15:10:29 -0700313 status_url = CONFIG.get_config_value('CROS', 'lab_status_url')
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800314 json_status = _get_lab_status(status_url)
315 if json_status is None:
316 # We go ahead and say the lab is open if we can't get the status.
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700317 logging.warning('Could not get a status from %s', status_url)
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800318 return
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800319 _decode_lab_status(json_status, build)
beeps023afc62014-02-04 16:59:22 -0800320
321
322def lock_host_with_labels(afe, lock_manager, labels):
323 """Lookup and lock one host that matches the list of input labels.
324
325 @param afe: An instance of the afe class, as defined in server.frontend.
326 @param lock_manager: A lock manager capable of locking hosts, eg the
327 one defined in server.cros.host_lock_manager.
328 @param labels: A list of labels to look for on hosts.
329
330 @return: The hostname of a host matching all labels, and locked through the
331 lock_manager. The hostname will be as specified in the database the afe
332 object is associated with, i.e if it exists in afe_hosts with a .cros
333 suffix, the hostname returned will contain a .cros suffix.
334
335 @raises: error.NoEligibleHostException: If no hosts matching the list of
336 input labels are available.
337 @raises: error.TestError: If unable to lock a host matching the labels.
338 """
339 potential_hosts = afe.get_hosts(multiple_labels=labels)
340 if not potential_hosts:
341 raise error.NoEligibleHostException(
342 'No devices found with labels %s.' % labels)
343
344 # This prevents errors where a fault might seem repeatable
345 # because we lock, say, the same packet capturer for each test run.
346 random.shuffle(potential_hosts)
347 for host in potential_hosts:
348 if lock_manager.lock([host.hostname]):
349 logging.info('Locked device %s with labels %s.',
350 host.hostname, labels)
351 return host.hostname
352 else:
353 logging.info('Unable to lock device %s with labels %s.',
354 host.hostname, labels)
355
356 raise error.TestError('Could not lock a device with labels %s' % labels)
Dan Shi7e04fa82013-07-25 15:08:48 -0700357
358
359def get_test_views_from_tko(suite_job_id, tko):
360 """Get test name and result for given suite job ID.
361
362 @param suite_job_id: ID of suite job.
363 @param tko: an instance of TKO as defined in server/frontend.py.
364 @return: A dictionary of test status keyed by test name, e.g.,
365 {'dummy_Fail.Error': 'ERROR', 'dummy_Fail.NAError': 'TEST_NA'}
366 @raise: Exception when there is no test view found.
367
368 """
369 views = tko.run('get_detailed_test_views', afe_job_id=suite_job_id)
370 relevant_views = filter(job_status.view_is_relevant, views)
371 if not relevant_views:
372 raise Exception('Failed to retrieve job results.')
373
374 test_views = {}
375 for view in relevant_views:
376 test_views[view['test_name']] = view['status']
377
378 return test_views
MK Ryu35d661e2014-09-25 17:44:10 -0700379
380
MK Ryuc9c0c3f2014-10-27 14:36:01 -0700381def get_data_key(prefix, suite, build, board):
382 """
383 Constructs a key string from parameters.
384
385 @param prefix: Prefix for the generating key.
386 @param suite: a suite name. e.g., bvt-cq, bvt-inline, dummy
387 @param build: The build string. This string should have a consistent
388 format eg: x86-mario-release/R26-3570.0.0. If the format of this
389 string changes such that we can't determine build_type or branch
390 we give up and use the parametes we're sure of instead (suite,
391 board). eg:
392 1. build = x86-alex-pgo-release/R26-3570.0.0
393 branch = 26
394 build_type = pgo-release
395 2. build = lumpy-paladin/R28-3993.0.0-rc5
396 branch = 28
397 build_type = paladin
398 @param board: The board that this suite ran on.
399 @return: The key string used for a dictionary.
400 """
401 try:
402 _board, build_type, branch = ParseBuildName(build)[:3]
403 except ParseBuildNameException as e:
404 logging.error(str(e))
405 branch = 'Unknown'
406 build_type = 'Unknown'
407 else:
408 embedded_str = re.search(r'x86-\w+-(.*)', _board)
409 if embedded_str:
410 build_type = embedded_str.group(1) + '-' + build_type
411
412 data_key_dict = {
413 'prefix': prefix,
414 'board': board,
415 'branch': branch,
416 'build_type': build_type,
417 'suite': suite,
418 }
419 return ('%(prefix)s.%(board)s.%(build_type)s.%(branch)s.%(suite)s'
420 % data_key_dict)
MK Ryu83184352014-12-10 14:59:40 -0800421
422
MK Ryu2d0a3642015-01-07 15:11:19 -0800423def setup_logging(logfile=None, prefix=False):
MK Ryu83184352014-12-10 14:59:40 -0800424 """Setup basic logging with all logging info stripped.
425
426 Calls to logging will only show the message. No severity is logged.
427
428 @param logfile: If specified dump output to a file as well.
MK Ryu2d0a3642015-01-07 15:11:19 -0800429 @param prefix: Flag for log prefix. Set to True to add prefix to log
430 entries to include timestamp and log level. Default is False.
MK Ryu83184352014-12-10 14:59:40 -0800431 """
432 # Remove all existing handlers. client/common_lib/logging_config adds
433 # a StreamHandler to logger when modules are imported, e.g.,
434 # autotest_lib.client.bin.utils. A new StreamHandler will be added here to
435 # log only messages, not severity.
436 logging.getLogger().handlers = []
437
MK Ryu2d0a3642015-01-07 15:11:19 -0800438 if prefix:
439 log_format = '%(asctime)s %(levelname)-5s| %(message)s'
440 else:
441 log_format = '%(message)s'
442
MK Ryu83184352014-12-10 14:59:40 -0800443 screen_handler = logging.StreamHandler()
MK Ryu2d0a3642015-01-07 15:11:19 -0800444 screen_handler.setFormatter(logging.Formatter(log_format))
MK Ryu83184352014-12-10 14:59:40 -0800445 logging.getLogger().addHandler(screen_handler)
446 logging.getLogger().setLevel(logging.INFO)
447 if logfile:
448 file_handler = logging.FileHandler(logfile)
MK Ryu2d0a3642015-01-07 15:11:19 -0800449 file_handler.setFormatter(logging.Formatter(log_format))
MK Ryu83184352014-12-10 14:59:40 -0800450 file_handler.setLevel(logging.DEBUG)
451 logging.getLogger().addHandler(file_handler)
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -0800452
453
454def is_shard():
455 """Determines if this instance is running as a shard.
456
457 Reads the global_config value shard_hostname in the section SHARD.
458
459 @return True, if shard_hostname is set, False otherwise.
460 """
Dan Shid37736b2016-07-06 15:10:29 -0700461 hostname = CONFIG.get_config_value('SHARD', 'shard_hostname', default=None)
MK Ryu0c1a37d2015-04-30 12:00:55 -0700462 return bool(hostname)
463
464
Fang Deng0cb2a3b2015-12-10 17:59:00 -0800465def get_global_afe_hostname():
466 """Read the hostname of the global AFE from the global configuration."""
Dan Shid37736b2016-07-06 15:10:29 -0700467 return CONFIG.get_config_value('SERVER', 'global_afe_hostname')
Fang Deng0cb2a3b2015-12-10 17:59:00 -0800468
469
Fang Deng18699fe2015-12-04 16:40:27 -0800470def is_restricted_user(username):
471 """Determines if a user is in a restricted group.
472
473 User in restricted group only have access to master.
474
475 @param username: A string, representing a username.
476
477 @returns: True if the user is in a restricted group.
478 """
479 if not username:
480 return False
481
Dan Shid37736b2016-07-06 15:10:29 -0700482 restricted_groups = CONFIG.get_config_value(
Fang Deng18699fe2015-12-04 16:40:27 -0800483 'AUTOTEST_WEB', 'restricted_groups', default='').split(',')
484 for group in restricted_groups:
Fang Deng5229c852016-02-09 13:30:31 -0800485 try:
486 if group and username in grp.getgrnam(group).gr_mem:
487 return True
488 except KeyError as e:
489 logging.debug("%s is not a valid group.", group)
Fang Deng18699fe2015-12-04 16:40:27 -0800490 return False
491
492
MK Ryu0c1a37d2015-04-30 12:00:55 -0700493def get_special_task_status(is_complete, success, is_active):
494 """Get the status of a special task.
495
496 Emulate a host queue entry status for a special task
497 Although SpecialTasks are not HostQueueEntries, it is helpful to
498 the user to present similar statuses.
499
500 @param is_complete Boolean if the task is completed.
501 @param success Boolean if the task succeeded.
502 @param is_active Boolean if the task is active.
503
504 @return The status of a special task.
505 """
506 if is_complete:
507 if success:
508 return host_queue_entry_states.Status.COMPLETED
509 return host_queue_entry_states.Status.FAILED
510 if is_active:
511 return host_queue_entry_states.Status.RUNNING
512 return host_queue_entry_states.Status.QUEUED
513
514
515def get_special_task_exec_path(hostname, task_id, task_name, time_requested):
516 """Get the execution path of the SpecialTask.
517
518 This method returns different paths depending on where a
519 the task ran:
520 * Master: hosts/hostname/task_id-task_type
521 * Shard: Master_path/time_created
522 This is to work around the fact that a shard can fail independent
523 of the master, and be replaced by another shard that has the same
524 hosts. Without the time_created stamp the logs of the tasks running
525 on the second shard will clobber the logs from the first in google
526 storage, because task ids are not globally unique.
527
528 @param hostname Hostname
529 @param task_id Special task id
530 @param task_name Special task name (e.g., Verify, Repair, etc)
531 @param time_requested Special task requested time.
532
533 @return An execution path for the task.
534 """
535 results_path = 'hosts/%s/%s-%s' % (hostname, task_id, task_name.lower())
536
537 # If we do this on the master it will break backward compatibility,
538 # as there are tasks that currently don't have timestamps. If a host
539 # or job has been sent to a shard, the rpc for that host/job will
540 # be redirected to the shard, so this global_config check will happen
541 # on the shard the logs are on.
542 if not is_shard():
543 return results_path
544
545 # Generate a uid to disambiguate special task result directories
546 # in case this shard fails. The simplest uid is the job_id, however
547 # in rare cases tasks do not have jobs associated with them (eg:
548 # frontend verify), so just use the creation timestamp. The clocks
549 # between a shard and master should always be in sync. Any discrepancies
550 # will be brought to our attention in the form of job timeouts.
551 uid = time_requested.strftime('%Y%d%m%H%M%S')
552
553 # TODO: This is a hack, however it is the easiest way to achieve
554 # correctness. There is currently some debate over the future of
555 # tasks in our infrastructure and refactoring everything right
556 # now isn't worth the time.
557 return '%s/%s' % (results_path, uid)
558
559
560def get_job_tag(id, owner):
561 """Returns a string tag for a job.
562
563 @param id Job id
564 @param owner Job owner
565
566 """
567 return '%s-%s' % (id, owner)
568
569
570def get_hqe_exec_path(tag, execution_subdir):
571 """Returns a execution path to a HQE's results.
572
573 @param tag Tag string for a job associated with a HQE.
574 @param execution_subdir Execution sub-directory string of a HQE.
575
576 """
577 return os.path.join(tag, execution_subdir)
Dan Shi82997b92015-05-06 12:08:02 -0700578
579
580def is_inside_chroot():
581 """Check if the process is running inside chroot.
582
583 This is a wrapper around chromite.lib.cros_build_lib.IsInsideChroot(). The
584 method checks if cros_build_lib can be imported first.
585
586 @return: True if the process is running inside chroot or cros_build_lib
587 cannot be imported.
588
589 """
J. Richard Barnette7f215d32015-06-19 12:44:38 -0700590 return not cros_build_lib or cros_build_lib.IsInsideChroot()
Dan Shi70647ca2015-07-16 22:52:35 -0700591
592
593def parse_job_name(name):
594 """Parse job name to get information including build, board and suite etc.
595
596 Suite job created by run_suite follows the naming convention of:
597 [build]-test_suites/control.[suite]
598 For example: lumpy-release/R46-7272.0.0-test_suites/control.bvt
599 The naming convention is defined in site_rpc_interface.create_suite_job.
600
601 Test job created by suite job follows the naming convention of:
602 [build]/[suite]/[test name]
603 For example: lumpy-release/R46-7272.0.0/bvt/login_LoginSuccess
604 The naming convention is defined in
605 server/cros/dynamic_suite/tools.create_job_name
606
607 Note that pgo and chrome-perf builds will fail the method. Since lab does
608 not run test for these builds, they can be ignored.
Dan Shief31f032016-05-13 15:51:39 -0700609 Also, tests for Launch Control builds have different naming convention.
610 The build ID will be used as build_version.
Dan Shi70647ca2015-07-16 22:52:35 -0700611
612 @param name: Name of the job.
613
614 @return: A dictionary containing the test information. The keyvals include:
615 build: Name of the build, e.g., lumpy-release/R46-7272.0.0
616 build_version: The version of the build, e.g., R46-7272.0.0
617 board: Name of the board, e.g., lumpy
618 suite: Name of the test suite, e.g., bvt
619
620 """
621 info = {}
Dan Shief31f032016-05-13 15:51:39 -0700622 suite_job_regex = '([^/]*/[^/]*(?:/\d+)?)-test_suites/control\.(.*)'
623 test_job_regex = '([^/]*/[^/]*(?:/\d+)?)/([^/]+)/.*'
Dan Shi70647ca2015-07-16 22:52:35 -0700624 match = re.match(suite_job_regex, name)
625 if not match:
626 match = re.match(test_job_regex, name)
627 if match:
628 info['build'] = match.groups()[0]
629 info['suite'] = match.groups()[1]
630 info['build_version'] = info['build'].split('/')[1]
631 try:
632 info['board'], _, _, _ = ParseBuildName(info['build'])
633 except ParseBuildNameException:
Dan Shief31f032016-05-13 15:51:39 -0700634 # Try to parse it as Launch Control build
635 # Launch Control builds have name format:
636 # branch/build_target-build_type/build_id.
637 try:
638 _, target, build_id = utils.parse_launch_control_build(
639 info['build'])
640 build_target, _ = utils.parse_launch_control_target(target)
641 if build_target:
642 info['board'] = build_target
643 info['build_version'] = build_id
644 except ValueError:
645 pass
Dan Shi70647ca2015-07-16 22:52:35 -0700646 return info
Kevin Cheng3a4a57a2015-09-30 12:09:50 -0700647
648
649def add_label_detector(label_function_list, label_list=None, label=None):
650 """Decorator used to group functions together into the provided list.
651
652 This is a helper function to automatically add label functions that have
653 the label decorator. This is to help populate the class list of label
654 functions to be retrieved by the get_labels class method.
655
656 @param label_function_list: List of label detecting functions to add
657 decorated function to.
658 @param label_list: List of detectable labels to add detectable labels to.
659 (Default: None)
660 @param label: Label string that is detectable by this detection function
661 (Default: None)
662 """
663 def add_func(func):
664 """
665 @param func: The function to be added as a detector.
666 """
667 label_function_list.append(func)
668 if label and label_list is not None:
669 label_list.append(label)
670 return func
671 return add_func
Simran Basi9f364a62015-12-07 14:15:19 -0800672
673
674def verify_not_root_user():
675 """Simple function to error out if running with uid == 0"""
676 if os.getuid() == 0:
Simran Basi1bf60eb2015-12-01 16:39:29 -0800677 raise error.IllegalUser('This script can not be ran as root.')
678
679
680def get_hostname_from_machine(machine):
681 """Lookup hostname from a machine string or dict.
682
683 @returns: Machine hostname in string format.
684 """
685 hostname, _ = get_host_info_from_machine(machine)
686 return hostname
687
688
689def get_host_info_from_machine(machine):
690 """Lookup host information from a machine string or dict.
691
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700692 @returns: Tuple of (hostname, afe_host)
Simran Basi1bf60eb2015-12-01 16:39:29 -0800693 """
694 if isinstance(machine, dict):
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700695 return (machine['hostname'], machine['afe_host'])
Simran Basi1bf60eb2015-12-01 16:39:29 -0800696 else:
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700697 return (machine, EmptyAFEHost())
698
699
700def get_afe_host_from_machine(machine):
701 """Return the afe_host from the machine dict if possible.
702
703 @returns: AFE host object.
704 """
705 _, afe_host = get_host_info_from_machine(machine)
706 return afe_host
Fang Dengf8a94e22015-12-07 13:39:13 -0800707
708
709def get_creds_abspath(creds_file):
710 """Returns the abspath of the credentials file.
711
712 If creds_file is already an absolute path, just return it.
713 Otherwise, assume it is located in the creds directory
714 specified in global_config and return the absolute path.
715
716 @param: creds_path, a path to the credentials.
717 @return: An absolute path to the credentials file.
718 """
719 if not creds_file:
720 return None
721 if os.path.isabs(creds_file):
722 return creds_file
Dan Shid37736b2016-07-06 15:10:29 -0700723 creds_dir = CONFIG.get_config_value('SERVER', 'creds_dir', default='')
Fang Dengf8a94e22015-12-07 13:39:13 -0800724 if not creds_dir or not os.path.exists(creds_dir):
725 creds_dir = common.autotest_dir
726 return os.path.join(creds_dir, creds_file)
Kevin Cheng3b111812015-12-15 11:52:08 -0800727
728
729def machine_is_testbed(machine):
730 """Checks if the machine is a testbed.
731
732 The signal we use to determine if the machine is a testbed
733 is if the host attributes contain more than 1 serial.
734
735 @param machine: is a list of dicts
736
737 @return: True if the machine is a testbed, False otherwise.
738 """
Kevin Cheng05ae2a42016-06-06 10:12:48 -0700739 _, afe_host = get_host_info_from_machine(machine)
740 return len(afe_host.attributes.get('serials', '').split(',')) > 1