blob: 19a29ac9f6917e294f6b11506776ecac452f84c3 [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
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -08006import httplib
7import json
Alex Millerdadc2c22013-07-08 15:21:21 -07008import logging
MK Ryu35d661e2014-09-25 17:44:10 -07009import os
beeps023afc62014-02-04 16:59:22 -080010import random
Alex Millerdadc2c22013-07-08 15:21:21 -070011import re
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080012import time
Paul Drewsbef578d2013-09-24 15:10:36 -070013import urllib2
Alex Millerdadc2c22013-07-08 15:21:21 -070014
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080015import common
beeps023afc62014-02-04 16:59:22 -080016from autotest_lib.client.common_lib import base_utils
17from autotest_lib.client.common_lib import error
18from autotest_lib.client.common_lib import global_config
MK Ryu0c1a37d2015-04-30 12:00:55 -070019from autotest_lib.client.common_lib import host_queue_entry_states
Dan Shia1ecd5c2013-06-06 11:21:31 -070020from autotest_lib.server.cros.dynamic_suite import constants
Dan Shi7e04fa82013-07-25 15:08:48 -070021from autotest_lib.server.cros.dynamic_suite import job_status
Dan Shia1ecd5c2013-06-06 11:21:31 -070022
23
Alex Millerdadc2c22013-07-08 15:21:21 -070024_SHERIFF_JS = global_config.global_config.get_config_value(
25 'NOTIFICATIONS', 'sheriffs', default='')
Fang Deng3197b392013-06-26 11:42:02 -070026_LAB_SHERIFF_JS = global_config.global_config.get_config_value(
27 'NOTIFICATIONS', 'lab_sheriffs', default='')
Alex Millerdadc2c22013-07-08 15:21:21 -070028_CHROMIUM_BUILD_URL = global_config.global_config.get_config_value(
29 'NOTIFICATIONS', 'chromium_build_url', default='')
30
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080031LAB_GOOD_STATES = ('open', 'throttled')
32
33
J. Richard Barnetteabbe0962013-12-10 18:15:44 -080034class TestLabException(Exception):
35 """Exception raised when the Test Lab blocks a test or suite."""
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080036 pass
37
38
39class ParseBuildNameException(Exception):
40 """Raised when ParseBuildName() cannot parse a build name."""
41 pass
42
43
44def ParseBuildName(name):
45 """Format a build name, given board, type, milestone, and manifest num.
46
Simran Basib7d21162014-05-21 15:26:16 -070047 @param name: a build name, e.g. 'x86-alex-release/R20-2015.0.0' or a
48 relative build name, e.g. 'x86-alex-release/LATEST'
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080049
50 @return board: board the manifest is for, e.g. x86-alex.
51 @return type: one of 'release', 'factory', or 'firmware'
52 @return milestone: (numeric) milestone the manifest was associated with.
Simran Basib7d21162014-05-21 15:26:16 -070053 Will be None for relative build names.
54 @return manifest: manifest number, e.g. '2015.0.0'.
55 Will be None for relative build names.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080056
57 """
Simran Basif8f648e2014-09-09 11:40:03 -070058 match = re.match(r'(trybot-)?(?P<board>[\w-]+)-(?P<type>\w+)/'
59 r'(R(?P<milestone>\d+)-(?P<manifest>[\d.ab-]+)|LATEST)',
60 name)
61 if match and len(match.groups()) >= 5:
Simran Basib7d21162014-05-21 15:26:16 -070062 return (match.group('board'), match.group('type'),
63 match.group('milestone'), match.group('manifest'))
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -080064 raise ParseBuildNameException('%s is a malformed build name.' % name)
65
Alex Millerdadc2c22013-07-08 15:21:21 -070066
Dan Shia1ecd5c2013-06-06 11:21:31 -070067def get_label_from_afe(hostname, label_prefix, afe):
68 """Retrieve a host's specific label from the AFE.
69
70 Looks for a host label that has the form <label_prefix>:<value>
71 and returns the "<value>" part of the label. None is returned
72 if there is not a label matching the pattern
73
74 @param hostname: hostname of given DUT.
75 @param label_prefix: prefix of label to be matched, e.g., |board:|
76 @param afe: afe instance.
77 @returns the label that matches the prefix or 'None'
78
79 """
80 labels = afe.get_labels(name__startswith=label_prefix,
81 host__hostname__in=[hostname])
82 if labels and len(labels) == 1:
83 return labels[0].name.split(label_prefix, 1)[1]
84
85
86def get_board_from_afe(hostname, afe):
87 """Retrieve given host's board from its labels in the AFE.
88
89 Looks for a host label of the form "board:<board>", and
90 returns the "<board>" part of the label. `None` is returned
91 if there is not a single, unique label matching the pattern.
92
93 @param hostname: hostname of given DUT.
94 @param afe: afe instance.
95 @returns board from label, or `None`.
96
97 """
98 return get_label_from_afe(hostname, constants.BOARD_PREFIX, afe)
99
100
101def get_build_from_afe(hostname, afe):
102 """Retrieve the current build for given host from the AFE.
103
104 Looks through the host's labels in the AFE to determine its build.
105
106 @param hostname: hostname of given DUT.
107 @param afe: afe instance.
108 @returns The current build or None if it could not find it or if there
109 were multiple build labels assigned to this host.
110
111 """
112 return get_label_from_afe(hostname, constants.VERSION_PREFIX, afe)
113
114
Fang Deng3197b392013-06-26 11:42:02 -0700115def get_sheriffs(lab_only=False):
Alex Millerdadc2c22013-07-08 15:21:21 -0700116 """
117 Polls the javascript file that holds the identity of the sheriff and
118 parses it's output to return a list of chromium sheriff email addresses.
119 The javascript file can contain the ldap of more than one sheriff, eg:
120 document.write('sheriff_one, sheriff_two').
121
Fang Deng3197b392013-06-26 11:42:02 -0700122 @param lab_only: if True, only pulls lab sheriff.
123 @return: A list of chroium.org sheriff email addresses to cc on the bug.
124 An empty list if failed to parse the javascript.
Alex Millerdadc2c22013-07-08 15:21:21 -0700125 """
126 sheriff_ids = []
Fang Deng3197b392013-06-26 11:42:02 -0700127 sheriff_js_list = _LAB_SHERIFF_JS.split(',')
128 if not lab_only:
129 sheriff_js_list.extend(_SHERIFF_JS.split(','))
130
131 for sheriff_js in sheriff_js_list:
Alex Millerdadc2c22013-07-08 15:21:21 -0700132 try:
133 url_content = base_utils.urlopen('%s%s'% (
134 _CHROMIUM_BUILD_URL, sheriff_js)).read()
135 except (ValueError, IOError) as e:
beeps4efdf032013-09-17 11:27:14 -0700136 logging.warning('could not parse sheriff from url %s%s: %s',
137 _CHROMIUM_BUILD_URL, sheriff_js, str(e))
Paul Drewsbef578d2013-09-24 15:10:36 -0700138 except (urllib2.URLError, httplib.HTTPException) as e:
139 logging.warning('unexpected error reading from url "%s%s": %s',
140 _CHROMIUM_BUILD_URL, sheriff_js, str(e))
Alex Millerdadc2c22013-07-08 15:21:21 -0700141 else:
142 ldaps = re.search(r"document.write\('(.*)'\)", url_content)
143 if not ldaps:
beeps4efdf032013-09-17 11:27:14 -0700144 logging.warning('Could not retrieve sheriff ldaps for: %s',
145 url_content)
Alex Millerdadc2c22013-07-08 15:21:21 -0700146 continue
147 sheriff_ids += ['%s@chromium.org' % alias.replace(' ', '')
148 for alias in ldaps.group(1).split(',')]
149 return sheriff_ids
beeps46dadc92013-11-07 14:07:10 -0800150
151
152def remote_wget(source_url, dest_path, ssh_cmd):
153 """wget source_url from localhost to dest_path on remote host using ssh.
154
155 @param source_url: The complete url of the source of the package to send.
156 @param dest_path: The path on the remote host's file system where we would
157 like to store the package.
158 @param ssh_cmd: The ssh command to use in performing the remote wget.
159 """
160 wget_cmd = ("wget -O - %s | %s 'cat >%s'" %
161 (source_url, ssh_cmd, dest_path))
162 base_utils.run(wget_cmd)
163
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800164
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800165_MAX_LAB_STATUS_ATTEMPTS = 5
166def _get_lab_status(status_url):
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800167 """Grabs the current lab status and message.
168
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800169 @returns The JSON object obtained from the given URL.
170
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800171 """
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800172 retry_waittime = 1
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800173 for _ in range(_MAX_LAB_STATUS_ATTEMPTS):
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800174 try:
175 response = urllib2.urlopen(status_url)
176 except IOError as e:
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800177 logging.debug('Error occurred when grabbing the lab status: %s.',
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800178 e)
179 time.sleep(retry_waittime)
180 continue
181 # Check for successful response code.
182 if response.getcode() == 200:
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800183 return json.load(response)
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800184 time.sleep(retry_waittime)
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800185 return None
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800186
187
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800188def _decode_lab_status(lab_status, build):
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800189 """Decode lab status, and report exceptions as needed.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800190
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800191 Take a deserialized JSON object from the lab status page, and
192 interpret it to determine the actual lab status. Raise
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800193 exceptions as required to report when the lab is down.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800194
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800195 @param build: build name that we want to check the status of.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800196
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800197 @raises TestLabException Raised if a request to test for the given
198 status and build should be blocked.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800199 """
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800200 # First check if the lab is up.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800201 if not lab_status['general_state'] in LAB_GOOD_STATES:
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800202 raise TestLabException('Chromium OS Test Lab is closed: '
203 '%s.' % lab_status['message'])
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800204
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800205 # Check if the build we wish to use is disabled.
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800206 # Lab messages should be in the format of:
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800207 # Lab is 'status' [regex ...] (comment)
208 # If the build name matches any regex, it will be blocked.
209 build_exceptions = re.search('\[(.*)\]', lab_status['message'])
Prashanth Balasubramanianae437212014-10-27 11:17:26 -0700210 if not build_exceptions or not build:
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800211 return
212 for build_pattern in build_exceptions.group(1).split():
213 if re.search(build_pattern, build):
214 raise TestLabException('Chromium OS Test Lab is closed: '
215 '%s matches %s.' % (
216 build, build_pattern))
J. Richard Barnette3cbd76b2013-11-27 12:11:25 -0800217 return
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800218
219
Dan Shi94234cb2014-05-23 20:04:31 -0700220def is_in_lab():
221 """Check if current Autotest instance is in lab
222
223 @return: True if the Autotest instance is in lab.
224 """
225 test_server_name = global_config.global_config.get_config_value(
226 'SERVER', 'hostname')
227 return test_server_name.startswith('cautotest')
228
229
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800230def check_lab_status(build):
231 """Check if the lab status allows us to schedule for a build.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800232
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800233 Checks if the lab is down, or if testing for the requested build
234 should be blocked.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800235
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800236 @param build: Name of the build to be scheduled for testing.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800237
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800238 @raises TestLabException Raised if a request to test for the given
239 status and build should be blocked.
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800240
241 """
242 # Ensure we are trying to schedule on the actual lab.
Dan Shi94234cb2014-05-23 20:04:31 -0700243 if not is_in_lab():
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800244 return
245
246 # Download the lab status from its home on the web.
247 status_url = global_config.global_config.get_config_value(
248 'CROS', 'lab_status_url')
249 json_status = _get_lab_status(status_url)
250 if json_status is None:
251 # We go ahead and say the lab is open if we can't get the status.
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700252 logging.warning('Could not get a status from %s', status_url)
J. Richard Barnette266da2a2013-11-27 15:09:55 -0800253 return
J. Richard Barnetteabbe0962013-12-10 18:15:44 -0800254 _decode_lab_status(json_status, build)
beeps023afc62014-02-04 16:59:22 -0800255
256
257def lock_host_with_labels(afe, lock_manager, labels):
258 """Lookup and lock one host that matches the list of input labels.
259
260 @param afe: An instance of the afe class, as defined in server.frontend.
261 @param lock_manager: A lock manager capable of locking hosts, eg the
262 one defined in server.cros.host_lock_manager.
263 @param labels: A list of labels to look for on hosts.
264
265 @return: The hostname of a host matching all labels, and locked through the
266 lock_manager. The hostname will be as specified in the database the afe
267 object is associated with, i.e if it exists in afe_hosts with a .cros
268 suffix, the hostname returned will contain a .cros suffix.
269
270 @raises: error.NoEligibleHostException: If no hosts matching the list of
271 input labels are available.
272 @raises: error.TestError: If unable to lock a host matching the labels.
273 """
274 potential_hosts = afe.get_hosts(multiple_labels=labels)
275 if not potential_hosts:
276 raise error.NoEligibleHostException(
277 'No devices found with labels %s.' % labels)
278
279 # This prevents errors where a fault might seem repeatable
280 # because we lock, say, the same packet capturer for each test run.
281 random.shuffle(potential_hosts)
282 for host in potential_hosts:
283 if lock_manager.lock([host.hostname]):
284 logging.info('Locked device %s with labels %s.',
285 host.hostname, labels)
286 return host.hostname
287 else:
288 logging.info('Unable to lock device %s with labels %s.',
289 host.hostname, labels)
290
291 raise error.TestError('Could not lock a device with labels %s' % labels)
Dan Shi7e04fa82013-07-25 15:08:48 -0700292
293
294def get_test_views_from_tko(suite_job_id, tko):
295 """Get test name and result for given suite job ID.
296
297 @param suite_job_id: ID of suite job.
298 @param tko: an instance of TKO as defined in server/frontend.py.
299 @return: A dictionary of test status keyed by test name, e.g.,
300 {'dummy_Fail.Error': 'ERROR', 'dummy_Fail.NAError': 'TEST_NA'}
301 @raise: Exception when there is no test view found.
302
303 """
304 views = tko.run('get_detailed_test_views', afe_job_id=suite_job_id)
305 relevant_views = filter(job_status.view_is_relevant, views)
306 if not relevant_views:
307 raise Exception('Failed to retrieve job results.')
308
309 test_views = {}
310 for view in relevant_views:
311 test_views[view['test_name']] = view['status']
312
313 return test_views
MK Ryu35d661e2014-09-25 17:44:10 -0700314
315
316def parse_simple_config(config_file):
317 """Get paths by parsing a simple config file.
318
319 Each line of the config file is a path for a file or directory.
320 Ignore an empty line and a line starting with a hash character ('#').
321 One example of this kind of simple config file is
322 client/common_lib/logs_to_collect.
323
324 @param config_file: Config file path
325 @return: A list of directory strings
326 """
327 dirs = []
328 for l in open(config_file):
329 l = l.strip()
330 if l and not l.startswith('#'):
331 dirs.append(l)
332 return dirs
333
334
335def concat_path_except_last(base, sub):
336 """Concatenate two paths but exclude last entry.
337
338 Take two paths as parameters and return a path string in which
339 the second path becomes under the first path.
340 In addition, remove the last path entry from the concatenated path.
341 This works even when two paths are absolute paths.
342
343 e.g., /usr/local/autotest/results/ + /var/log/ =
344 /usr/local/autotest/results/var
345
346 e.g., /usr/local/autotest/results/ + /var/log/syslog =
347 /usr/local/autotest/results/var/log
348
349 @param base: Beginning path
350 @param sub: The path that is concatenated to base
351 @return: Concatenated path string
352 """
353 dirname = os.path.dirname(sub.rstrip('/'))
354 return os.path.join(base, dirname.strip('/'))
MK Ryuc9c0c3f2014-10-27 14:36:01 -0700355
356
357def get_data_key(prefix, suite, build, board):
358 """
359 Constructs a key string from parameters.
360
361 @param prefix: Prefix for the generating key.
362 @param suite: a suite name. e.g., bvt-cq, bvt-inline, dummy
363 @param build: The build string. This string should have a consistent
364 format eg: x86-mario-release/R26-3570.0.0. If the format of this
365 string changes such that we can't determine build_type or branch
366 we give up and use the parametes we're sure of instead (suite,
367 board). eg:
368 1. build = x86-alex-pgo-release/R26-3570.0.0
369 branch = 26
370 build_type = pgo-release
371 2. build = lumpy-paladin/R28-3993.0.0-rc5
372 branch = 28
373 build_type = paladin
374 @param board: The board that this suite ran on.
375 @return: The key string used for a dictionary.
376 """
377 try:
378 _board, build_type, branch = ParseBuildName(build)[:3]
379 except ParseBuildNameException as e:
380 logging.error(str(e))
381 branch = 'Unknown'
382 build_type = 'Unknown'
383 else:
384 embedded_str = re.search(r'x86-\w+-(.*)', _board)
385 if embedded_str:
386 build_type = embedded_str.group(1) + '-' + build_type
387
388 data_key_dict = {
389 'prefix': prefix,
390 'board': board,
391 'branch': branch,
392 'build_type': build_type,
393 'suite': suite,
394 }
395 return ('%(prefix)s.%(board)s.%(build_type)s.%(branch)s.%(suite)s'
396 % data_key_dict)
MK Ryu83184352014-12-10 14:59:40 -0800397
398
MK Ryu2d0a3642015-01-07 15:11:19 -0800399def setup_logging(logfile=None, prefix=False):
MK Ryu83184352014-12-10 14:59:40 -0800400 """Setup basic logging with all logging info stripped.
401
402 Calls to logging will only show the message. No severity is logged.
403
404 @param logfile: If specified dump output to a file as well.
MK Ryu2d0a3642015-01-07 15:11:19 -0800405 @param prefix: Flag for log prefix. Set to True to add prefix to log
406 entries to include timestamp and log level. Default is False.
MK Ryu83184352014-12-10 14:59:40 -0800407 """
408 # Remove all existing handlers. client/common_lib/logging_config adds
409 # a StreamHandler to logger when modules are imported, e.g.,
410 # autotest_lib.client.bin.utils. A new StreamHandler will be added here to
411 # log only messages, not severity.
412 logging.getLogger().handlers = []
413
MK Ryu2d0a3642015-01-07 15:11:19 -0800414 if prefix:
415 log_format = '%(asctime)s %(levelname)-5s| %(message)s'
416 else:
417 log_format = '%(message)s'
418
MK Ryu83184352014-12-10 14:59:40 -0800419 screen_handler = logging.StreamHandler()
MK Ryu2d0a3642015-01-07 15:11:19 -0800420 screen_handler.setFormatter(logging.Formatter(log_format))
MK Ryu83184352014-12-10 14:59:40 -0800421 logging.getLogger().addHandler(screen_handler)
422 logging.getLogger().setLevel(logging.INFO)
423 if logfile:
424 file_handler = logging.FileHandler(logfile)
MK Ryu2d0a3642015-01-07 15:11:19 -0800425 file_handler.setFormatter(logging.Formatter(log_format))
MK Ryu83184352014-12-10 14:59:40 -0800426 file_handler.setLevel(logging.DEBUG)
427 logging.getLogger().addHandler(file_handler)
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -0800428
429
430def is_shard():
431 """Determines if this instance is running as a shard.
432
433 Reads the global_config value shard_hostname in the section SHARD.
434
435 @return True, if shard_hostname is set, False otherwise.
436 """
437 hostname = global_config.global_config.get_config_value(
438 'SHARD', 'shard_hostname', default=None)
MK Ryu0c1a37d2015-04-30 12:00:55 -0700439 return bool(hostname)
440
441
442def get_special_task_status(is_complete, success, is_active):
443 """Get the status of a special task.
444
445 Emulate a host queue entry status for a special task
446 Although SpecialTasks are not HostQueueEntries, it is helpful to
447 the user to present similar statuses.
448
449 @param is_complete Boolean if the task is completed.
450 @param success Boolean if the task succeeded.
451 @param is_active Boolean if the task is active.
452
453 @return The status of a special task.
454 """
455 if is_complete:
456 if success:
457 return host_queue_entry_states.Status.COMPLETED
458 return host_queue_entry_states.Status.FAILED
459 if is_active:
460 return host_queue_entry_states.Status.RUNNING
461 return host_queue_entry_states.Status.QUEUED
462
463
464def get_special_task_exec_path(hostname, task_id, task_name, time_requested):
465 """Get the execution path of the SpecialTask.
466
467 This method returns different paths depending on where a
468 the task ran:
469 * Master: hosts/hostname/task_id-task_type
470 * Shard: Master_path/time_created
471 This is to work around the fact that a shard can fail independent
472 of the master, and be replaced by another shard that has the same
473 hosts. Without the time_created stamp the logs of the tasks running
474 on the second shard will clobber the logs from the first in google
475 storage, because task ids are not globally unique.
476
477 @param hostname Hostname
478 @param task_id Special task id
479 @param task_name Special task name (e.g., Verify, Repair, etc)
480 @param time_requested Special task requested time.
481
482 @return An execution path for the task.
483 """
484 results_path = 'hosts/%s/%s-%s' % (hostname, task_id, task_name.lower())
485
486 # If we do this on the master it will break backward compatibility,
487 # as there are tasks that currently don't have timestamps. If a host
488 # or job has been sent to a shard, the rpc for that host/job will
489 # be redirected to the shard, so this global_config check will happen
490 # on the shard the logs are on.
491 if not is_shard():
492 return results_path
493
494 # Generate a uid to disambiguate special task result directories
495 # in case this shard fails. The simplest uid is the job_id, however
496 # in rare cases tasks do not have jobs associated with them (eg:
497 # frontend verify), so just use the creation timestamp. The clocks
498 # between a shard and master should always be in sync. Any discrepancies
499 # will be brought to our attention in the form of job timeouts.
500 uid = time_requested.strftime('%Y%d%m%H%M%S')
501
502 # TODO: This is a hack, however it is the easiest way to achieve
503 # correctness. There is currently some debate over the future of
504 # tasks in our infrastructure and refactoring everything right
505 # now isn't worth the time.
506 return '%s/%s' % (results_path, uid)
507
508
509def get_job_tag(id, owner):
510 """Returns a string tag for a job.
511
512 @param id Job id
513 @param owner Job owner
514
515 """
516 return '%s-%s' % (id, owner)
517
518
519def get_hqe_exec_path(tag, execution_subdir):
520 """Returns a execution path to a HQE's results.
521
522 @param tag Tag string for a job associated with a HQE.
523 @param execution_subdir Execution sub-directory string of a HQE.
524
525 """
526 return os.path.join(tag, execution_subdir)