blob: a974b9f13f70e389fa63b64cc42e6afb6a6b74af [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
jamesrenc3940222010-02-19 21:57:37 +00008import os
9import 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
jamesren3e9f6092010-03-11 21:32:10 +000014from autotest_lib.frontend.tko import models as tko_models
jamesrenb852bce2010-04-07 20:36:13 +000015from autotest_lib.frontend.planner import models, rpc_utils, model_attributes
jamesren4be631f2010-04-08 23:01:22 +000016from autotest_lib.frontend.planner import failure_actions
jamesrenc3940222010-02-19 21:57:37 +000017from autotest_lib.client.common_lib import utils
18
19# basic getter/setter calls
20# TODO: deprecate the basic calls and reimplement them in the REST framework
21
22def get_plan(id):
23 return afe_rpc_utils.prepare_for_serialization(
24 models.Plan.smart_get(id).get_object_dict())
25
26
27def modify_plan(id, **data):
28 models.Plan.smart_get(id).update_object(data)
29
30
jamesren3e9f6092010-03-11 21:32:10 +000031def modify_test_run(id, **data):
32 models.TestRun.objects.get(id=id).update_object(data)
33
34
35def modify_host(id, **data):
36 models.Host.objects.get(id=id).update_object(data)
37
38
39def get_test_config(id):
40 return afe_rpc_utils.prepare_rows_as_nested_dicts(
41 models.TestConfig.objects.filter(id=id), ('control_file',))[0]
42
43
44def add_job(plan_id, test_config_id, afe_job_id):
45 models.Job.objects.create(
46 plan=models.Plan.objects.get(id=plan_id),
47 test_config=models.TestConfig.objects.get(id=test_config_id),
48 afe_job=afe_models.Job.objects.get(id=afe_job_id))
49
50
jamesrenc3940222010-02-19 21:57:37 +000051# more advanced calls
52
53def submit_plan(name, hosts, host_labels, tests,
54 support=None, label_override=None):
55 """
56 Submits a plan to the Test Planner
57
58 @param name: the name of the plan
59 @param hosts: a list of hostnames
60 @param host_labels: a list of host labels. The hosts under test will update
61 to reflect changes in the label
jamesren3e9f6092010-03-11 21:32:10 +000062 @param tests: an ordered list of dictionaries:
63 alias: an alias for the test
64 control_file: the test control file
65 is_server: True if is a server-side control file
66 estimated_runtime: estimated number of hours this test
67 will run
jamesrendbeebf82010-04-08 22:58:26 +000068 @param support: the global support script
jamesrenc3940222010-02-19 21:57:37 +000069 @param label_override: label to prepend to all AFE jobs for this test plan.
70 Defaults to the plan name.
71 """
72 host_objects = []
73 label_objects = []
74
75 for host in hosts or []:
76 try:
77 host_objects.append(
78 afe_models.Host.valid_objects.get(hostname=host))
79 except afe_models.Host.DoesNotExist:
80 raise model_logic.ValidationError(
81 {'hosts': 'host %s does not exist' % host})
82
83 for label in host_labels or []:
84 try:
85 label_objects.append(afe_models.Label.valid_objects.get(name=label))
86 except afe_models.Label.DoesNotExist:
87 raise model_logic.ValidationError(
88 {'host_labels': 'host label %s does not exist' % label})
89
jamesren3e9f6092010-03-11 21:32:10 +000090 aliases_seen = set()
91 test_required_fields = (
92 'alias', 'control_file', 'is_server', 'estimated_runtime')
93 for test in tests:
94 for field in test_required_fields:
95 if field not in test:
96 raise model_logic.ValidationError(
97 {'tests': 'field %s is required' % field})
98
99 alias = test['alias']
100 if alias in aliases_seen:
101 raise model_logic.Validationerror(
102 {'tests': 'alias %s occurs more than once' % alias})
103 aliases_seen.add(alias)
104
jamesrenc3940222010-02-19 21:57:37 +0000105 plan, created = models.Plan.objects.get_or_create(name=name)
106 if not created:
107 raise model_logic.ValidationError(
108 {'name': 'Plan name %s already exists' % name})
109
110 try:
111 label = rpc_utils.create_plan_label(plan)
jamesren3e9f6092010-03-11 21:32:10 +0000112 try:
113 for i, test in enumerate(tests):
114 control, _ = models.ControlFile.objects.get_or_create(
115 contents=test['control_file'])
116 models.TestConfig.objects.create(
117 plan=plan, alias=test['alias'], control_file=control,
118 is_server=test['is_server'], execution_order=i,
119 estimated_runtime=test['estimated_runtime'])
120
121 plan.label_override = label_override
122 plan.support = support or ''
123 plan.save()
124
125 plan.owners.add(afe_models.User.current_user())
126
127 for host in host_objects:
128 planner_host = models.Host.objects.create(plan=plan, host=host)
129
130 plan.host_labels.add(*label_objects)
131
132 rpc_utils.start_plan(plan, label)
133
134 return plan.id
135 except:
136 label.delete()
137 raise
jamesrenc3940222010-02-19 21:57:37 +0000138 except:
139 plan.delete()
140 raise
141
jamesrenc3940222010-02-19 21:57:37 +0000142
143def get_hosts(plan_id):
144 """
145 Gets the hostnames of all the hosts in this test plan.
146
147 Resolves host labels in the plan.
148 """
149 plan = models.Plan.smart_get(plan_id)
150
151 hosts = set(plan.hosts.all().values_list('hostname', flat=True))
152 for label in plan.host_labels.all():
153 hosts.update(label.host_set.all().values_list('hostname', flat=True))
154
155 return afe_rpc_utils.prepare_for_serialization(hosts)
156
157
158def get_atomic_group_control_file():
159 """
160 Gets the control file to apply the atomic group for a set of machines
161 """
162 return rpc_utils.lazy_load(os.path.join(os.path.dirname(__file__),
163 'set_atomic_group_control.srv'))
jamesren3e9f6092010-03-11 21:32:10 +0000164
165
166def get_next_test_configs(plan_id):
167 """
168 Gets information about the next planner test configs that need to be run
169
170 @param plan_id: the ID or name of the test plan
171 @return a dictionary:
172 complete: True or False, shows test plan completion
173 next_configs: a list of dictionaries:
174 host: ID of the host
175 next_test_config_id: ID of the next Planner test to run
176 """
177 plan = models.Plan.smart_get(plan_id)
178
179 result = {'next_configs': []}
180
181 rpc_utils.update_hosts_table(plan)
182 for host in models.Host.objects.filter(plan=plan):
jamesrendbeebf82010-04-08 22:58:26 +0000183 next_test_config = rpc_utils.compute_next_test_config(plan, host)
184 if next_test_config:
185 config = {'next_test_config_id': next_test_config.id,
186 'next_test_config_alias': next_test_config.alias,
jamesren3e9f6092010-03-11 21:32:10 +0000187 'host': host.host.hostname}
188 result['next_configs'].append(config)
189
190 rpc_utils.check_for_completion(plan)
191 result['complete'] = plan.complete
192
193 return result
194
195
196def update_test_runs(plan_id):
197 """
198 Add all applicable TKO jobs to the Planner DB tables
199
200 Looks for tests in the TKO tables that were started as a part of the test
201 plan, and add them to the Planner tables.
202
203 Also updates the status of the test run if the underlying TKO test move from
204 an active status to a completed status.
205
206 @return a list of dictionaries:
207 status: the status of the new (or updated) test run
208 tko_test_idx: the ID of the TKO test added
209 hostname: the host added
210 """
jamesren4be631f2010-04-08 23:01:22 +0000211 plan = models.Plan.smart_get(plan_id)
jamesren3e9f6092010-03-11 21:32:10 +0000212 updated = []
213
214 for planner_job in plan.job_set.all():
215 known_statuses = dict((test_run.tko_test.test_idx, test_run.status)
216 for test_run in planner_job.testrun_set.all())
217 tko_tests_for_job = tko_models.Test.objects.filter(
218 job__afe_job_id=planner_job.afe_job.id)
219
220 for tko_test in tko_tests_for_job:
221 status = rpc_utils.compute_test_run_status(tko_test.status.word)
222 needs_update = (tko_test.test_idx not in known_statuses or
223 status != known_statuses[tko_test.test_idx])
224 if needs_update:
225 hostnames = tko_test.machine.hostname.split(',')
226 for hostname in hostnames:
227 rpc_utils.add_test_run(
228 plan, planner_job, tko_test, hostname, status)
229 updated.append({'status': status,
230 'tko_test_idx': tko_test.test_idx,
231 'hostname': hostname})
232
233 return updated
jamesrenb852bce2010-04-07 20:36:13 +0000234
235
236def get_failures(plan_id):
237 """
238 Gets a list of the untriaged failures associated with this plan
239
240 @return a list of dictionaries:
241 id: the failure ID, for passing back to triage the failure
242 group: the group for the failure. Normally the same as the
243 reason, but can be different for custom queries
244 machine: the failed machine
245 blocked: True if the failure caused the machine to block
246 test_name: Concatenation of the Planner alias and the TKO test
247 name for the failed test
248 reason: test failure reason
249 seen: True if the failure is marked as "seen"
250 """
251 plan = models.Plan.smart_get(plan_id)
252 result = {}
253
254 failures = plan.testrun_set.filter(
255 finalized=True, triaged=False,
256 status=model_attributes.TestRunStatus.FAILED)
257 failures = failures.select_related('test_job__test', 'host__host',
258 'tko_test')
259 for failure in failures:
260 test_name = '%s:%s' % (
261 failure.test_job.test_config.alias, failure.tko_test.test)
262
263 group_failures = result.setdefault(failure.tko_test.reason, [])
264 failure_dict = {'id': failure.id,
265 'machine': failure.host.host.hostname,
266 'blocked': bool(failure.host.blocked),
267 'test_name': test_name,
268 'reason': failure.tko_test.reason,
269 'seen': bool(failure.seen)}
270 group_failures.append(failure_dict)
271
272 return result
273
274
jamesrendbeebf82010-04-08 22:58:26 +0000275def get_test_runs(**filter_data):
276 """
277 Gets a list of test runs that match the filter data.
278
279 Returns a list of expanded TestRun object dictionaries. Specifically, the
280 "host" and "test_job" fields are expanded. Additionally, the "test_config"
281 field of the "test_job" expansion is also expanded.
282 """
283 result = []
284 for test_run in models.TestRun.objects.filter(**filter_data):
285 test_run_dict = test_run.get_object_dict()
286 test_run_dict['host'] = test_run.host.get_object_dict()
287 test_run_dict['test_job'] = test_run.test_job.get_object_dict()
288 test_run_dict['test_job']['test_config'] = (
289 test_run.test_job.test_config.get_object_dict())
290 result.append(test_run_dict)
291 return result
292
293
294def skip_test(test_config_id, hostname):
295 """
296 Marks a test config as "skipped" for a given host
297 """
298 config = models.TestConfig.objects.get(id=test_config_id)
299 config.skipped_hosts.add(afe_models.Host.objects.get(hostname=hostname))
300
301
jamesren4be631f2010-04-08 23:01:22 +0000302def mark_failures_as_seen(failure_ids):
303 """
304 Marks a set of failures as 'seen'
305
306 @param failure_ids: A list of failure IDs, as returned by get_failures(), to
307 mark as seen
308 """
309 models.TestRun.objects.filter(id__in=failure_ids).update(seen=True)
310
311
312def process_failure(failure_id, host_action, test_action, labels=(),
313 keyvals=None, bugs=(), reason=None, invalidate=False):
314 """
315 Triage a failure
316
317 @param failure_id: The failure ID, as returned by get_failures()
318 @param host_action: One of 'Block', 'Unblock', 'Reinstall'
319 @param test_action: One of 'Skip', 'Rerun'
320
321 @param labels: Test labels to apply, by name
322 @param keyvals: Dictionary of job keyvals to add (or replace)
323 @param bugs: List of bug IDs to associate with this failure
324 @param reason: An override for the test failure reason
325 @param invalidate: True if failure should be invalidated for the purposes of
326 reporting. Defaults to False.
327 """
328 if keyvals is None:
329 keyvals = {}
330
331 host_choices = failure_actions.HostAction.values
332 test_choices = failure_actions.TestAction.values
333 if host_action not in host_choices:
334 raise model_logic.ValidationError(
335 {'host_action': ('host action %s not valid; must be one of %s'
336 % (host_action, ', '.join(host_choices)))})
337 if test_action not in test_choices:
338 raise model_logic.ValidationError(
339 {'test_action': ('test action %s not valid; must be one of %s'
340 % (test_action, ', '.join(test_choices)))})
341
342 failure = models.TestRun.objects.get(id=failure_id)
343
344 rpc_utils.process_host_action(failure.host, host_action)
345 rpc_utils.process_test_action(failure.test_job, test_action)
346
347 # Add the test labels
348 for label in labels:
349 tko_test_label, _ = (
350 tko_models.TestLabel.objects.get_or_create(name=label))
351 failure.tko_test.testlabel_set.add(tko_test_label)
352
353 # Set the job keyvals
354 for key, value in keyvals.iteritems():
355 keyval, created = tko_models.JobKeyval.objects.get_or_create(
356 job=failure.tko_test.job, key=key)
357 if not created:
358 tko_models.JobKeyval.objects.create(job=failure.tko_test.job,
359 key='original_' + key,
360 value=keyval.value)
361 keyval.value = value
362 keyval.save()
363
364 # Add the bugs
365 for bug_id in bugs:
366 bug, _ = models.Bug.objects.get_or_create(external_uid=bug_id)
367 failure.bugs.add(bug)
368
369 # Set the failure reason
370 if reason is not None:
371 tko_models.TestAttribute.objects.create(test=failure.tko_test,
372 attribute='original_reason',
373 value=failure.tko_test.reason)
374 failure.tko_test.reason = reason
375 failure.tko_test.save()
376
377 # Set 'invalidated', 'seen', and 'triaged'
378 failure.invalidated = invalidate
379 failure.seen = True
380 failure.triaged = True
381 failure.save()
382
383
jamesrenb852bce2010-04-07 20:36:13 +0000384def get_static_data():
jamesren4be631f2010-04-08 23:01:22 +0000385 result = {'motd': afe_rpc_utils.get_motd(),
386 'host_actions': sorted(failure_actions.HostAction.values),
387 'test_actions': sorted(failure_actions.TestAction.values)}
jamesrenb852bce2010-04-07 20:36:13 +0000388 return result