blob: 51284b0da6618afdf68f751a5d08bf0436bc8d47 [file] [log] [blame]
showard26b7ec72009-12-21 22:43:57 +00001"""\
2Functions to expose over the RPC interface.
3"""
4
5__author__ = 'jamesren@google.com (James Ren)'
6
7
jamesren9a6f5f62010-05-05 22:55:54 +00008import os, re
jamesrenc3940222010-02-19 21:57:37 +00009import common
10from django.db import models as django_models
11from autotest_lib.frontend import thread_local
12from autotest_lib.frontend.afe import model_logic, models as afe_models
13from autotest_lib.frontend.afe import rpc_utils as afe_rpc_utils
jamesren9a6f5f62010-05-05 22:55:54 +000014from autotest_lib.frontend.afe import rpc_interface as afe_rpc_interface
jamesren3e9f6092010-03-11 21:32:10 +000015from autotest_lib.frontend.tko import models as tko_models
jamesrenb852bce2010-04-07 20:36:13 +000016from autotest_lib.frontend.planner import models, rpc_utils, model_attributes
jamesren4be631f2010-04-08 23:01:22 +000017from autotest_lib.frontend.planner import failure_actions
jamesrenc3940222010-02-19 21:57:37 +000018from autotest_lib.client.common_lib import utils
19
20# basic getter/setter calls
21# TODO: deprecate the basic calls and reimplement them in the REST framework
22
23def get_plan(id):
24 return afe_rpc_utils.prepare_for_serialization(
25 models.Plan.smart_get(id).get_object_dict())
26
27
28def modify_plan(id, **data):
29 models.Plan.smart_get(id).update_object(data)
30
31
jamesren3e9f6092010-03-11 21:32:10 +000032def modify_test_run(id, **data):
33 models.TestRun.objects.get(id=id).update_object(data)
34
35
36def modify_host(id, **data):
37 models.Host.objects.get(id=id).update_object(data)
38
39
jamesren3e9f6092010-03-11 21:32:10 +000040def add_job(plan_id, test_config_id, afe_job_id):
41 models.Job.objects.create(
42 plan=models.Plan.objects.get(id=plan_id),
43 test_config=models.TestConfig.objects.get(id=test_config_id),
44 afe_job=afe_models.Job.objects.get(id=afe_job_id))
45
46
jamesrenc3940222010-02-19 21:57:37 +000047# more advanced calls
48
jamesren9a6f5f62010-05-05 22:55:54 +000049def submit_plan(name, hosts, host_labels, tests, support=None,
50 label_override=None, additional_parameters=None):
jamesrenc3940222010-02-19 21:57:37 +000051 """
52 Submits a plan to the Test Planner
53
54 @param name: the name of the plan
55 @param hosts: a list of hostnames
56 @param host_labels: a list of host labels. The hosts under test will update
57 to reflect changes in the label
jamesren3e9f6092010-03-11 21:32:10 +000058 @param tests: an ordered list of dictionaries:
59 alias: an alias for the test
60 control_file: the test control file
61 is_server: True if is a server-side control file
62 estimated_runtime: estimated number of hours this test
63 will run
jamesrendbeebf82010-04-08 22:58:26 +000064 @param support: the global support script
jamesrenc3940222010-02-19 21:57:37 +000065 @param label_override: label to prepend to all AFE jobs for this test plan.
66 Defaults to the plan name.
jamesren9a6f5f62010-05-05 22:55:54 +000067 @param additional_parameters: A mapping of AdditionalParameters to apply to
68 this test plan, as an ordered list. Each item
69 of the list is a dictionary:
70 hostname_regex: A regular expression; the
71 additional parameter in the
72 value will be applied if the
73 hostname matches this regex
74 param_type: The type of additional parameter
75 param_values: A dictionary of key=value pairs
76 for this parameter
77 example:
78 [{'hostname_regex': 'host[0-9]',
79 'param_type': 'Verify',
80 'param_values': {'key1': 'value1',
81 'key2': 'value2'}},
82 {'hostname_regex': '.*',
83 'param_type': 'Verify',
84 'param_values': {'key': 'value'}}]
85
86 Currently, the only (non-site-specific)
87 param_type available is 'Verify'. Setting
88 these parameters allows the user to specify
89 arguments to the
90 job.run_test('verify_test', ...) line at the
91 beginning of the wrapped control file for each
92 test
jamesrenc3940222010-02-19 21:57:37 +000093 """
94 host_objects = []
95 label_objects = []
96
97 for host in hosts or []:
98 try:
99 host_objects.append(
100 afe_models.Host.valid_objects.get(hostname=host))
101 except afe_models.Host.DoesNotExist:
102 raise model_logic.ValidationError(
103 {'hosts': 'host %s does not exist' % host})
104
105 for label in host_labels or []:
106 try:
107 label_objects.append(afe_models.Label.valid_objects.get(name=label))
108 except afe_models.Label.DoesNotExist:
109 raise model_logic.ValidationError(
110 {'host_labels': 'host label %s does not exist' % label})
111
jamesren3e9f6092010-03-11 21:32:10 +0000112 aliases_seen = set()
113 test_required_fields = (
114 'alias', 'control_file', 'is_server', 'estimated_runtime')
115 for test in tests:
116 for field in test_required_fields:
117 if field not in test:
118 raise model_logic.ValidationError(
119 {'tests': 'field %s is required' % field})
120
121 alias = test['alias']
122 if alias in aliases_seen:
123 raise model_logic.Validationerror(
124 {'tests': 'alias %s occurs more than once' % alias})
125 aliases_seen.add(alias)
126
jamesrenc3940222010-02-19 21:57:37 +0000127 plan, created = models.Plan.objects.get_or_create(name=name)
128 if not created:
129 raise model_logic.ValidationError(
130 {'name': 'Plan name %s already exists' % name})
131
132 try:
jamesren9a6f5f62010-05-05 22:55:54 +0000133 rpc_utils.set_additional_parameters(plan, additional_parameters)
jamesrenc3940222010-02-19 21:57:37 +0000134 label = rpc_utils.create_plan_label(plan)
jamesren3e9f6092010-03-11 21:32:10 +0000135 try:
136 for i, test in enumerate(tests):
137 control, _ = models.ControlFile.objects.get_or_create(
138 contents=test['control_file'])
139 models.TestConfig.objects.create(
140 plan=plan, alias=test['alias'], control_file=control,
141 is_server=test['is_server'], execution_order=i,
142 estimated_runtime=test['estimated_runtime'])
143
144 plan.label_override = label_override
145 plan.support = support or ''
146 plan.save()
147
148 plan.owners.add(afe_models.User.current_user())
149
150 for host in host_objects:
151 planner_host = models.Host.objects.create(plan=plan, host=host)
152
153 plan.host_labels.add(*label_objects)
154
155 rpc_utils.start_plan(plan, label)
156
157 return plan.id
158 except:
159 label.delete()
160 raise
jamesrenc3940222010-02-19 21:57:37 +0000161 except:
162 plan.delete()
163 raise
164
jamesrenc3940222010-02-19 21:57:37 +0000165
166def get_hosts(plan_id):
167 """
168 Gets the hostnames of all the hosts in this test plan.
169
170 Resolves host labels in the plan.
171 """
172 plan = models.Plan.smart_get(plan_id)
173
174 hosts = set(plan.hosts.all().values_list('hostname', flat=True))
175 for label in plan.host_labels.all():
176 hosts.update(label.host_set.all().values_list('hostname', flat=True))
177
178 return afe_rpc_utils.prepare_for_serialization(hosts)
179
180
181def get_atomic_group_control_file():
182 """
183 Gets the control file to apply the atomic group for a set of machines
184 """
185 return rpc_utils.lazy_load(os.path.join(os.path.dirname(__file__),
186 'set_atomic_group_control.srv'))
jamesren3e9f6092010-03-11 21:32:10 +0000187
188
189def get_next_test_configs(plan_id):
190 """
191 Gets information about the next planner test configs that need to be run
192
193 @param plan_id: the ID or name of the test plan
194 @return a dictionary:
195 complete: True or False, shows test plan completion
196 next_configs: a list of dictionaries:
197 host: ID of the host
198 next_test_config_id: ID of the next Planner test to run
199 """
200 plan = models.Plan.smart_get(plan_id)
201
202 result = {'next_configs': []}
203
204 rpc_utils.update_hosts_table(plan)
205 for host in models.Host.objects.filter(plan=plan):
jamesrendbeebf82010-04-08 22:58:26 +0000206 next_test_config = rpc_utils.compute_next_test_config(plan, host)
207 if next_test_config:
208 config = {'next_test_config_id': next_test_config.id,
209 'next_test_config_alias': next_test_config.alias,
jamesren3e9f6092010-03-11 21:32:10 +0000210 'host': host.host.hostname}
211 result['next_configs'].append(config)
212
213 rpc_utils.check_for_completion(plan)
214 result['complete'] = plan.complete
215
216 return result
217
218
219def update_test_runs(plan_id):
220 """
221 Add all applicable TKO jobs to the Planner DB tables
222
223 Looks for tests in the TKO tables that were started as a part of the test
224 plan, and add them to the Planner tables.
225
226 Also updates the status of the test run if the underlying TKO test move from
227 an active status to a completed status.
228
229 @return a list of dictionaries:
230 status: the status of the new (or updated) test run
231 tko_test_idx: the ID of the TKO test added
232 hostname: the host added
233 """
jamesren4be631f2010-04-08 23:01:22 +0000234 plan = models.Plan.smart_get(plan_id)
jamesren3e9f6092010-03-11 21:32:10 +0000235 updated = []
236
237 for planner_job in plan.job_set.all():
238 known_statuses = dict((test_run.tko_test.test_idx, test_run.status)
239 for test_run in planner_job.testrun_set.all())
240 tko_tests_for_job = tko_models.Test.objects.filter(
241 job__afe_job_id=planner_job.afe_job.id)
242
243 for tko_test in tko_tests_for_job:
244 status = rpc_utils.compute_test_run_status(tko_test.status.word)
245 needs_update = (tko_test.test_idx not in known_statuses or
246 status != known_statuses[tko_test.test_idx])
247 if needs_update:
248 hostnames = tko_test.machine.hostname.split(',')
249 for hostname in hostnames:
250 rpc_utils.add_test_run(
251 plan, planner_job, tko_test, hostname, status)
252 updated.append({'status': status,
253 'tko_test_idx': tko_test.test_idx,
254 'hostname': hostname})
255
256 return updated
jamesrenb852bce2010-04-07 20:36:13 +0000257
258
259def get_failures(plan_id):
260 """
261 Gets a list of the untriaged failures associated with this plan
262
263 @return a list of dictionaries:
264 id: the failure ID, for passing back to triage the failure
265 group: the group for the failure. Normally the same as the
266 reason, but can be different for custom queries
267 machine: the failed machine
268 blocked: True if the failure caused the machine to block
269 test_name: Concatenation of the Planner alias and the TKO test
270 name for the failed test
271 reason: test failure reason
272 seen: True if the failure is marked as "seen"
273 """
274 plan = models.Plan.smart_get(plan_id)
275 result = {}
276
277 failures = plan.testrun_set.filter(
278 finalized=True, triaged=False,
279 status=model_attributes.TestRunStatus.FAILED)
jamesren62758242010-04-28 18:08:25 +0000280 failures = failures.order_by('seen').select_related('test_job__test',
281 'host__host',
282 'tko_test')
jamesrenb852bce2010-04-07 20:36:13 +0000283 for failure in failures:
jamesren62758242010-04-28 18:08:25 +0000284 test_name = '%s: %s' % (
jamesrenb852bce2010-04-07 20:36:13 +0000285 failure.test_job.test_config.alias, failure.tko_test.test)
286
287 group_failures = result.setdefault(failure.tko_test.reason, [])
288 failure_dict = {'id': failure.id,
289 'machine': failure.host.host.hostname,
290 'blocked': bool(failure.host.blocked),
291 'test_name': test_name,
292 'reason': failure.tko_test.reason,
293 'seen': bool(failure.seen)}
294 group_failures.append(failure_dict)
295
296 return result
297
298
jamesrendbeebf82010-04-08 22:58:26 +0000299def get_test_runs(**filter_data):
300 """
301 Gets a list of test runs that match the filter data.
302
303 Returns a list of expanded TestRun object dictionaries. Specifically, the
304 "host" and "test_job" fields are expanded. Additionally, the "test_config"
305 field of the "test_job" expansion is also expanded.
306 """
307 result = []
308 for test_run in models.TestRun.objects.filter(**filter_data):
309 test_run_dict = test_run.get_object_dict()
310 test_run_dict['host'] = test_run.host.get_object_dict()
311 test_run_dict['test_job'] = test_run.test_job.get_object_dict()
312 test_run_dict['test_job']['test_config'] = (
313 test_run.test_job.test_config.get_object_dict())
314 result.append(test_run_dict)
315 return result
316
317
318def skip_test(test_config_id, hostname):
319 """
320 Marks a test config as "skipped" for a given host
321 """
322 config = models.TestConfig.objects.get(id=test_config_id)
323 config.skipped_hosts.add(afe_models.Host.objects.get(hostname=hostname))
324
325
jamesren4be631f2010-04-08 23:01:22 +0000326def mark_failures_as_seen(failure_ids):
327 """
328 Marks a set of failures as 'seen'
329
330 @param failure_ids: A list of failure IDs, as returned by get_failures(), to
331 mark as seen
332 """
333 models.TestRun.objects.filter(id__in=failure_ids).update(seen=True)
334
335
jamesren62758242010-04-28 18:08:25 +0000336def process_failures(failure_ids, host_action, test_action, labels=(),
337 keyvals=None, bugs=(), reason=None, invalidate=False):
jamesren4be631f2010-04-08 23:01:22 +0000338 """
339 Triage a failure
340
341 @param failure_id: The failure ID, as returned by get_failures()
342 @param host_action: One of 'Block', 'Unblock', 'Reinstall'
343 @param test_action: One of 'Skip', 'Rerun'
344
345 @param labels: Test labels to apply, by name
346 @param keyvals: Dictionary of job keyvals to add (or replace)
347 @param bugs: List of bug IDs to associate with this failure
348 @param reason: An override for the test failure reason
349 @param invalidate: True if failure should be invalidated for the purposes of
350 reporting. Defaults to False.
351 """
jamesren4be631f2010-04-08 23:01:22 +0000352 host_choices = failure_actions.HostAction.values
353 test_choices = failure_actions.TestAction.values
354 if host_action not in host_choices:
355 raise model_logic.ValidationError(
356 {'host_action': ('host action %s not valid; must be one of %s'
357 % (host_action, ', '.join(host_choices)))})
358 if test_action not in test_choices:
359 raise model_logic.ValidationError(
360 {'test_action': ('test action %s not valid; must be one of %s'
361 % (test_action, ', '.join(test_choices)))})
362
jamesren62758242010-04-28 18:08:25 +0000363 for failure_id in failure_ids:
364 rpc_utils.process_failure(
365 failure_id=failure_id, host_action=host_action,
366 test_action=test_action, labels=labels, keyvals=keyvals,
367 bugs=bugs, reason=reason, invalidate=invalidate)
jamesren4be631f2010-04-08 23:01:22 +0000368
369
jamesren9a6f5f62010-05-05 22:55:54 +0000370def generate_test_config(alias, afe_test_name=None,
371 estimated_runtime=0, **kwargs):
372 """
373 Creates and returns a test config suitable for passing into submit_plan()
374
375 Also accepts optional parameters to pass directly in to the AFE RPC
376 interface's generate_control_file() method.
377
378 @param alias: The alias for the test
379 @param afe_test_name: The name of the test, as shown on AFE
380 @param estimated_runtime: Estimated number of hours this test is expected to
381 run. For reporting purposes.
382 """
383 if afe_test_name is None:
384 afe_test_name = alias
385 alias = alias.replace(' ', '_')
386
387 control = afe_rpc_interface.generate_control_file(tests=[afe_test_name],
388 **kwargs)
389
390 return {'alias': alias,
391 'control_file': control['control_file'],
392 'is_server': control['is_server'],
393 'estimated_runtime': estimated_runtime}
394
395
396def get_wrapped_test_config(id, hostname, run_verify):
397 """
398 Gets the TestConfig object identified by the ID
399
400 Returns the object dict of the TestConfig, plus an additional
401 'wrapped_control_file' value, which includes the pre-processing that the
402 ControlParameters specify.
403
404 @param hostname: Hostname of the machine this test config will run on
405 @param run_verify: Set to True or False to override the default behavior
406 (which is to run the verify test unless the skip_verify
407 ControlParameter is set)
408 """
409 test_config = models.TestConfig.objects.get(id=id)
410 object_dict = test_config.get_object_dict()
411 object_dict['control_file'] = test_config.control_file.get_object_dict()
412 object_dict['wrapped_control_file'] = rpc_utils.wrap_control_file(
413 plan=test_config.plan, hostname=hostname,
414 run_verify=run_verify, test_config=test_config)
415
416 return object_dict
417
418
419def generate_additional_parameters(hostname_regex, param_type, param_values):
420 """
421 Generates an AdditionalParamter dictionary, for passing in to submit_plan()
422
423 Returns a dictionary. To use in submit_job(), put this dictionary into a
424 list (possibly with other additional_parameters dictionaries)
425
426 @param hostname_regex: The hostname regular expression to match
427 @param param_type: One of get_static_data()['additional_parameter_types']
428 @param param_values: Dictionary of key=value pairs for this parameter
429 """
430 try:
431 re.compile(hostname_regex)
432 except Exception:
433 raise model_logic.ValidationError(
434 {'hostname_regex': '%s is not a valid regex' % hostname_regex})
435
436 if param_type not in model_attributes.AdditionalParameterType.values:
437 raise model_logic.ValidationError(
438 {'param_type': '%s is not a valid parameter type' % param_type})
439
440 if type(param_values) is not dict:
441 raise model_logic.ValidationError(
442 {'param_values': '%s is not a dictionary' % repr(param_values)})
443
444 return {'hostname_regex': hostname_regex,
445 'param_type': param_type,
446 'param_values': param_values}
447
448
jamesren9af703a2010-05-06 19:41:21 +0000449def get_machine_view_data(plan_id):
450 """
451 Gets the data required for the web frontend Machine View.
452
453 @param plan_id: The ID of the test plan
454 @return An array. Each element is a dictionary:
455 machine: The name of the machine
456 status: The machine's status (one of
457 model_attributes.HostStatus)
458 bug_ids: List of the IDs for the bugs filed
459 tests_run: An array of dictionaries:
460 test_name: The TKO name of the test
461 success: True if the test passed
462 """
463 plan = models.Plan.smart_get(plan_id)
464 result = []
465 for host in plan.host_set.all():
466 tests_run = []
467
468 machine = host.host.hostname
jamesren55fc38a2010-05-07 21:32:54 +0000469 host_status = host.status()
jamesren9af703a2010-05-06 19:41:21 +0000470 bug_ids = set()
471
472 testruns = plan.testrun_set.filter(host=host, invalidated=False,
473 finalized=True)
474 for testrun in testruns:
475 test_name = testrun.tko_test.test
jamesren55fc38a2010-05-07 21:32:54 +0000476 test_status = testrun.tko_test.status.word
jamesren9af703a2010-05-06 19:41:21 +0000477 testrun_bug_ids = testrun.bugs.all().values_list(
478 'external_uid', flat=True)
479
480 tests_run.append({'test_name': test_name,
jamesren55fc38a2010-05-07 21:32:54 +0000481 'status': test_status})
jamesren9af703a2010-05-06 19:41:21 +0000482 bug_ids.update(testrun_bug_ids)
483
484 result.append({'machine': machine,
jamesren55fc38a2010-05-07 21:32:54 +0000485 'status': host_status,
jamesren9af703a2010-05-06 19:41:21 +0000486 'tests_run': tests_run,
487 'bug_ids': list(bug_ids)})
488 return result
489
490
jamesren2e48bcb2010-05-18 20:41:36 +0000491def get_overview_data(plan_ids):
492 """
493 Gets the data for the Overview tab
494
495 @param plan_ids: A list of the plans, by id or name
496 @return A dictionary - keys are plan names, values are dictionaries of data:
497 machines: A list of dictionaries:
498 hostname: The machine's hostname
499 status: The host's status
500 passed: True if the machine passed the test plan. A 'pass' means
501 that, for every test configuration in the plan, the
502 machine had at least one AFE job with no failed tests.
503 'passed' could also be None, meaning that this host is
504 still running tests.
505 bugs: A list of the bugs filed
506 test_configs: A list of dictionaries, each representing a test
507 config:
508 complete: Number of hosts that have completed this test
509 config
510 estimated_runtime: Number of hours this test config is
511 expected to run on each host
512 """
513 plans = models.Plan.smart_get_bulk(plan_ids)
514 result = {}
515
516 for plan in plans:
517 machines = []
518 for host in plan.host_set.all():
519 machines.append({'hostname': host.host.hostname,
520 'status': host.status(),
521 'passed': rpc_utils.compute_passed(host)})
522
523 bugs = set()
524 for testrun in plan.testrun_set.all():
525 bugs.update(testrun.bugs.values_list('external_uid', flat=True))
526
527 test_configs = []
528 for test_config in plan.testconfig_set.all():
529 complete_statuses = afe_models.HostQueueEntry.COMPLETE_STATUSES
530 complete_jobs = test_config.job_set.filter(
531 afe_job__hostqueueentry__status__in=complete_statuses)
532 complete_afe_jobs = afe_models.Job.objects.filter(
533 id__in=complete_jobs.values_list('afe_job', flat=True))
534
535 complete_hosts = afe_models.Host.objects.filter(
536 hostqueueentry__job__in=complete_afe_jobs)
537 complete_hosts |= test_config.skipped_hosts.all()
538
539 test_configs.append(
540 {'complete': complete_hosts.distinct().count(),
541 'estimated_runtime': test_config.estimated_runtime})
542
543 plan_data = {'machines': machines,
544 'bugs': list(bugs),
545 'test_configs': test_configs}
546 result[plan.name] = plan_data
547
548 return result
549
550
jamesren1602f3b2010-04-09 20:46:29 +0000551def get_motd():
552 return afe_rpc_utils.get_motd()
553
554
jamesrenb852bce2010-04-07 20:36:13 +0000555def get_static_data():
jamesren1602f3b2010-04-09 20:46:29 +0000556 result = {'motd': get_motd(),
jamesren4be631f2010-04-08 23:01:22 +0000557 'host_actions': sorted(failure_actions.HostAction.values),
jamesren9a6f5f62010-05-05 22:55:54 +0000558 'test_actions': sorted(failure_actions.TestAction.values),
559 'additional_parameter_types':
jamesren2e48bcb2010-05-18 20:41:36 +0000560 sorted(model_attributes.AdditionalParameterType.values),
561 'host_statuses': sorted(model_attributes.HostStatus.values)}
jamesrenb852bce2010-04-07 20:36:13 +0000562 return result