blob: 3d88b261e2e9e2d2b00fd769e979a5bd48684d61 [file] [log] [blame]
Mike Frysinger0e2cb7a2019-08-20 17:04:52 -04001#!/usr/bin/python2 -u
mblighc2514542008-02-19 15:54:26 +00002
Aviv Keshet687d2dc2016-10-20 15:41:16 -07003import collections
Aviv Keshet687d2dc2016-10-20 15:41:16 -07004import errno
5import fcntl
Simran Basi1e10e922015-04-16 15:09:56 -07006import json
Aviv Keshet687d2dc2016-10-20 15:41:16 -07007import optparse
8import os
9import socket
Shuqian Zhao31425d52016-12-07 09:35:03 -080010import subprocess
Aviv Keshet687d2dc2016-10-20 15:41:16 -070011import sys
Dan Shi11e35062017-11-03 10:09:05 -070012import time
Aviv Keshet687d2dc2016-10-20 15:41:16 -070013import traceback
mblighbb7b8912006-10-08 03:59:02 +000014
mbligh96cf0512008-04-17 15:25:38 +000015import common
Dan Shi4f8c0242017-07-07 15:34:49 -070016from autotest_lib.client.bin.result_tools import utils as result_utils
17from autotest_lib.client.bin.result_tools import utils_lib as result_utils_lib
18from autotest_lib.client.bin.result_tools import runner as result_runner
19from autotest_lib.client.common_lib import control_data
Benny Peakefeb775c2017-02-08 15:14:14 -080020from autotest_lib.client.common_lib import global_config
jadmanskidb4f9b52008-12-03 22:52:53 +000021from autotest_lib.client.common_lib import mail, pidfile
Fang Deng49822682014-10-21 16:29:22 -070022from autotest_lib.client.common_lib import utils
Fang Deng49822682014-10-21 16:29:22 -070023from autotest_lib.frontend import setup_django_environment
Fang Deng9ec66802014-04-28 19:04:33 +000024from autotest_lib.frontend.tko import models as tko_models
Shuqian Zhao19e62fb2017-01-09 10:10:14 -080025from autotest_lib.server import site_utils
Fang Deng49822682014-10-21 16:29:22 -070026from autotest_lib.server.cros.dynamic_suite import constants
Benny Peaked322d3d2017-02-08 15:39:28 -080027from autotest_lib.site_utils.sponge_lib import sponge_utils
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070028from autotest_lib.tko import db as tko_db, utils as tko_utils
Luigi Semenzatoe7064812017-02-03 14:47:59 -080029from autotest_lib.tko import models, parser_lib
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -070030from autotest_lib.tko.perf_upload import perf_uploader
Alex Zamorzaevd2516e12019-10-11 17:14:28 -070031from autotest_lib.utils.side_effects import config_loader
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',
Alex Zamorzaevd2516e12019-10-11 17:14:28 -070041 'datastore_creds', 'export_to_gcloud_path',
42 'disable_perf_upload'])
Aviv Keshet687d2dc2016-10-20 15:41:16 -070043
Prathmesh Prabhu531188a2018-08-10 12:47:39 -070044_HARDCODED_CONTROL_FILE_NAMES = (
45 # client side test control, as saved in old Autotest paths.
46 'control',
47 # server side test control, as saved in old Autotest paths.
48 'control.srv',
49 # All control files, as saved in skylab.
50 'control.from_control_name',
51)
52
53
mbligh96cf0512008-04-17 15:25:38 +000054def parse_args():
Fang Deng49822682014-10-21 16:29:22 -070055 """Parse args."""
jadmanski0afbb632008-06-06 21:10:57 +000056 # build up our options parser and parse sys.argv
57 parser = optparse.OptionParser()
58 parser.add_option("-m", help="Send mail for FAILED tests",
59 dest="mailit", action="store_true")
60 parser.add_option("-r", help="Reparse the results of a job",
61 dest="reparse", action="store_true")
62 parser.add_option("-o", help="Parse a single results directory",
63 dest="singledir", action="store_true")
64 parser.add_option("-l", help=("Levels of subdirectories to include "
65 "in the job name"),
66 type="int", dest="level", default=1)
67 parser.add_option("-n", help="No blocking on an existing parse",
68 dest="noblock", action="store_true")
69 parser.add_option("-s", help="Database server hostname",
70 dest="db_host", action="store")
71 parser.add_option("-u", help="Database username", dest="db_user",
72 action="store")
73 parser.add_option("-p", help="Database password", dest="db_pass",
74 action="store")
75 parser.add_option("-d", help="Database name", dest="db_name",
76 action="store")
Aviv Keshet0b7bab02016-10-20 17:17:36 -070077 parser.add_option("--dry-run", help="Do not actually commit any results.",
78 dest="dry_run", action="store_true", default=False)
Prathmesh Prabhu3e319da2017-08-30 19:13:03 -070079 parser.add_option(
80 "--detach", action="store_true",
81 help="Detach parsing process from the caller process. Used by "
82 "monitor_db to safely restart without affecting parsing.",
83 default=False)
jadmanskid5ab8c52008-12-03 16:27:07 +000084 parser.add_option("--write-pidfile",
85 help="write pidfile (.parser_execute)",
86 dest="write_pidfile", action="store_true",
87 default=False)
Fang Deng49822682014-10-21 16:29:22 -070088 parser.add_option("--record-duration",
Prathmesh Prabhu77769452018-04-17 13:30:50 -070089 help="[DEPRECATED] Record timing to metadata db",
Fang Deng49822682014-10-21 16:29:22 -070090 dest="record_duration", action="store_true",
91 default=False)
Shuqian Zhao31425d52016-12-07 09:35:03 -080092 parser.add_option("--suite-report",
93 help=("Allows parsing job to attempt to create a suite "
Shuqian Zhao19e62fb2017-01-09 10:10:14 -080094 "timeline report, if it detects that the job being "
Shuqian Zhao31425d52016-12-07 09:35:03 -080095 "parsed is a suite job."),
96 dest="suite_report", action="store_true",
97 default=False)
Shuqian Zhao19e62fb2017-01-09 10:10:14 -080098 parser.add_option("--datastore-creds",
99 help=("The path to gcloud datastore credentials file, "
100 "which will be used to upload suite timeline "
101 "report to gcloud. If not specified, the one "
102 "defined in shadow_config will be used."),
103 dest="datastore_creds", action="store", default=None)
104 parser.add_option("--export-to-gcloud-path",
105 help=("The path to export_to_gcloud script. Please find "
106 "chromite path on your server. The script is under "
107 "chromite/bin/."),
108 dest="export_to_gcloud_path", action="store",
109 default=None)
Alex Zamorzaevd2516e12019-10-11 17:14:28 -0700110 parser.add_option("--disable-perf-upload",
111 help=("Do not upload perf results to chrome perf."),
112 dest="disable_perf_upload", action="store_true",
113 default=False)
jadmanski0afbb632008-06-06 21:10:57 +0000114 options, args = parser.parse_args()
mbligh74fc0462007-11-05 20:24:17 +0000115
jadmanski0afbb632008-06-06 21:10:57 +0000116 # we need a results directory
117 if len(args) == 0:
118 tko_utils.dprint("ERROR: at least one results directory must "
119 "be provided")
120 parser.print_help()
121 sys.exit(1)
mbligh74fc0462007-11-05 20:24:17 +0000122
Shuqian Zhao19e62fb2017-01-09 10:10:14 -0800123 if not options.datastore_creds:
124 gcloud_creds = global_config.global_config.get_config_value(
125 'GCLOUD', 'cidb_datastore_writer_creds', default=None)
126 options.datastore_creds = (site_utils.get_creds_abspath(gcloud_creds)
127 if gcloud_creds else None)
128
129 if not options.export_to_gcloud_path:
130 export_script = 'chromiumos/chromite/bin/export_to_gcloud'
131 # If it is a lab server, the script is under ~chromeos-test/
132 if os.path.exists(os.path.expanduser('~chromeos-test/%s' %
133 export_script)):
134 path = os.path.expanduser('~chromeos-test/%s' % export_script)
135 # If it is a local workstation, it is probably under ~/
136 elif os.path.exists(os.path.expanduser('~/%s' % export_script)):
137 path = os.path.expanduser('~/%s' % export_script)
138 # If it is not found anywhere, the default will be set to None.
139 else:
140 path = None
141 options.export_to_gcloud_path = path
142
jadmanski0afbb632008-06-06 21:10:57 +0000143 # pass the options back
144 return options, args
mbligh74fc0462007-11-05 20:24:17 +0000145
146
mbligh96cf0512008-04-17 15:25:38 +0000147def format_failure_message(jobname, kernel, testname, status, reason):
Fang Deng49822682014-10-21 16:29:22 -0700148 """Format failure message with the given information.
149
150 @param jobname: String representing the job name.
151 @param kernel: String representing the kernel.
152 @param testname: String representing the test name.
153 @param status: String representing the test status.
154 @param reason: String representing the reason.
155
156 @return: Failure message as a string.
157 """
jadmanski0afbb632008-06-06 21:10:57 +0000158 format_string = "%-12s %-20s %-12s %-10s %s"
159 return format_string % (jobname, kernel, testname, status, reason)
mblighb85e6b02006-10-08 17:20:56 +0000160
mblighbb7b8912006-10-08 03:59:02 +0000161
mbligh96cf0512008-04-17 15:25:38 +0000162def mailfailure(jobname, job, message):
Fang Deng49822682014-10-21 16:29:22 -0700163 """Send an email about the failure.
164
165 @param jobname: String representing the job name.
166 @param job: A job object.
167 @param message: The message to mail.
168 """
jadmanski0afbb632008-06-06 21:10:57 +0000169 message_lines = [""]
170 message_lines.append("The following tests FAILED for this job")
171 message_lines.append("http://%s/results/%s" %
172 (socket.gethostname(), jobname))
173 message_lines.append("")
174 message_lines.append(format_failure_message("Job name", "Kernel",
175 "Test name", "FAIL/WARN",
176 "Failure reason"))
177 message_lines.append(format_failure_message("=" * 8, "=" * 6, "=" * 8,
178 "=" * 8, "=" * 14))
179 message_header = "\n".join(message_lines)
mbligh96cf0512008-04-17 15:25:38 +0000180
jadmanski0afbb632008-06-06 21:10:57 +0000181 subject = "AUTOTEST: FAILED tests from job %s" % jobname
182 mail.send("", job.user, "", subject, message_header + message)
mbligh006f2302007-09-13 20:46:46 +0000183
184
Fang Deng9ec66802014-04-28 19:04:33 +0000185def _invalidate_original_tests(orig_job_idx, retry_job_idx):
186 """Retry tests invalidates original tests.
187
188 Whenever a retry job is complete, we want to invalidate the original
189 job's test results, such that the consumers of the tko database
190 (e.g. tko frontend, wmatrix) could figure out which results are the latest.
191
192 When a retry job is parsed, we retrieve the original job's afe_job_id
193 from the retry job's keyvals, which is then converted to tko job_idx and
194 passed into this method as |orig_job_idx|.
195
196 In this method, we are going to invalidate the rows in tko_tests that are
197 associated with the original job by flipping their 'invalid' bit to True.
198 In addition, in tko_tests, we also maintain a pointer from the retry results
199 to the original results, so that later we can always know which rows in
200 tko_tests are retries and which are the corresponding original results.
201 This is done by setting the field 'invalidates_test_idx' of the tests
202 associated with the retry job.
203
204 For example, assume Job(job_idx=105) are retried by Job(job_idx=108), after
205 this method is run, their tko_tests rows will look like:
206 __________________________________________________________________________
207 test_idx| job_idx | test | ... | invalid | invalidates_test_idx
208 10 | 105 | dummy_Fail.Error| ... | 1 | NULL
209 11 | 105 | dummy_Fail.Fail | ... | 1 | NULL
210 ...
211 20 | 108 | dummy_Fail.Error| ... | 0 | 10
212 21 | 108 | dummy_Fail.Fail | ... | 0 | 11
213 __________________________________________________________________________
214 Note the invalid bits of the rows for Job(job_idx=105) are set to '1'.
215 And the 'invalidates_test_idx' fields of the rows for Job(job_idx=108)
216 are set to 10 and 11 (the test_idx of the rows for the original job).
217
218 @param orig_job_idx: An integer representing the original job's
219 tko job_idx. Tests associated with this job will
220 be marked as 'invalid'.
221 @param retry_job_idx: An integer representing the retry job's
222 tko job_idx. The field 'invalidates_test_idx'
223 of the tests associated with this job will be updated.
224
225 """
226 msg = 'orig_job_idx: %s, retry_job_idx: %s' % (orig_job_idx, retry_job_idx)
227 if not orig_job_idx or not retry_job_idx:
228 tko_utils.dprint('ERROR: Could not invalidate tests: ' + msg)
229 # Using django models here makes things easier, but make sure that
230 # before this method is called, all other relevant transactions have been
231 # committed to avoid race condition. In the long run, we might consider
232 # to make the rest of parser use django models.
233 orig_tests = tko_models.Test.objects.filter(job__job_idx=orig_job_idx)
234 retry_tests = tko_models.Test.objects.filter(job__job_idx=retry_job_idx)
235
236 # Invalidate original tests.
237 orig_tests.update(invalid=True)
238
239 # Maintain a dictionary that maps (test, subdir) to original tests.
240 # Note that within the scope of a job, (test, subdir) uniquelly
241 # identifies a test run, but 'test' does not.
242 # In a control file, one could run the same test with different
243 # 'subdir_tag', for example,
244 # job.run_test('dummy_Fail', tag='Error', subdir_tag='subdir_1')
245 # job.run_test('dummy_Fail', tag='Error', subdir_tag='subdir_2')
246 # In tko, we will get
247 # (test='dummy_Fail.Error', subdir='dummy_Fail.Error.subdir_1')
248 # (test='dummy_Fail.Error', subdir='dummy_Fail.Error.subdir_2')
249 invalidated_tests = {(orig_test.test, orig_test.subdir): orig_test
250 for orig_test in orig_tests}
251 for retry in retry_tests:
252 # It is possible that (retry.test, retry.subdir) doesn't exist
253 # in invalidated_tests. This could happen when the original job
254 # didn't run some of its tests. For example, a dut goes offline
255 # since the beginning of the job, in which case invalidated_tests
256 # will only have one entry for 'SERVER_JOB'.
257 orig_test = invalidated_tests.get((retry.test, retry.subdir), None)
258 if orig_test:
259 retry.invalidates_test = orig_test
260 retry.save()
261 tko_utils.dprint('DEBUG: Invalidated tests associated to job: ' + msg)
262
263
Dan Shi4f8c0242017-07-07 15:34:49 -0700264def _throttle_result_size(path):
265 """Limit the total size of test results for the given path.
266
267 @param path: Path of the result directory.
268 """
269 if not result_runner.ENABLE_RESULT_THROTTLING:
270 tko_utils.dprint(
271 'Result throttling is not enabled. Skipping throttling %s' %
272 path)
273 return
274
Prathmesh Prabhu5f8c9202018-08-14 10:54:31 -0700275 max_result_size_KB = _max_result_size_from_control(path)
Prathmesh Prabhudfbaac12018-08-10 12:43:47 -0700276 if max_result_size_KB is None:
277 max_result_size_KB = control_data.DEFAULT_MAX_RESULT_SIZE_KB
278
279 try:
280 result_utils.execute(path, max_result_size_KB)
281 except:
282 tko_utils.dprint(
283 'Failed to throttle result size of %s.\nDetails %s' %
284 (path, traceback.format_exc()))
285
286
287def _max_result_size_from_control(path):
288 """Gets the max result size set in a control file, if any.
289
290 If not overrides is found, returns None.
291 """
Prathmesh Prabhu531188a2018-08-10 12:47:39 -0700292 for control_file in _HARDCODED_CONTROL_FILE_NAMES:
Dan Shi4f8c0242017-07-07 15:34:49 -0700293 control = os.path.join(path, control_file)
Prathmesh Prabhu5848ed02018-08-10 12:46:13 -0700294 if not os.path.exists(control):
295 continue
296
Dan Shi4f8c0242017-07-07 15:34:49 -0700297 try:
298 max_result_size_KB = control_data.parse_control(
299 control, raise_warnings=False).max_result_size_KB
Dan Shi4f8c0242017-07-07 15:34:49 -0700300 if max_result_size_KB != control_data.DEFAULT_MAX_RESULT_SIZE_KB:
Prathmesh Prabhudfbaac12018-08-10 12:43:47 -0700301 return max_result_size_KB
Dan Shi4f8c0242017-07-07 15:34:49 -0700302 except IOError as e:
303 tko_utils.dprint(
304 'Failed to access %s. Error: %s\nDetails %s' %
305 (control, e, traceback.format_exc()))
306 except control_data.ControlVariableException as e:
307 tko_utils.dprint(
308 'Failed to parse %s. Error: %s\nDetails %s' %
309 (control, e, traceback.format_exc()))
Prathmesh Prabhudfbaac12018-08-10 12:43:47 -0700310 return None
Dan Shi4f8c0242017-07-07 15:34:49 -0700311
312
Michael Tangc89efa72017-08-03 14:27:10 -0700313def export_tko_job_to_file(job, jobname, filename):
314 """Exports the tko job to disk file.
315
316 @param job: database object.
317 @param jobname: the job name as string.
318 @param filename: The path to the results to be parsed.
319 """
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700320 from autotest_lib.tko import job_serializer
Michael Tangc89efa72017-08-03 14:27:10 -0700321
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700322 serializer = job_serializer.JobSerializer()
323 serializer.serialize_to_binary(job, jobname, filename)
Michael Tangc89efa72017-08-03 14:27:10 -0700324
325
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700326def parse_one(db, pid_file_manager, jobname, path, parse_options):
Fang Deng49822682014-10-21 16:29:22 -0700327 """Parse a single job. Optionally send email on failure.
328
329 @param db: database object.
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700330 @param pid_file_manager: pidfile.PidFileManager object.
Fang Deng49822682014-10-21 16:29:22 -0700331 @param jobname: the tag used to search for existing job in db,
332 e.g. '1234-chromeos-test/host1'
333 @param path: The path to the results to be parsed.
Aviv Keshet687d2dc2016-10-20 15:41:16 -0700334 @param parse_options: _ParseOptions instance.
jadmanski0afbb632008-06-06 21:10:57 +0000335 """
Aviv Keshet687d2dc2016-10-20 15:41:16 -0700336 reparse = parse_options.reparse
337 mail_on_failure = parse_options.mail_on_failure
Aviv Keshet0b7bab02016-10-20 17:17:36 -0700338 dry_run = parse_options.dry_run
Shuqian Zhao31425d52016-12-07 09:35:03 -0800339 suite_report = parse_options.suite_report
Shuqian Zhao19e62fb2017-01-09 10:10:14 -0800340 datastore_creds = parse_options.datastore_creds
341 export_to_gcloud_path = parse_options.export_to_gcloud_path
Aviv Keshet687d2dc2016-10-20 15:41:16 -0700342
jadmanski0afbb632008-06-06 21:10:57 +0000343 tko_utils.dprint("\nScanning %s (%s)" % (jobname, path))
jadmanski9b6babf2009-04-21 17:57:40 +0000344 old_job_idx = db.find_job(jobname)
Prathmesh Prabhuedac1ee2018-04-18 19:16:34 -0700345 if old_job_idx is not None and not reparse:
346 tko_utils.dprint("! Job is already parsed, done")
347 return
mbligh96cf0512008-04-17 15:25:38 +0000348
jadmanski0afbb632008-06-06 21:10:57 +0000349 # look up the status version
jadmanskidb4f9b52008-12-03 22:52:53 +0000350 job_keyval = models.job.read_keyval(path)
351 status_version = job_keyval.get("status_version", 0)
jadmanski6e8bf752008-05-14 00:17:48 +0000352
Luigi Semenzatoe7064812017-02-03 14:47:59 -0800353 parser = parser_lib.parser(status_version)
jadmanski0afbb632008-06-06 21:10:57 +0000354 job = parser.make_job(path)
Prathmesh Prabhue06c49b2018-04-18 19:01:23 -0700355 tko_utils.dprint("+ Parsing dir=%s, jobname=%s" % (path, jobname))
356 status_log_path = _find_status_log_path(path)
357 if not status_log_path:
jadmanski0afbb632008-06-06 21:10:57 +0000358 tko_utils.dprint("! Unable to parse job, no status file")
359 return
Prathmesh Prabhue06c49b2018-04-18 19:01:23 -0700360 _parse_status_log(parser, job, status_log_path)
jadmanski9b6babf2009-04-21 17:57:40 +0000361
Prathmesh Prabhuedac1ee2018-04-18 19:16:34 -0700362 if old_job_idx is not None:
363 job.job_idx = old_job_idx
364 unmatched_tests = _match_existing_tests(db, job)
Aviv Keshet0b7bab02016-10-20 17:17:36 -0700365 if not dry_run:
Prathmesh Prabhuedac1ee2018-04-18 19:16:34 -0700366 _delete_tests_from_db(db, unmatched_tests)
mbligh96cf0512008-04-17 15:25:38 +0000367
Prathmesh Prabhu30dee862018-04-18 20:24:20 -0700368 job.afe_job_id = tko_utils.get_afe_job_id(jobname)
Prathmesh Prabhu17905882018-04-18 22:09:08 -0700369 job.skylab_task_id = tko_utils.get_skylab_task_id(jobname)
Prathmesh Prabhud25f15a2018-05-03 13:49:58 -0700370 job.afe_parent_job_id = job_keyval.get(constants.PARENT_JOB_ID)
371 job.skylab_parent_task_id = job_keyval.get(constants.PARENT_JOB_ID)
Benny Peakefeb775c2017-02-08 15:14:14 -0800372 job.build = None
373 job.board = None
374 job.build_version = None
375 job.suite = None
376 if job.label:
377 label_info = site_utils.parse_job_name(job.label)
378 if label_info:
379 job.build = label_info.get('build', None)
380 job.build_version = label_info.get('build_version', None)
381 job.board = label_info.get('board', None)
382 job.suite = label_info.get('suite', None)
383
Alex Zamorzaev2d886122019-09-10 16:17:54 -0700384 if 'suite' in job.keyval_dict:
385 job.suite = job.keyval_dict['suite']
386
Dan Shi4f8c0242017-07-07 15:34:49 -0700387 result_utils_lib.LOG = tko_utils.dprint
388 _throttle_result_size(path)
389
Dan Shiffd5b822017-07-14 11:16:23 -0700390 # Record test result size to job_keyvals
Dan Shi11e35062017-11-03 10:09:05 -0700391 start_time = time.time()
Dan Shiffd5b822017-07-14 11:16:23 -0700392 result_size_info = site_utils.collect_result_sizes(
393 path, log=tko_utils.dprint)
Dan Shi11e35062017-11-03 10:09:05 -0700394 tko_utils.dprint('Finished collecting result sizes after %s seconds' %
395 (time.time()-start_time))
Dan Shiffd5b822017-07-14 11:16:23 -0700396 job.keyval_dict.update(result_size_info.__dict__)
397
Dan Shiffd5b822017-07-14 11:16:23 -0700398 # TODO(dshi): Update sizes with sponge_invocation.xml and throttle it.
Dan Shi96c3bdc2017-05-24 11:34:30 -0700399
jadmanski0afbb632008-06-06 21:10:57 +0000400 # check for failures
401 message_lines = [""]
Simran Basi1e10e922015-04-16 15:09:56 -0700402 job_successful = True
jadmanski0afbb632008-06-06 21:10:57 +0000403 for test in job.tests:
404 if not test.subdir:
405 continue
Sida Liuafe550a2017-09-03 19:03:40 -0700406 tko_utils.dprint("* testname, subdir, status, reason: %s %s %s %s"
407 % (test.testname, test.subdir, test.status,
408 test.reason))
Allen Li82c68ce2019-03-20 15:30:29 -0700409 if test.status not in ('GOOD', 'WARN'):
Simran Basi1e10e922015-04-16 15:09:56 -0700410 job_successful = False
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700411 pid_file_manager.num_tests_failed += 1
jadmanski0afbb632008-06-06 21:10:57 +0000412 message_lines.append(format_failure_message(
413 jobname, test.kernel.base, test.subdir,
414 test.status, test.reason))
Simran Basi1e10e922015-04-16 15:09:56 -0700415
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700416 message = "\n".join(message_lines)
mbligh96cf0512008-04-17 15:25:38 +0000417
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700418 if not dry_run:
419 # send out a email report of failure
420 if len(message) > 2 and mail_on_failure:
421 tko_utils.dprint("Sending email report of failure on %s to %s"
422 % (jobname, job.user))
423 mailfailure(jobname, job, message)
Dan Shie5d063f2017-09-29 15:37:34 -0700424
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700425 # Upload perf values to the perf dashboard, if applicable.
Alex Zamorzaevd2516e12019-10-11 17:14:28 -0700426 if parse_options.disable_perf_upload:
427 tko_utils.dprint("Skipping results upload to chrome perf as it is "
428 "disabled by config")
429 else:
430 for test in job.tests:
431 perf_uploader.upload_test(job, test, jobname)
Dan Shie5d063f2017-09-29 15:37:34 -0700432
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700433 # Upload job details to Sponge.
434 sponge_url = sponge_utils.upload_results(job, log=tko_utils.dprint)
435 if sponge_url:
436 job.keyval_dict['sponge_url'] = sponge_url
mbligh96cf0512008-04-17 15:25:38 +0000437
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700438 _write_job_to_db(db, jobname, job)
Dan Shib0af6212017-07-17 14:40:02 -0700439
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700440 # Verify the job data is written to the database.
441 if job.tests:
442 tests_in_db = db.find_tests(job.job_idx)
443 tests_in_db_count = len(tests_in_db) if tests_in_db else 0
444 if tests_in_db_count != len(job.tests):
445 tko_utils.dprint(
446 'Failed to find enough tests for job_idx: %d. The '
447 'job should have %d tests, only found %d tests.' %
448 (job.job_idx, len(job.tests), tests_in_db_count))
449 metrics.Counter(
450 'chromeos/autotest/result/db_save_failure',
451 description='The number of times parse failed to '
452 'save job to TKO database.').increment()
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700453
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700454 # Although the cursor has autocommit, we still need to force it to
455 # commit existing changes before we can use django models, otherwise
456 # it will go into deadlock when django models try to start a new
457 # trasaction while the current one has not finished yet.
458 db.commit()
459
460 # Handle retry job.
461 orig_afe_job_id = job_keyval.get(constants.RETRY_ORIGINAL_JOB_ID,
462 None)
463 if orig_afe_job_id:
464 orig_job_idx = tko_models.Job.objects.get(
465 afe_job_id=orig_afe_job_id).job_idx
466 _invalidate_original_tests(orig_job_idx, job.job_idx)
Fang Deng9ec66802014-04-28 19:04:33 +0000467
jamesren7a522042010-06-10 22:53:55 +0000468 # Serializing job into a binary file
Michael Tangc89efa72017-08-03 14:27:10 -0700469 export_tko_to_file = global_config.global_config.get_config_value(
470 'AUTOSERV', 'export_tko_job_to_file', type=bool, default=False)
Michael Tang8303a372017-08-11 11:03:50 -0700471
472 binary_file_name = os.path.join(path, "job.serialize")
Michael Tangc89efa72017-08-03 14:27:10 -0700473 if export_tko_to_file:
Michael Tangc89efa72017-08-03 14:27:10 -0700474 export_tko_job_to_file(job, jobname, binary_file_name)
jamesren4826cc42010-06-15 20:33:22 +0000475
Aviv Keshet0b7bab02016-10-20 17:17:36 -0700476 if not dry_run:
477 db.commit()
mbligh26b992b2008-02-19 15:46:21 +0000478
Shuqian Zhao31425d52016-12-07 09:35:03 -0800479 # Generate a suite report.
480 # Check whether this is a suite job, a suite job will be a hostless job, its
481 # jobname will be <JOB_ID>-<USERNAME>/hostless, the suite field will not be
Shuqian Zhaoa42bba12017-03-10 14:20:11 -0800482 # NULL. Only generate timeline report when datastore_parent_key is given.
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700483 datastore_parent_key = job_keyval.get('datastore_parent_key', None)
484 provision_job_id = job_keyval.get('provision_job_id', None)
485 if (suite_report and jobname.endswith('/hostless')
486 and job.suite and datastore_parent_key):
487 tko_utils.dprint('Start dumping suite timing report...')
488 timing_log = os.path.join(path, 'suite_timing.log')
489 dump_cmd = ("%s/site_utils/dump_suite_report.py %s "
490 "--output='%s' --debug" %
491 (common.autotest_dir, job.afe_job_id,
492 timing_log))
Ningning Xiabbba11f2018-03-16 13:35:24 -0700493
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700494 if provision_job_id is not None:
495 dump_cmd += " --provision_job_id=%d" % int(provision_job_id)
Ningning Xiabbba11f2018-03-16 13:35:24 -0700496
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700497 subprocess.check_output(dump_cmd, shell=True)
498 tko_utils.dprint('Successfully finish dumping suite timing report')
Shuqian Zhao31425d52016-12-07 09:35:03 -0800499
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700500 if (datastore_creds and export_to_gcloud_path
501 and os.path.exists(export_to_gcloud_path)):
502 upload_cmd = [export_to_gcloud_path, datastore_creds,
503 timing_log, '--parent_key',
504 datastore_parent_key]
505 tko_utils.dprint('Start exporting timeline report to gcloud')
506 subprocess.check_output(upload_cmd)
507 tko_utils.dprint('Successfully export timeline report to '
508 'gcloud')
509 else:
510 tko_utils.dprint('DEBUG: skip exporting suite timeline to '
511 'gcloud, because either gcloud creds or '
512 'export_to_gcloud script is not found.')
Shuqian Zhao31425d52016-12-07 09:35:03 -0800513
Dan Shi5f626332016-01-27 15:25:58 -0800514 # Mark GS_OFFLOADER_NO_OFFLOAD in gs_offloader_instructions at the end of
515 # the function, so any failure, e.g., db connection error, will stop
516 # gs_offloader_instructions being updated, and logs can be uploaded for
517 # troubleshooting.
518 if job_successful:
519 # Check if we should not offload this test's results.
520 if job_keyval.get(constants.JOB_OFFLOAD_FAILURES_KEY, False):
521 # Update the gs_offloader_instructions json file.
522 gs_instructions_file = os.path.join(
523 path, constants.GS_OFFLOADER_INSTRUCTIONS)
524 gs_offloader_instructions = {}
525 if os.path.exists(gs_instructions_file):
526 with open(gs_instructions_file, 'r') as f:
527 gs_offloader_instructions = json.load(f)
528
529 gs_offloader_instructions[constants.GS_OFFLOADER_NO_OFFLOAD] = True
530 with open(gs_instructions_file, 'w') as f:
531 json.dump(gs_offloader_instructions, f)
532
533
Prathmesh Prabhu30dee862018-04-18 20:24:20 -0700534def _write_job_to_db(db, jobname, job):
Prathmesh Prabhu8957a342018-04-18 18:29:09 -0700535 """Write all TKO data associated with a job to DB.
536
537 This updates the job object as a side effect.
538
539 @param db: tko.db.db_sql object.
540 @param jobname: Name of the job to write.
541 @param job: tko.models.job object.
542 """
543 db.insert_or_update_machine(job)
Prathmesh Prabhu30dee862018-04-18 20:24:20 -0700544 db.insert_job(jobname, job)
Prathmesh Prabhu17905882018-04-18 22:09:08 -0700545 db.insert_or_update_task_reference(
546 job,
547 'skylab' if tko_utils.is_skylab_task(jobname) else 'afe',
548 )
Prathmesh Prabhu8957a342018-04-18 18:29:09 -0700549 db.update_job_keyvals(job)
550 for test in job.tests:
551 db.insert_test(job, test)
552
553
Prathmesh Prabhu42a2bb42018-04-18 18:56:16 -0700554def _find_status_log_path(path):
555 if os.path.exists(os.path.join(path, "status.log")):
556 return os.path.join(path, "status.log")
557 if os.path.exists(os.path.join(path, "status")):
558 return os.path.join(path, "status")
559 return ""
560
561
Prathmesh Prabhue06c49b2018-04-18 19:01:23 -0700562def _parse_status_log(parser, job, status_log_path):
563 status_lines = open(status_log_path).readlines()
564 parser.start(job)
565 tests = parser.end(status_lines)
566
567 # parser.end can return the same object multiple times, so filter out dups
568 job.tests = []
569 already_added = set()
570 for test in tests:
571 if test not in already_added:
572 already_added.add(test)
573 job.tests.append(test)
574
575
Prathmesh Prabhuedac1ee2018-04-18 19:16:34 -0700576def _match_existing_tests(db, job):
577 """Find entries in the DB corresponding to the job's tests, update job.
578
579 @return: Any unmatched tests in the db.
580 """
581 old_job_idx = job.job_idx
582 raw_old_tests = db.select("test_idx,subdir,test", "tko_tests",
583 {"job_idx": old_job_idx})
584 if raw_old_tests:
585 old_tests = dict(((test, subdir), test_idx)
586 for test_idx, subdir, test in raw_old_tests)
587 else:
588 old_tests = {}
589
590 for test in job.tests:
591 test_idx = old_tests.pop((test.testname, test.subdir), None)
592 if test_idx is not None:
593 test.test_idx = test_idx
594 else:
595 tko_utils.dprint("! Reparse returned new test "
596 "testname=%r subdir=%r" %
597 (test.testname, test.subdir))
598 return old_tests
599
600
601def _delete_tests_from_db(db, tests):
602 for test_idx in tests.itervalues():
603 where = {'test_idx' : test_idx}
604 db.delete('tko_iteration_result', where)
605 db.delete('tko_iteration_perf_value', where)
606 db.delete('tko_iteration_attributes', where)
607 db.delete('tko_test_attributes', where)
608 db.delete('tko_test_labels_tests', {'test_id': test_idx})
609 db.delete('tko_tests', where)
610
611
jadmanski8e9c2572008-11-11 00:29:02 +0000612def _get_job_subdirs(path):
613 """
614 Returns a list of job subdirectories at path. Returns None if the test
615 is itself a job directory. Does not recurse into the subdirs.
616 """
617 # if there's a .machines file, use it to get the subdirs
jadmanski0afbb632008-06-06 21:10:57 +0000618 machine_list = os.path.join(path, ".machines")
619 if os.path.exists(machine_list):
jadmanski42fbd072009-01-30 15:07:05 +0000620 subdirs = set(line.strip() for line in file(machine_list))
621 existing_subdirs = set(subdir for subdir in subdirs
622 if os.path.exists(os.path.join(path, subdir)))
623 if len(existing_subdirs) != 0:
624 return existing_subdirs
jadmanski8e9c2572008-11-11 00:29:02 +0000625
626 # if this dir contains ONLY subdirectories, return them
627 contents = set(os.listdir(path))
628 contents.discard(".parse.lock")
629 subdirs = set(sub for sub in contents if
630 os.path.isdir(os.path.join(path, sub)))
631 if len(contents) == len(subdirs) != 0:
632 return subdirs
633
634 # this is a job directory, or something else we don't understand
635 return None
636
637
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700638def parse_leaf_path(db, pid_file_manager, path, level, parse_options):
Fang Deng49822682014-10-21 16:29:22 -0700639 """Parse a leaf path.
640
641 @param db: database handle.
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700642 @param pid_file_manager: pidfile.PidFileManager object.
Fang Deng49822682014-10-21 16:29:22 -0700643 @param path: The path to the results to be parsed.
644 @param level: Integer, level of subdirectories to include in the job name.
Aviv Keshet687d2dc2016-10-20 15:41:16 -0700645 @param parse_options: _ParseOptions instance.
Fang Deng49822682014-10-21 16:29:22 -0700646
647 @returns: The job name of the parsed job, e.g. '123-chromeos-test/host1'
648 """
mbligha48eeb22009-03-11 16:44:43 +0000649 job_elements = path.split("/")[-level:]
650 jobname = "/".join(job_elements)
Prathmesh Prabhufa654f82018-08-16 09:34:59 -0700651 db.run_with_retry(parse_one, db, pid_file_manager, jobname, path,
652 parse_options)
Fang Deng49822682014-10-21 16:29:22 -0700653 return jobname
mbligha48eeb22009-03-11 16:44:43 +0000654
655
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700656def parse_path(db, pid_file_manager, path, level, parse_options):
Fang Deng49822682014-10-21 16:29:22 -0700657 """Parse a path
658
659 @param db: database handle.
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700660 @param pid_file_manager: pidfile.PidFileManager object.
Fang Deng49822682014-10-21 16:29:22 -0700661 @param path: The path to the results to be parsed.
662 @param level: Integer, level of subdirectories to include in the job name.
Aviv Keshet687d2dc2016-10-20 15:41:16 -0700663 @param parse_options: _ParseOptions instance.
Fang Deng49822682014-10-21 16:29:22 -0700664
665 @returns: A set of job names of the parsed jobs.
666 set(['123-chromeos-test/host1', '123-chromeos-test/host2'])
667 """
668 processed_jobs = set()
jadmanski8e9c2572008-11-11 00:29:02 +0000669 job_subdirs = _get_job_subdirs(path)
670 if job_subdirs is not None:
mbligha48eeb22009-03-11 16:44:43 +0000671 # parse status.log in current directory, if it exists. multi-machine
672 # synchronous server side tests record output in this directory. without
673 # this check, we do not parse these results.
674 if os.path.exists(os.path.join(path, 'status.log')):
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700675 new_job = parse_leaf_path(db, pid_file_manager, path, level,
676 parse_options)
Fang Deng49822682014-10-21 16:29:22 -0700677 processed_jobs.add(new_job)
jadmanski0afbb632008-06-06 21:10:57 +0000678 # multi-machine job
jadmanski8e9c2572008-11-11 00:29:02 +0000679 for subdir in job_subdirs:
680 jobpath = os.path.join(path, subdir)
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700681 new_jobs = parse_path(db, pid_file_manager, jobpath, level + 1,
682 parse_options)
Fang Deng49822682014-10-21 16:29:22 -0700683 processed_jobs.update(new_jobs)
jadmanski0afbb632008-06-06 21:10:57 +0000684 else:
685 # single machine job
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700686 new_job = parse_leaf_path(db, pid_file_manager, path, level,
687 parse_options)
Fang Deng49822682014-10-21 16:29:22 -0700688 processed_jobs.add(new_job)
689 return processed_jobs
690
691
Prathmesh Prabhu3e319da2017-08-30 19:13:03 -0700692def _detach_from_parent_process():
693 """Allow reparenting the parse process away from caller.
694
695 When monitor_db is run via upstart, restarting the job sends SIGTERM to
696 the whole process group. This makes us immune from that.
697 """
698 if os.getpid() != os.getpgid(0):
699 os.setsid()
mblighbb7b8912006-10-08 03:59:02 +0000700
Aviv Keshet6469b532018-07-17 16:44:39 -0700701
mbligh96cf0512008-04-17 15:25:38 +0000702def main():
Aviv Keshet6469b532018-07-17 16:44:39 -0700703 """tko_parse entry point."""
704 options, args = parse_args()
705
706 # We are obliged to use indirect=False, not use the SetupTsMonGlobalState
707 # context manager, and add a manual flush, because tko/parse is expected to
708 # be a very short lived (<1 min) script when working effectively, and we
709 # can't afford to either a) wait for up to 1min for metrics to flush at the
710 # end or b) drop metrics that were sent within the last minute of execution.
711 site_utils.SetupTsMonGlobalState('tko_parse', indirect=False,
712 short_lived=True)
713 try:
714 with metrics.SuccessCounter('chromeos/autotest/tko_parse/runs'):
715 _main_with_options(options, args)
716 finally:
717 metrics.Flush()
718
719
720def _main_with_options(options, args):
721 """Entry point with options parsed and metrics already set up."""
Fang Deng49822682014-10-21 16:29:22 -0700722 # Record the processed jobs so that
723 # we can send the duration of parsing to metadata db.
724 processed_jobs = set()
725
Prathmesh Prabhu3e319da2017-08-30 19:13:03 -0700726 if options.detach:
727 _detach_from_parent_process()
728
Alex Zamorzaevd2516e12019-10-11 17:14:28 -0700729 results_dir = os.path.abspath(args[0])
730 assert os.path.exists(results_dir)
731
732 _update_db_config_from_json(options, results_dir)
733
Aviv Keshet0b7bab02016-10-20 17:17:36 -0700734 parse_options = _ParseOptions(options.reparse, options.mailit,
Shuqian Zhao19e62fb2017-01-09 10:10:14 -0800735 options.dry_run, options.suite_report,
736 options.datastore_creds,
Alex Zamorzaevd2516e12019-10-11 17:14:28 -0700737 options.export_to_gcloud_path,
738 options.disable_perf_upload)
mbligh96cf0512008-04-17 15:25:38 +0000739
jadmanskid5ab8c52008-12-03 16:27:07 +0000740 pid_file_manager = pidfile.PidFileManager("parser", results_dir)
mbligh96cf0512008-04-17 15:25:38 +0000741
jadmanskid5ab8c52008-12-03 16:27:07 +0000742 if options.write_pidfile:
743 pid_file_manager.open_file()
mbligh96cf0512008-04-17 15:25:38 +0000744
jadmanskid5ab8c52008-12-03 16:27:07 +0000745 try:
746 # build up the list of job dirs to parse
747 if options.singledir:
748 jobs_list = [results_dir]
749 else:
750 jobs_list = [os.path.join(results_dir, subdir)
751 for subdir in os.listdir(results_dir)]
752
753 # build up the database
754 db = tko_db.db(autocommit=False, host=options.db_host,
755 user=options.db_user, password=options.db_pass,
756 database=options.db_name)
757
758 # parse all the jobs
759 for path in jobs_list:
760 lockfile = open(os.path.join(path, ".parse.lock"), "w")
761 flags = fcntl.LOCK_EX
762 if options.noblock:
mblighdb18b0e2009-01-30 00:34:32 +0000763 flags |= fcntl.LOCK_NB
jadmanskid5ab8c52008-12-03 16:27:07 +0000764 try:
765 fcntl.flock(lockfile, flags)
766 except IOError, e:
mblighdb18b0e2009-01-30 00:34:32 +0000767 # lock is not available and nonblock has been requested
jadmanskid5ab8c52008-12-03 16:27:07 +0000768 if e.errno == errno.EWOULDBLOCK:
769 lockfile.close()
770 continue
771 else:
772 raise # something unexpected happened
773 try:
Prathmesh Prabhub1241d12018-04-19 18:09:43 -0700774 new_jobs = parse_path(db, pid_file_manager, path, options.level,
775 parse_options)
Fang Deng49822682014-10-21 16:29:22 -0700776 processed_jobs.update(new_jobs)
mbligh9e936402009-05-13 20:42:17 +0000777
jadmanskid5ab8c52008-12-03 16:27:07 +0000778 finally:
779 fcntl.flock(lockfile, fcntl.LOCK_UN)
jadmanski0afbb632008-06-06 21:10:57 +0000780 lockfile.close()
mblighe97e0e62009-05-21 01:41:58 +0000781
Dan Shib7a36ea2017-02-28 21:52:20 -0800782 except Exception as e:
jadmanskid5ab8c52008-12-03 16:27:07 +0000783 pid_file_manager.close_file(1)
784 raise
785 else:
786 pid_file_manager.close_file(0)
mbligh71d340d2008-03-05 15:51:16 +0000787
mbligh532cb272007-11-26 18:54:20 +0000788
Alex Zamorzaevd2516e12019-10-11 17:14:28 -0700789def _update_db_config_from_json(options, test_results_dir):
790 """Uptade DB config options using a side_effects_config.json file.
791
792 @param options: parsed args to be updated.
793 @param test_results_dir: path to test results dir.
794
795 @raises: json_format.ParseError if the file is not a valid JSON.
796 ValueError if the JSON config is incomplete.
797 OSError if some files from the JSON config are missing.
798 """
799 # results_dir passed to tko/parse is a subdir of the root results dir
800 config_dir = os.path.join(test_results_dir, os.pardir)
801 tko_utils.dprint("Attempting to read side_effects.Config from %s" %
802 config_dir)
803 config = config_loader.load(config_dir)
804
805 if config:
806 tko_utils.dprint("Validating side_effects.Config.tko")
807 config_loader.validate_tko(config)
808
809 tko_utils.dprint("Using the following DB config params from "
810 "side_effects.Config.tko:\n%s" % config.tko)
811 options.db_host = config.tko.proxy_socket
812 options.db_user = config.tko.mysql_user
813
814 with open(config.tko.mysql_password_file, 'r') as f:
815 options.db_pass = f.read().rstrip('\n')
816
817 options.disable_perf_upload = not config.chrome_perf.enabled
818 else:
819 tko_utils.dprint("No side_effects.Config found in %s - "
820 "defaulting to DB config values from shadow config" % config_dir)
821
822
mbligh96cf0512008-04-17 15:25:38 +0000823if __name__ == "__main__":
Aviv Keshet6469b532018-07-17 16:44:39 -0700824 main()