blob: 83ca301bd0874927e5047d1745a510ee5860a7e4 [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)
jamesren62758242010-04-28 18:08:25 +0000257 failures = failures.order_by('seen').select_related('test_job__test',
258 'host__host',
259 'tko_test')
jamesrenb852bce2010-04-07 20:36:13 +0000260 for failure in failures:
jamesren62758242010-04-28 18:08:25 +0000261 test_name = '%s: %s' % (
jamesrenb852bce2010-04-07 20:36:13 +0000262 failure.test_job.test_config.alias, failure.tko_test.test)
263
264 group_failures = result.setdefault(failure.tko_test.reason, [])
265 failure_dict = {'id': failure.id,
266 'machine': failure.host.host.hostname,
267 'blocked': bool(failure.host.blocked),
268 'test_name': test_name,
269 'reason': failure.tko_test.reason,
270 'seen': bool(failure.seen)}
271 group_failures.append(failure_dict)
272
273 return result
274
275
jamesrendbeebf82010-04-08 22:58:26 +0000276def get_test_runs(**filter_data):
277 """
278 Gets a list of test runs that match the filter data.
279
280 Returns a list of expanded TestRun object dictionaries. Specifically, the
281 "host" and "test_job" fields are expanded. Additionally, the "test_config"
282 field of the "test_job" expansion is also expanded.
283 """
284 result = []
285 for test_run in models.TestRun.objects.filter(**filter_data):
286 test_run_dict = test_run.get_object_dict()
287 test_run_dict['host'] = test_run.host.get_object_dict()
288 test_run_dict['test_job'] = test_run.test_job.get_object_dict()
289 test_run_dict['test_job']['test_config'] = (
290 test_run.test_job.test_config.get_object_dict())
291 result.append(test_run_dict)
292 return result
293
294
295def skip_test(test_config_id, hostname):
296 """
297 Marks a test config as "skipped" for a given host
298 """
299 config = models.TestConfig.objects.get(id=test_config_id)
300 config.skipped_hosts.add(afe_models.Host.objects.get(hostname=hostname))
301
302
jamesren4be631f2010-04-08 23:01:22 +0000303def mark_failures_as_seen(failure_ids):
304 """
305 Marks a set of failures as 'seen'
306
307 @param failure_ids: A list of failure IDs, as returned by get_failures(), to
308 mark as seen
309 """
310 models.TestRun.objects.filter(id__in=failure_ids).update(seen=True)
311
312
jamesren62758242010-04-28 18:08:25 +0000313def process_failures(failure_ids, host_action, test_action, labels=(),
314 keyvals=None, bugs=(), reason=None, invalidate=False):
jamesren4be631f2010-04-08 23:01:22 +0000315 """
316 Triage a failure
317
318 @param failure_id: The failure ID, as returned by get_failures()
319 @param host_action: One of 'Block', 'Unblock', 'Reinstall'
320 @param test_action: One of 'Skip', 'Rerun'
321
322 @param labels: Test labels to apply, by name
323 @param keyvals: Dictionary of job keyvals to add (or replace)
324 @param bugs: List of bug IDs to associate with this failure
325 @param reason: An override for the test failure reason
326 @param invalidate: True if failure should be invalidated for the purposes of
327 reporting. Defaults to False.
328 """
jamesren4be631f2010-04-08 23:01:22 +0000329 host_choices = failure_actions.HostAction.values
330 test_choices = failure_actions.TestAction.values
331 if host_action not in host_choices:
332 raise model_logic.ValidationError(
333 {'host_action': ('host action %s not valid; must be one of %s'
334 % (host_action, ', '.join(host_choices)))})
335 if test_action not in test_choices:
336 raise model_logic.ValidationError(
337 {'test_action': ('test action %s not valid; must be one of %s'
338 % (test_action, ', '.join(test_choices)))})
339
jamesren62758242010-04-28 18:08:25 +0000340 for failure_id in failure_ids:
341 rpc_utils.process_failure(
342 failure_id=failure_id, host_action=host_action,
343 test_action=test_action, labels=labels, keyvals=keyvals,
344 bugs=bugs, reason=reason, invalidate=invalidate)
jamesren4be631f2010-04-08 23:01:22 +0000345
346
jamesren1602f3b2010-04-09 20:46:29 +0000347def get_motd():
348 return afe_rpc_utils.get_motd()
349
350
jamesrenb852bce2010-04-07 20:36:13 +0000351def get_static_data():
jamesren1602f3b2010-04-09 20:46:29 +0000352 result = {'motd': get_motd(),
jamesren4be631f2010-04-08 23:01:22 +0000353 'host_actions': sorted(failure_actions.HostAction.values),
354 'test_actions': sorted(failure_actions.TestAction.values)}
jamesrenb852bce2010-04-07 20:36:13 +0000355 return result