blob: 591221f0cf9a739206bded485765146fed05fece [file] [log] [blame]
mbligh96cf0512008-04-17 15:25:38 +00001#!/usr/bin/python -u
mblighc2514542008-02-19 15:54:26 +00002
Aviv Keshet687d2dc2016-10-20 15:41:16 -07003import collections
Fang Deng49822682014-10-21 16:29:22 -07004import datetime
Aviv Keshet687d2dc2016-10-20 15:41:16 -07005import errno
6import fcntl
Simran Basi1e10e922015-04-16 15:09:56 -07007import json
Aviv Keshet687d2dc2016-10-20 15:41:16 -07008import optparse
9import os
10import socket
Shuqian Zhao31425d52016-12-07 09:35:03 -080011import subprocess
Aviv Keshet687d2dc2016-10-20 15:41:16 -070012import sys
Dan Shi11e35062017-11-03 10:09:05 -070013import time
Aviv Keshet687d2dc2016-10-20 15:41:16 -070014import traceback
mblighbb7b8912006-10-08 03:59:02 +000015
mbligh96cf0512008-04-17 15:25:38 +000016import common
Dan Shi4f8c0242017-07-07 15:34:49 -070017from autotest_lib.client.bin.result_tools import utils as result_utils
18from autotest_lib.client.bin.result_tools import utils_lib as result_utils_lib
19from autotest_lib.client.bin.result_tools import runner as result_runner
20from autotest_lib.client.common_lib import control_data
Benny Peakefeb775c2017-02-08 15:14:14 -080021from autotest_lib.client.common_lib import global_config
jadmanskidb4f9b52008-12-03 22:52:53 +000022from autotest_lib.client.common_lib import mail, pidfile
Fang Deng49822682014-10-21 16:29:22 -070023from autotest_lib.client.common_lib import utils
Fang Deng49822682014-10-21 16:29:22 -070024from autotest_lib.frontend import setup_django_environment
Fang Deng9ec66802014-04-28 19:04:33 +000025from autotest_lib.frontend.tko import models as tko_models
Shuqian Zhao19e62fb2017-01-09 10:10:14 -080026from autotest_lib.server import site_utils
Fang Deng49822682014-10-21 16:29:22 -070027from autotest_lib.server.cros.dynamic_suite import constants
Benny Peaked322d3d2017-02-08 15:39:28 -080028from autotest_lib.site_utils.sponge_lib import sponge_utils
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070029from autotest_lib.tko import db as tko_db, utils as tko_utils
Luigi Semenzatoe7064812017-02-03 14:47:59 -080030from autotest_lib.tko import models, parser_lib
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070031from autotest_lib.tko.perf_upload import perf_uploader
mbligh74fc0462007-11-05 20:24:17 +000032
Dan Shib0af6212017-07-17 14:40:02 -070033try:
34 from chromite.lib import metrics
35except ImportError:
36 metrics = utils.metrics_mock
37
38
Aviv Keshet687d2dc2016-10-20 15:41:16 -070039_ParseOptions = collections.namedtuple(
Shuqian Zhao19e62fb2017-01-09 10:10:14 -080040 'ParseOptions', ['reparse', 'mail_on_failure', 'dry_run', 'suite_report',
41 'datastore_creds', 'export_to_gcloud_path'])
Aviv Keshet687d2dc2016-10-20 15:41:16 -070042
mbligh96cf0512008-04-17 15:25:38 +000043def parse_args():
Fang Deng49822682014-10-21 16:29:22 -070044 """Parse args."""
jadmanski0afbb632008-06-06 21:10:57 +000045 # build up our options parser and parse sys.argv
46 parser = optparse.OptionParser()
47 parser.add_option("-m", help="Send mail for FAILED tests",
48 dest="mailit", action="store_true")
49 parser.add_option("-r", help="Reparse the results of a job",
50 dest="reparse", action="store_true")
51 parser.add_option("-o", help="Parse a single results directory",
52 dest="singledir", action="store_true")
53 parser.add_option("-l", help=("Levels of subdirectories to include "
54 "in the job name"),
55 type="int", dest="level", default=1)
56 parser.add_option("-n", help="No blocking on an existing parse",
57 dest="noblock", action="store_true")
58 parser.add_option("-s", help="Database server hostname",
59 dest="db_host", action="store")
60 parser.add_option("-u", help="Database username", dest="db_user",
61 action="store")
62 parser.add_option("-p", help="Database password", dest="db_pass",
63 action="store")
64 parser.add_option("-d", help="Database name", dest="db_name",
65 action="store")
Aviv Keshet0b7bab02016-10-20 17:17:36 -070066 parser.add_option("--dry-run", help="Do not actually commit any results.",
67 dest="dry_run", action="store_true", default=False)
Prathmesh Prabhu3e319da2017-08-30 19:13:03 -070068 parser.add_option(
69 "--detach", action="store_true",
70 help="Detach parsing process from the caller process. Used by "
71 "monitor_db to safely restart without affecting parsing.",
72 default=False)
jadmanskid5ab8c52008-12-03 16:27:07 +000073 parser.add_option("--write-pidfile",
74 help="write pidfile (.parser_execute)",
75 dest="write_pidfile", action="store_true",
76 default=False)
Fang Deng49822682014-10-21 16:29:22 -070077 parser.add_option("--record-duration",
Prathmesh Prabhu77769452018-04-17 13:30:50 -070078 help="[DEPRECATED] Record timing to metadata db",
Fang Deng49822682014-10-21 16:29:22 -070079 dest="record_duration", action="store_true",
80 default=False)
Shuqian Zhao31425d52016-12-07 09:35:03 -080081 parser.add_option("--suite-report",
82 help=("Allows parsing job to attempt to create a suite "
Shuqian Zhao19e62fb2017-01-09 10:10:14 -080083 "timeline report, if it detects that the job being "
Shuqian Zhao31425d52016-12-07 09:35:03 -080084 "parsed is a suite job."),
85 dest="suite_report", action="store_true",
86 default=False)
Shuqian Zhao19e62fb2017-01-09 10:10:14 -080087 parser.add_option("--datastore-creds",
88 help=("The path to gcloud datastore credentials file, "
89 "which will be used to upload suite timeline "
90 "report to gcloud. If not specified, the one "
91 "defined in shadow_config will be used."),
92 dest="datastore_creds", action="store", default=None)
93 parser.add_option("--export-to-gcloud-path",
94 help=("The path to export_to_gcloud script. Please find "
95 "chromite path on your server. The script is under "
96 "chromite/bin/."),
97 dest="export_to_gcloud_path", action="store",
98 default=None)
jadmanski0afbb632008-06-06 21:10:57 +000099 options, args = parser.parse_args()
mbligh74fc0462007-11-05 20:24:17 +0000100
jadmanski0afbb632008-06-06 21:10:57 +0000101 # we need a results directory
102 if len(args) == 0:
103 tko_utils.dprint("ERROR: at least one results directory must "
104 "be provided")
105 parser.print_help()
106 sys.exit(1)
mbligh74fc0462007-11-05 20:24:17 +0000107
Shuqian Zhao19e62fb2017-01-09 10:10:14 -0800108 if not options.datastore_creds:
109 gcloud_creds = global_config.global_config.get_config_value(
110 'GCLOUD', 'cidb_datastore_writer_creds', default=None)
111 options.datastore_creds = (site_utils.get_creds_abspath(gcloud_creds)
112 if gcloud_creds else None)
113
114 if not options.export_to_gcloud_path:
115 export_script = 'chromiumos/chromite/bin/export_to_gcloud'
116 # If it is a lab server, the script is under ~chromeos-test/
117 if os.path.exists(os.path.expanduser('~chromeos-test/%s' %
118 export_script)):
119 path = os.path.expanduser('~chromeos-test/%s' % export_script)
120 # If it is a local workstation, it is probably under ~/
121 elif os.path.exists(os.path.expanduser('~/%s' % export_script)):
122 path = os.path.expanduser('~/%s' % export_script)
123 # If it is not found anywhere, the default will be set to None.
124 else:
125 path = None
126 options.export_to_gcloud_path = path
127
jadmanski0afbb632008-06-06 21:10:57 +0000128 # pass the options back
129 return options, args
mbligh74fc0462007-11-05 20:24:17 +0000130
131
mbligh96cf0512008-04-17 15:25:38 +0000132def format_failure_message(jobname, kernel, testname, status, reason):
Fang Deng49822682014-10-21 16:29:22 -0700133 """Format failure message with the given information.
134
135 @param jobname: String representing the job name.
136 @param kernel: String representing the kernel.
137 @param testname: String representing the test name.
138 @param status: String representing the test status.
139 @param reason: String representing the reason.
140
141 @return: Failure message as a string.
142 """
jadmanski0afbb632008-06-06 21:10:57 +0000143 format_string = "%-12s %-20s %-12s %-10s %s"
144 return format_string % (jobname, kernel, testname, status, reason)
mblighb85e6b02006-10-08 17:20:56 +0000145
mblighbb7b8912006-10-08 03:59:02 +0000146
mbligh96cf0512008-04-17 15:25:38 +0000147def mailfailure(jobname, job, message):
Fang Deng49822682014-10-21 16:29:22 -0700148 """Send an email about the failure.
149
150 @param jobname: String representing the job name.
151 @param job: A job object.
152 @param message: The message to mail.
153 """
jadmanski0afbb632008-06-06 21:10:57 +0000154 message_lines = [""]
155 message_lines.append("The following tests FAILED for this job")
156 message_lines.append("http://%s/results/%s" %
157 (socket.gethostname(), jobname))
158 message_lines.append("")
159 message_lines.append(format_failure_message("Job name", "Kernel",
160 "Test name", "FAIL/WARN",
161 "Failure reason"))
162 message_lines.append(format_failure_message("=" * 8, "=" * 6, "=" * 8,
163 "=" * 8, "=" * 14))
164 message_header = "\n".join(message_lines)
mbligh96cf0512008-04-17 15:25:38 +0000165
jadmanski0afbb632008-06-06 21:10:57 +0000166 subject = "AUTOTEST: FAILED tests from job %s" % jobname
167 mail.send("", job.user, "", subject, message_header + message)
mbligh006f2302007-09-13 20:46:46 +0000168
169
Fang Deng9ec66802014-04-28 19:04:33 +0000170def _invalidate_original_tests(orig_job_idx, retry_job_idx):
171 """Retry tests invalidates original tests.
172
173 Whenever a retry job is complete, we want to invalidate the original
174 job's test results, such that the consumers of the tko database
175 (e.g. tko frontend, wmatrix) could figure out which results are the latest.
176
177 When a retry job is parsed, we retrieve the original job's afe_job_id
178 from the retry job's keyvals, which is then converted to tko job_idx and
179 passed into this method as |orig_job_idx|.
180
181 In this method, we are going to invalidate the rows in tko_tests that are
182 associated with the original job by flipping their 'invalid' bit to True.
183 In addition, in tko_tests, we also maintain a pointer from the retry results
184 to the original results, so that later we can always know which rows in
185 tko_tests are retries and which are the corresponding original results.
186 This is done by setting the field 'invalidates_test_idx' of the tests
187 associated with the retry job.
188
189 For example, assume Job(job_idx=105) are retried by Job(job_idx=108), after
190 this method is run, their tko_tests rows will look like:
191 __________________________________________________________________________
192 test_idx| job_idx | test | ... | invalid | invalidates_test_idx
193 10 | 105 | dummy_Fail.Error| ... | 1 | NULL
194 11 | 105 | dummy_Fail.Fail | ... | 1 | NULL
195 ...
196 20 | 108 | dummy_Fail.Error| ... | 0 | 10
197 21 | 108 | dummy_Fail.Fail | ... | 0 | 11
198 __________________________________________________________________________
199 Note the invalid bits of the rows for Job(job_idx=105) are set to '1'.
200 And the 'invalidates_test_idx' fields of the rows for Job(job_idx=108)
201 are set to 10 and 11 (the test_idx of the rows for the original job).
202
203 @param orig_job_idx: An integer representing the original job's
204 tko job_idx. Tests associated with this job will
205 be marked as 'invalid'.
206 @param retry_job_idx: An integer representing the retry job's
207 tko job_idx. The field 'invalidates_test_idx'
208 of the tests associated with this job will be updated.
209
210 """
211 msg = 'orig_job_idx: %s, retry_job_idx: %s' % (orig_job_idx, retry_job_idx)
212 if not orig_job_idx or not retry_job_idx:
213 tko_utils.dprint('ERROR: Could not invalidate tests: ' + msg)
214 # Using django models here makes things easier, but make sure that
215 # before this method is called, all other relevant transactions have been
216 # committed to avoid race condition. In the long run, we might consider
217 # to make the rest of parser use django models.
218 orig_tests = tko_models.Test.objects.filter(job__job_idx=orig_job_idx)
219 retry_tests = tko_models.Test.objects.filter(job__job_idx=retry_job_idx)
220
221 # Invalidate original tests.
222 orig_tests.update(invalid=True)
223
224 # Maintain a dictionary that maps (test, subdir) to original tests.
225 # Note that within the scope of a job, (test, subdir) uniquelly
226 # identifies a test run, but 'test' does not.
227 # In a control file, one could run the same test with different
228 # 'subdir_tag', for example,
229 # job.run_test('dummy_Fail', tag='Error', subdir_tag='subdir_1')
230 # job.run_test('dummy_Fail', tag='Error', subdir_tag='subdir_2')
231 # In tko, we will get
232 # (test='dummy_Fail.Error', subdir='dummy_Fail.Error.subdir_1')
233 # (test='dummy_Fail.Error', subdir='dummy_Fail.Error.subdir_2')
234 invalidated_tests = {(orig_test.test, orig_test.subdir): orig_test
235 for orig_test in orig_tests}
236 for retry in retry_tests:
237 # It is possible that (retry.test, retry.subdir) doesn't exist
238 # in invalidated_tests. This could happen when the original job
239 # didn't run some of its tests. For example, a dut goes offline
240 # since the beginning of the job, in which case invalidated_tests
241 # will only have one entry for 'SERVER_JOB'.
242 orig_test = invalidated_tests.get((retry.test, retry.subdir), None)
243 if orig_test:
244 retry.invalidates_test = orig_test
245 retry.save()
246 tko_utils.dprint('DEBUG: Invalidated tests associated to job: ' + msg)
247
248
Dan Shi4f8c0242017-07-07 15:34:49 -0700249def _throttle_result_size(path):
250 """Limit the total size of test results for the given path.
251
252 @param path: Path of the result directory.
253 """
254 if not result_runner.ENABLE_RESULT_THROTTLING:
255 tko_utils.dprint(
256 'Result throttling is not enabled. Skipping throttling %s' %
257 path)
258 return
259
Prathmesh Prabhudfbaac12018-08-10 12:43:47 -0700260 max_result_size_KB = _max_result_size_from_control()
261 if max_result_size_KB is None:
262 max_result_size_KB = control_data.DEFAULT_MAX_RESULT_SIZE_KB
263
264 try:
265 result_utils.execute(path, max_result_size_KB)
266 except:
267 tko_utils.dprint(
268 'Failed to throttle result size of %s.\nDetails %s' %
269 (path, traceback.format_exc()))
270
271
272def _max_result_size_from_control(path):
273 """Gets the max result size set in a control file, if any.
274
275 If not overrides is found, returns None.
276 """
Prathmesh Prabhu7e976822018-08-10 12:37:08 -0700277 hardcoded_control_file_names = (
278 # client side test control, as saved in old Autotest paths.
279 'control',
280 # server side test control, as saved in old Autotest paths.
281 'control.srv',
282 # All control files, as saved in skylab.
283 'control.from_control_name',
284 )
285 for control_file in hardcoded_control_file_names:
Dan Shi4f8c0242017-07-07 15:34:49 -0700286 control = os.path.join(path, control_file)
Prathmesh Prabhu5848ed02018-08-10 12:46:13 -0700287 if not os.path.exists(control):
288 continue
289
Dan Shi4f8c0242017-07-07 15:34:49 -0700290 try:
291 max_result_size_KB = control_data.parse_control(
292 control, raise_warnings=False).max_result_size_KB
Dan Shi4f8c0242017-07-07 15:34:49 -0700293 if max_result_size_KB != control_data.DEFAULT_MAX_RESULT_SIZE_KB:
Prathmesh Prabhudfbaac12018-08-10 12:43:47 -0700294 return max_result_size_KB
Dan Shi4f8c0242017-07-07 15:34:49 -0700295 except IOError as e:
296 tko_utils.dprint(
297 'Failed to access %s. Error: %s\nDetails %s' %
298 (control, e, traceback.format_exc()))
299 except control_data.ControlVariableException as e:
300 tko_utils.dprint(
301 'Failed to parse %s. Error: %s\nDetails %s' %
302 (control, e, traceback.format_exc()))
Prathmesh Prabhudfbaac12018-08-10 12:43:47 -0700303 return None
Dan Shi4f8c0242017-07-07 15:34:49 -0700304
305
Michael Tangc89efa72017-08-03 14:27:10 -0700306def export_tko_job_to_file(job, jobname, filename):
307 """Exports the tko job to disk file.
308
309 @param job: database object.
310 @param jobname: the job name as string.
311 @param filename: The path to the results to be parsed.
312 """
313 try:
314 from autotest_lib.tko import job_serializer
315
316 serializer = job_serializer.JobSerializer()
317 serializer.serialize_to_binary(job, jobname, filename)
318 except ImportError:
319 tko_utils.dprint("WARNING: tko_pb2.py doesn't exist. Create by "
320 "compiling tko/tko.proto.")
321
322
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700323def parse_one(db, pid_file_manager, jobname, path, parse_options):
Fang Deng49822682014-10-21 16:29:22 -0700324 """Parse a single job. Optionally send email on failure.
325
326 @param db: database object.
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700327 @param pid_file_manager: pidfile.PidFileManager object.
Fang Deng49822682014-10-21 16:29:22 -0700328 @param jobname: the tag used to search for existing job in db,
329 e.g. '1234-chromeos-test/host1'
330 @param path: The path to the results to be parsed.
Aviv Keshet687d2dc2016-10-20 15:41:16 -0700331 @param parse_options: _ParseOptions instance.
jadmanski0afbb632008-06-06 21:10:57 +0000332 """
Aviv Keshet687d2dc2016-10-20 15:41:16 -0700333 reparse = parse_options.reparse
334 mail_on_failure = parse_options.mail_on_failure
Aviv Keshet0b7bab02016-10-20 17:17:36 -0700335 dry_run = parse_options.dry_run
Shuqian Zhao31425d52016-12-07 09:35:03 -0800336 suite_report = parse_options.suite_report
Shuqian Zhao19e62fb2017-01-09 10:10:14 -0800337 datastore_creds = parse_options.datastore_creds
338 export_to_gcloud_path = parse_options.export_to_gcloud_path
Aviv Keshet687d2dc2016-10-20 15:41:16 -0700339
jadmanski0afbb632008-06-06 21:10:57 +0000340 tko_utils.dprint("\nScanning %s (%s)" % (jobname, path))
jadmanski9b6babf2009-04-21 17:57:40 +0000341 old_job_idx = db.find_job(jobname)
Prathmesh Prabhuedac1ee2018-04-18 19:16:34 -0700342 if old_job_idx is not None and not reparse:
343 tko_utils.dprint("! Job is already parsed, done")
344 return
mbligh96cf0512008-04-17 15:25:38 +0000345
jadmanski0afbb632008-06-06 21:10:57 +0000346 # look up the status version
jadmanskidb4f9b52008-12-03 22:52:53 +0000347 job_keyval = models.job.read_keyval(path)
348 status_version = job_keyval.get("status_version", 0)
jadmanski6e8bf752008-05-14 00:17:48 +0000349
Luigi Semenzatoe7064812017-02-03 14:47:59 -0800350 parser = parser_lib.parser(status_version)
jadmanski0afbb632008-06-06 21:10:57 +0000351 job = parser.make_job(path)
Prathmesh Prabhue06c49b2018-04-18 19:01:23 -0700352 tko_utils.dprint("+ Parsing dir=%s, jobname=%s" % (path, jobname))
353 status_log_path = _find_status_log_path(path)
354 if not status_log_path:
jadmanski0afbb632008-06-06 21:10:57 +0000355 tko_utils.dprint("! Unable to parse job, no status file")
356 return
Prathmesh Prabhue06c49b2018-04-18 19:01:23 -0700357 _parse_status_log(parser, job, status_log_path)
jadmanski9b6babf2009-04-21 17:57:40 +0000358
Prathmesh Prabhuedac1ee2018-04-18 19:16:34 -0700359 if old_job_idx is not None:
360 job.job_idx = old_job_idx
361 unmatched_tests = _match_existing_tests(db, job)
Aviv Keshet0b7bab02016-10-20 17:17:36 -0700362 if not dry_run:
Prathmesh Prabhuedac1ee2018-04-18 19:16:34 -0700363 _delete_tests_from_db(db, unmatched_tests)
mbligh96cf0512008-04-17 15:25:38 +0000364
Prathmesh Prabhu30dee862018-04-18 20:24:20 -0700365 job.afe_job_id = tko_utils.get_afe_job_id(jobname)
Prathmesh Prabhu17905882018-04-18 22:09:08 -0700366 job.skylab_task_id = tko_utils.get_skylab_task_id(jobname)
Prathmesh Prabhud25f15a2018-05-03 13:49:58 -0700367 job.afe_parent_job_id = job_keyval.get(constants.PARENT_JOB_ID)
368 job.skylab_parent_task_id = job_keyval.get(constants.PARENT_JOB_ID)
Benny Peakefeb775c2017-02-08 15:14:14 -0800369 job.build = None
370 job.board = None
371 job.build_version = None
372 job.suite = None
373 if job.label:
374 label_info = site_utils.parse_job_name(job.label)
375 if label_info:
376 job.build = label_info.get('build', None)
377 job.build_version = label_info.get('build_version', None)
378 job.board = label_info.get('board', None)
379 job.suite = label_info.get('suite', None)
380
Dan Shi4f8c0242017-07-07 15:34:49 -0700381 result_utils_lib.LOG = tko_utils.dprint
382 _throttle_result_size(path)
383
Dan Shiffd5b822017-07-14 11:16:23 -0700384 # Record test result size to job_keyvals
Dan Shi11e35062017-11-03 10:09:05 -0700385 start_time = time.time()
Dan Shiffd5b822017-07-14 11:16:23 -0700386 result_size_info = site_utils.collect_result_sizes(
387 path, log=tko_utils.dprint)
Dan Shi11e35062017-11-03 10:09:05 -0700388 tko_utils.dprint('Finished collecting result sizes after %s seconds' %
389 (time.time()-start_time))
Dan Shiffd5b822017-07-14 11:16:23 -0700390 job.keyval_dict.update(result_size_info.__dict__)
391
Dan Shiffd5b822017-07-14 11:16:23 -0700392 # TODO(dshi): Update sizes with sponge_invocation.xml and throttle it.
Dan Shi96c3bdc2017-05-24 11:34:30 -0700393
jadmanski0afbb632008-06-06 21:10:57 +0000394 # check for failures
395 message_lines = [""]
Simran Basi1e10e922015-04-16 15:09:56 -0700396 job_successful = True
jadmanski0afbb632008-06-06 21:10:57 +0000397 for test in job.tests:
398 if not test.subdir:
399 continue
Sida Liuafe550a2017-09-03 19:03:40 -0700400 tko_utils.dprint("* testname, subdir, status, reason: %s %s %s %s"
401 % (test.testname, test.subdir, test.status,
402 test.reason))
Simran Basi1e10e922015-04-16 15:09:56 -0700403 if test.status != 'GOOD':
404 job_successful = False
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700405 pid_file_manager.num_tests_failed += 1
jadmanski0afbb632008-06-06 21:10:57 +0000406 message_lines.append(format_failure_message(
407 jobname, test.kernel.base, test.subdir,
408 test.status, test.reason))
Simran Basi59ca5ac2016-09-22 16:57:56 -0700409 try:
410 message = "\n".join(message_lines)
Simran Basi1e10e922015-04-16 15:09:56 -0700411
Aviv Keshet0b7bab02016-10-20 17:17:36 -0700412 if not dry_run:
413 # send out a email report of failure
414 if len(message) > 2 and mail_on_failure:
415 tko_utils.dprint("Sending email report of failure on %s to %s"
416 % (jobname, job.user))
417 mailfailure(jobname, job, message)
mbligh96cf0512008-04-17 15:25:38 +0000418
Dan Shie5d063f2017-09-29 15:37:34 -0700419 # Upload perf values to the perf dashboard, if applicable.
420 for test in job.tests:
421 perf_uploader.upload_test(job, test, jobname)
422
423 # Upload job details to Sponge.
424 sponge_url = sponge_utils.upload_results(job, log=tko_utils.dprint)
425 if sponge_url:
426 job.keyval_dict['sponge_url'] = sponge_url
427
Prathmesh Prabhu30dee862018-04-18 20:24:20 -0700428 _write_job_to_db(db, jobname, job)
mbligh96cf0512008-04-17 15:25:38 +0000429
Dan Shib0af6212017-07-17 14:40:02 -0700430 # Verify the job data is written to the database.
431 if job.tests:
Prathmesh Prabhuc2a8a6a2018-04-19 16:23:32 -0700432 tests_in_db = db.find_tests(job.job_idx)
Dan Shib0af6212017-07-17 14:40:02 -0700433 tests_in_db_count = len(tests_in_db) if tests_in_db else 0
434 if tests_in_db_count != len(job.tests):
435 tko_utils.dprint(
436 'Failed to find enough tests for job_idx: %d. The '
437 'job should have %d tests, only found %d tests.' %
Prathmesh Prabhuc2a8a6a2018-04-19 16:23:32 -0700438 (job.job_idx, len(job.tests), tests_in_db_count))
Dan Shib0af6212017-07-17 14:40:02 -0700439 metrics.Counter(
440 'chromeos/autotest/result/db_save_failure',
441 description='The number of times parse failed to '
442 'save job to TKO database.').increment()
443
Aviv Keshet0b7bab02016-10-20 17:17:36 -0700444 # Although the cursor has autocommit, we still need to force it to
445 # commit existing changes before we can use django models, otherwise
446 # it will go into deadlock when django models try to start a new
447 # trasaction while the current one has not finished yet.
448 db.commit()
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700449
Aviv Keshet0b7bab02016-10-20 17:17:36 -0700450 # Handle retry job.
451 orig_afe_job_id = job_keyval.get(constants.RETRY_ORIGINAL_JOB_ID,
452 None)
453 if orig_afe_job_id:
454 orig_job_idx = tko_models.Job.objects.get(
455 afe_job_id=orig_afe_job_id).job_idx
Prathmesh Prabhuc2a8a6a2018-04-19 16:23:32 -0700456 _invalidate_original_tests(orig_job_idx, job.job_idx)
Simran Basi59ca5ac2016-09-22 16:57:56 -0700457 except Exception as e:
Simran Basi59ca5ac2016-09-22 16:57:56 -0700458 tko_utils.dprint("Hit exception while uploading to tko db:\n%s" %
459 traceback.format_exc())
Simran Basi59ca5ac2016-09-22 16:57:56 -0700460 raise e
Fang Deng9ec66802014-04-28 19:04:33 +0000461
jamesren7a522042010-06-10 22:53:55 +0000462 # Serializing job into a binary file
Michael Tangc89efa72017-08-03 14:27:10 -0700463 export_tko_to_file = global_config.global_config.get_config_value(
464 'AUTOSERV', 'export_tko_job_to_file', type=bool, default=False)
Michael Tang8303a372017-08-11 11:03:50 -0700465
466 binary_file_name = os.path.join(path, "job.serialize")
Michael Tangc89efa72017-08-03 14:27:10 -0700467 if export_tko_to_file:
Michael Tangc89efa72017-08-03 14:27:10 -0700468 export_tko_job_to_file(job, jobname, binary_file_name)
jamesren4826cc42010-06-15 20:33:22 +0000469
Aviv Keshet0b7bab02016-10-20 17:17:36 -0700470 if not dry_run:
471 db.commit()
mbligh26b992b2008-02-19 15:46:21 +0000472
Shuqian Zhao31425d52016-12-07 09:35:03 -0800473 # Generate a suite report.
474 # Check whether this is a suite job, a suite job will be a hostless job, its
475 # jobname will be <JOB_ID>-<USERNAME>/hostless, the suite field will not be
Shuqian Zhaoa42bba12017-03-10 14:20:11 -0800476 # NULL. Only generate timeline report when datastore_parent_key is given.
Shuqian Zhao31425d52016-12-07 09:35:03 -0800477 try:
Shuqian Zhaoa42bba12017-03-10 14:20:11 -0800478 datastore_parent_key = job_keyval.get('datastore_parent_key', None)
Ningning Xiabbba11f2018-03-16 13:35:24 -0700479 provision_job_id = job_keyval.get('provision_job_id', None)
Shuqian Zhaoa42bba12017-03-10 14:20:11 -0800480 if (suite_report and jobname.endswith('/hostless')
Prathmesh Prabhu6d4d8b62018-04-18 18:24:54 -0700481 and job.suite and datastore_parent_key):
Shuqian Zhao31425d52016-12-07 09:35:03 -0800482 tko_utils.dprint('Start dumping suite timing report...')
483 timing_log = os.path.join(path, 'suite_timing.log')
484 dump_cmd = ("%s/site_utils/dump_suite_report.py %s "
485 "--output='%s' --debug" %
Prathmesh Prabhu6d4d8b62018-04-18 18:24:54 -0700486 (common.autotest_dir, job.afe_job_id,
Shuqian Zhao31425d52016-12-07 09:35:03 -0800487 timing_log))
Ningning Xiabbba11f2018-03-16 13:35:24 -0700488
489 if provision_job_id is not None:
490 dump_cmd += " --provision_job_id=%d" % int(provision_job_id)
491
Shuqian Zhao31425d52016-12-07 09:35:03 -0800492 subprocess.check_output(dump_cmd, shell=True)
493 tko_utils.dprint('Successfully finish dumping suite timing report')
494
Shuqian Zhao19e62fb2017-01-09 10:10:14 -0800495 if (datastore_creds and export_to_gcloud_path
496 and os.path.exists(export_to_gcloud_path)):
Shuqian Zhaoa42bba12017-03-10 14:20:11 -0800497 upload_cmd = [export_to_gcloud_path, datastore_creds,
498 timing_log, '--parent_key',
Shuqian Zhao4ff74732017-03-30 16:20:10 -0700499 datastore_parent_key]
Shuqian Zhao19e62fb2017-01-09 10:10:14 -0800500 tko_utils.dprint('Start exporting timeline report to gcloud')
Shuqian Zhaoa42bba12017-03-10 14:20:11 -0800501 subprocess.check_output(upload_cmd)
Shuqian Zhao19e62fb2017-01-09 10:10:14 -0800502 tko_utils.dprint('Successfully export timeline report to '
503 'gcloud')
504 else:
505 tko_utils.dprint('DEBUG: skip exporting suite timeline to '
506 'gcloud, because either gcloud creds or '
507 'export_to_gcloud script is not found.')
Shuqian Zhao31425d52016-12-07 09:35:03 -0800508 except Exception as e:
Shuqian Zhao19e62fb2017-01-09 10:10:14 -0800509 tko_utils.dprint("WARNING: fail to dump/export suite report. "
510 "Error:\n%s" % e)
Shuqian Zhao31425d52016-12-07 09:35:03 -0800511
Dan Shi5f626332016-01-27 15:25:58 -0800512 # Mark GS_OFFLOADER_NO_OFFLOAD in gs_offloader_instructions at the end of
513 # the function, so any failure, e.g., db connection error, will stop
514 # gs_offloader_instructions being updated, and logs can be uploaded for
515 # troubleshooting.
516 if job_successful:
517 # Check if we should not offload this test's results.
518 if job_keyval.get(constants.JOB_OFFLOAD_FAILURES_KEY, False):
519 # Update the gs_offloader_instructions json file.
520 gs_instructions_file = os.path.join(
521 path, constants.GS_OFFLOADER_INSTRUCTIONS)
522 gs_offloader_instructions = {}
523 if os.path.exists(gs_instructions_file):
524 with open(gs_instructions_file, 'r') as f:
525 gs_offloader_instructions = json.load(f)
526
527 gs_offloader_instructions[constants.GS_OFFLOADER_NO_OFFLOAD] = True
528 with open(gs_instructions_file, 'w') as f:
529 json.dump(gs_offloader_instructions, f)
530
531
Prathmesh Prabhu30dee862018-04-18 20:24:20 -0700532def _write_job_to_db(db, jobname, job):
Prathmesh Prabhu8957a342018-04-18 18:29:09 -0700533 """Write all TKO data associated with a job to DB.
534
535 This updates the job object as a side effect.
536
537 @param db: tko.db.db_sql object.
538 @param jobname: Name of the job to write.
539 @param job: tko.models.job object.
540 """
541 db.insert_or_update_machine(job)
Prathmesh Prabhu30dee862018-04-18 20:24:20 -0700542 db.insert_job(jobname, job)
Prathmesh Prabhu17905882018-04-18 22:09:08 -0700543 db.insert_or_update_task_reference(
544 job,
545 'skylab' if tko_utils.is_skylab_task(jobname) else 'afe',
546 )
Prathmesh Prabhu8957a342018-04-18 18:29:09 -0700547 db.update_job_keyvals(job)
548 for test in job.tests:
549 db.insert_test(job, test)
550
551
Prathmesh Prabhu42a2bb42018-04-18 18:56:16 -0700552def _find_status_log_path(path):
553 if os.path.exists(os.path.join(path, "status.log")):
554 return os.path.join(path, "status.log")
555 if os.path.exists(os.path.join(path, "status")):
556 return os.path.join(path, "status")
557 return ""
558
559
Prathmesh Prabhue06c49b2018-04-18 19:01:23 -0700560def _parse_status_log(parser, job, status_log_path):
561 status_lines = open(status_log_path).readlines()
562 parser.start(job)
563 tests = parser.end(status_lines)
564
565 # parser.end can return the same object multiple times, so filter out dups
566 job.tests = []
567 already_added = set()
568 for test in tests:
569 if test not in already_added:
570 already_added.add(test)
571 job.tests.append(test)
572
573
Prathmesh Prabhuedac1ee2018-04-18 19:16:34 -0700574def _match_existing_tests(db, job):
575 """Find entries in the DB corresponding to the job's tests, update job.
576
577 @return: Any unmatched tests in the db.
578 """
579 old_job_idx = job.job_idx
580 raw_old_tests = db.select("test_idx,subdir,test", "tko_tests",
581 {"job_idx": old_job_idx})
582 if raw_old_tests:
583 old_tests = dict(((test, subdir), test_idx)
584 for test_idx, subdir, test in raw_old_tests)
585 else:
586 old_tests = {}
587
588 for test in job.tests:
589 test_idx = old_tests.pop((test.testname, test.subdir), None)
590 if test_idx is not None:
591 test.test_idx = test_idx
592 else:
593 tko_utils.dprint("! Reparse returned new test "
594 "testname=%r subdir=%r" %
595 (test.testname, test.subdir))
596 return old_tests
597
598
599def _delete_tests_from_db(db, tests):
600 for test_idx in tests.itervalues():
601 where = {'test_idx' : test_idx}
602 db.delete('tko_iteration_result', where)
603 db.delete('tko_iteration_perf_value', where)
604 db.delete('tko_iteration_attributes', where)
605 db.delete('tko_test_attributes', where)
606 db.delete('tko_test_labels_tests', {'test_id': test_idx})
607 db.delete('tko_tests', where)
608
609
jadmanski8e9c2572008-11-11 00:29:02 +0000610def _get_job_subdirs(path):
611 """
612 Returns a list of job subdirectories at path. Returns None if the test
613 is itself a job directory. Does not recurse into the subdirs.
614 """
615 # if there's a .machines file, use it to get the subdirs
jadmanski0afbb632008-06-06 21:10:57 +0000616 machine_list = os.path.join(path, ".machines")
617 if os.path.exists(machine_list):
jadmanski42fbd072009-01-30 15:07:05 +0000618 subdirs = set(line.strip() for line in file(machine_list))
619 existing_subdirs = set(subdir for subdir in subdirs
620 if os.path.exists(os.path.join(path, subdir)))
621 if len(existing_subdirs) != 0:
622 return existing_subdirs
jadmanski8e9c2572008-11-11 00:29:02 +0000623
624 # if this dir contains ONLY subdirectories, return them
625 contents = set(os.listdir(path))
626 contents.discard(".parse.lock")
627 subdirs = set(sub for sub in contents if
628 os.path.isdir(os.path.join(path, sub)))
629 if len(contents) == len(subdirs) != 0:
630 return subdirs
631
632 # this is a job directory, or something else we don't understand
633 return None
634
635
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700636def parse_leaf_path(db, pid_file_manager, path, level, parse_options):
Fang Deng49822682014-10-21 16:29:22 -0700637 """Parse a leaf path.
638
639 @param db: database handle.
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700640 @param pid_file_manager: pidfile.PidFileManager object.
Fang Deng49822682014-10-21 16:29:22 -0700641 @param path: The path to the results to be parsed.
642 @param level: Integer, level of subdirectories to include in the job name.
Aviv Keshet687d2dc2016-10-20 15:41:16 -0700643 @param parse_options: _ParseOptions instance.
Fang Deng49822682014-10-21 16:29:22 -0700644
645 @returns: The job name of the parsed job, e.g. '123-chromeos-test/host1'
646 """
mbligha48eeb22009-03-11 16:44:43 +0000647 job_elements = path.split("/")[-level:]
648 jobname = "/".join(job_elements)
649 try:
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700650 db.run_with_retry(parse_one, db, pid_file_manager, jobname, path,
651 parse_options)
Simran Basi8de306c2016-12-21 12:04:21 -0800652 except Exception as e:
653 tko_utils.dprint("Error parsing leaf path: %s\nException:\n%s\n%s" %
654 (path, e, traceback.format_exc()))
Fang Deng49822682014-10-21 16:29:22 -0700655 return jobname
mbligha48eeb22009-03-11 16:44:43 +0000656
657
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700658def parse_path(db, pid_file_manager, path, level, parse_options):
Fang Deng49822682014-10-21 16:29:22 -0700659 """Parse a path
660
661 @param db: database handle.
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700662 @param pid_file_manager: pidfile.PidFileManager object.
Fang Deng49822682014-10-21 16:29:22 -0700663 @param path: The path to the results to be parsed.
664 @param level: Integer, level of subdirectories to include in the job name.
Aviv Keshet687d2dc2016-10-20 15:41:16 -0700665 @param parse_options: _ParseOptions instance.
Fang Deng49822682014-10-21 16:29:22 -0700666
667 @returns: A set of job names of the parsed jobs.
668 set(['123-chromeos-test/host1', '123-chromeos-test/host2'])
669 """
670 processed_jobs = set()
jadmanski8e9c2572008-11-11 00:29:02 +0000671 job_subdirs = _get_job_subdirs(path)
672 if job_subdirs is not None:
mbligha48eeb22009-03-11 16:44:43 +0000673 # parse status.log in current directory, if it exists. multi-machine
674 # synchronous server side tests record output in this directory. without
675 # this check, we do not parse these results.
676 if os.path.exists(os.path.join(path, 'status.log')):
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700677 new_job = parse_leaf_path(db, pid_file_manager, path, level,
678 parse_options)
Fang Deng49822682014-10-21 16:29:22 -0700679 processed_jobs.add(new_job)
jadmanski0afbb632008-06-06 21:10:57 +0000680 # multi-machine job
jadmanski8e9c2572008-11-11 00:29:02 +0000681 for subdir in job_subdirs:
682 jobpath = os.path.join(path, subdir)
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700683 new_jobs = parse_path(db, pid_file_manager, jobpath, level + 1,
684 parse_options)
Fang Deng49822682014-10-21 16:29:22 -0700685 processed_jobs.update(new_jobs)
jadmanski0afbb632008-06-06 21:10:57 +0000686 else:
687 # single machine job
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700688 new_job = parse_leaf_path(db, pid_file_manager, path, level,
689 parse_options)
Fang Deng49822682014-10-21 16:29:22 -0700690 processed_jobs.add(new_job)
691 return processed_jobs
692
693
Prathmesh Prabhu3e319da2017-08-30 19:13:03 -0700694def _detach_from_parent_process():
695 """Allow reparenting the parse process away from caller.
696
697 When monitor_db is run via upstart, restarting the job sends SIGTERM to
698 the whole process group. This makes us immune from that.
699 """
700 if os.getpid() != os.getpgid(0):
701 os.setsid()
mblighbb7b8912006-10-08 03:59:02 +0000702
Aviv Keshet6469b532018-07-17 16:44:39 -0700703
mbligh96cf0512008-04-17 15:25:38 +0000704def main():
Aviv Keshet6469b532018-07-17 16:44:39 -0700705 """tko_parse entry point."""
706 options, args = parse_args()
707
708 # We are obliged to use indirect=False, not use the SetupTsMonGlobalState
709 # context manager, and add a manual flush, because tko/parse is expected to
710 # be a very short lived (<1 min) script when working effectively, and we
711 # can't afford to either a) wait for up to 1min for metrics to flush at the
712 # end or b) drop metrics that were sent within the last minute of execution.
713 site_utils.SetupTsMonGlobalState('tko_parse', indirect=False,
714 short_lived=True)
715 try:
716 with metrics.SuccessCounter('chromeos/autotest/tko_parse/runs'):
717 _main_with_options(options, args)
718 finally:
719 metrics.Flush()
720
721
722def _main_with_options(options, args):
723 """Entry point with options parsed and metrics already set up."""
Fang Deng49822682014-10-21 16:29:22 -0700724 start_time = datetime.datetime.now()
725 # Record the processed jobs so that
726 # we can send the duration of parsing to metadata db.
727 processed_jobs = set()
728
Prathmesh Prabhu3e319da2017-08-30 19:13:03 -0700729 if options.detach:
730 _detach_from_parent_process()
731
Aviv Keshet0b7bab02016-10-20 17:17:36 -0700732 parse_options = _ParseOptions(options.reparse, options.mailit,
Shuqian Zhao19e62fb2017-01-09 10:10:14 -0800733 options.dry_run, options.suite_report,
734 options.datastore_creds,
735 options.export_to_gcloud_path)
jadmanski0afbb632008-06-06 21:10:57 +0000736 results_dir = os.path.abspath(args[0])
737 assert os.path.exists(results_dir)
mbligh96cf0512008-04-17 15:25:38 +0000738
jadmanskid5ab8c52008-12-03 16:27:07 +0000739 pid_file_manager = pidfile.PidFileManager("parser", results_dir)
mbligh96cf0512008-04-17 15:25:38 +0000740
jadmanskid5ab8c52008-12-03 16:27:07 +0000741 if options.write_pidfile:
742 pid_file_manager.open_file()
mbligh96cf0512008-04-17 15:25:38 +0000743
jadmanskid5ab8c52008-12-03 16:27:07 +0000744 try:
745 # build up the list of job dirs to parse
746 if options.singledir:
747 jobs_list = [results_dir]
748 else:
749 jobs_list = [os.path.join(results_dir, subdir)
750 for subdir in os.listdir(results_dir)]
751
752 # build up the database
753 db = tko_db.db(autocommit=False, host=options.db_host,
754 user=options.db_user, password=options.db_pass,
755 database=options.db_name)
756
757 # parse all the jobs
758 for path in jobs_list:
759 lockfile = open(os.path.join(path, ".parse.lock"), "w")
760 flags = fcntl.LOCK_EX
761 if options.noblock:
mblighdb18b0e2009-01-30 00:34:32 +0000762 flags |= fcntl.LOCK_NB
jadmanskid5ab8c52008-12-03 16:27:07 +0000763 try:
764 fcntl.flock(lockfile, flags)
765 except IOError, e:
mblighdb18b0e2009-01-30 00:34:32 +0000766 # lock is not available and nonblock has been requested
jadmanskid5ab8c52008-12-03 16:27:07 +0000767 if e.errno == errno.EWOULDBLOCK:
768 lockfile.close()
769 continue
770 else:
771 raise # something unexpected happened
772 try:
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700773 new_jobs = parse_path(db, pid_file_manager, path, options.level,
774 parse_options)
Fang Deng49822682014-10-21 16:29:22 -0700775 processed_jobs.update(new_jobs)
mbligh9e936402009-05-13 20:42:17 +0000776
jadmanskid5ab8c52008-12-03 16:27:07 +0000777 finally:
778 fcntl.flock(lockfile, fcntl.LOCK_UN)
jadmanski0afbb632008-06-06 21:10:57 +0000779 lockfile.close()
mblighe97e0e62009-05-21 01:41:58 +0000780
Dan Shib7a36ea2017-02-28 21:52:20 -0800781 except Exception as e:
jadmanskid5ab8c52008-12-03 16:27:07 +0000782 pid_file_manager.close_file(1)
783 raise
784 else:
785 pid_file_manager.close_file(0)
Fang Deng49822682014-10-21 16:29:22 -0700786 duration_secs = (datetime.datetime.now() - start_time).total_seconds()
mbligh71d340d2008-03-05 15:51:16 +0000787
mbligh532cb272007-11-26 18:54:20 +0000788
mbligh96cf0512008-04-17 15:25:38 +0000789if __name__ == "__main__":
Aviv Keshet6469b532018-07-17 16:44:39 -0700790 main()