blob: 76aa4a264492170da576cfae28d4c84caa05f6de [file] [log] [blame]
jamesren9a6f5f62010-05-05 22:55:54 +00001import re
showard26b7ec72009-12-21 22:43:57 +00002from django.db import models as dbmodels
3import common
4from autotest_lib.frontend.afe import models as afe_models
5from autotest_lib.frontend.afe import model_logic, rpc_utils
showard250d84d2010-01-12 21:59:48 +00006from autotest_lib.frontend.tko import models as tko_models
jamesren3e9f6092010-03-11 21:32:10 +00007from autotest_lib.frontend.planner import model_attributes
jamesren06e59f12010-05-24 17:05:06 +00008from autotest_lib.client.common_lib import utils, host_queue_entry_states
showard26b7ec72009-12-21 22:43:57 +00009
10
jamesrenc3940222010-02-19 21:57:37 +000011class Plan(dbmodels.Model, model_logic.ModelExtensions):
showard26b7ec72009-12-21 22:43:57 +000012 """A test plan
13
14 Required:
15 name: Plan name, unique
16 complete: True if the plan is completed
17 dirty: True if the plan has been changed since the execution engine has
18 last seen it
19 initialized: True if the plan has started
20
21 Optional:
22 label_override: A label to apply to each Autotest job.
jamesrendbeebf82010-04-08 22:58:26 +000023 support: The global support script to apply to this plan
showard26b7ec72009-12-21 22:43:57 +000024 """
25 name = dbmodels.CharField(max_length=255, unique=True)
26 label_override = dbmodels.CharField(max_length=255, null=True, blank=True)
27 support = dbmodels.TextField(blank=True)
28 complete = dbmodels.BooleanField(default=False)
29 dirty = dbmodels.BooleanField(default=False)
30 initialized = dbmodels.BooleanField(default=False)
31
32 owners = dbmodels.ManyToManyField(afe_models.User,
33 db_table='planner_plan_owners')
34 hosts = dbmodels.ManyToManyField(afe_models.Host, through='Host')
jamesrenc3940222010-02-19 21:57:37 +000035 host_labels = dbmodels.ManyToManyField(afe_models.Label,
36 db_table='planner_plan_host_labels')
37
38 name_field = 'name'
showard26b7ec72009-12-21 22:43:57 +000039
40 class Meta:
41 db_table = 'planner_plans'
42
43
44 def __unicode__(self):
45 return unicode(self.name)
46
47
48class ModelWithPlan(dbmodels.Model):
49 """Superclass for models that have a plan_id
50
51 Required:
52 plan: The associated test plan
53 """
54 plan = dbmodels.ForeignKey(Plan)
55
56 class Meta:
57 abstract = True
58
59
60 def __unicode__(self):
61 return u'%s (%s)' % (self._get_details_unicode(), self.plan.name)
62
63
64 def _get_details_unicode(self):
65 """Gets the first part of the unicode string
66
67 subclasses must override this method
68 """
69 raise NotImplementedError(
70 'Subclasses must override _get_details_unicode()')
71
72
jamesrenc3940222010-02-19 21:57:37 +000073class Host(ModelWithPlan, model_logic.ModelExtensions):
showard26b7ec72009-12-21 22:43:57 +000074 """A plan host
75
76 Required:
77 host: The AFE host
78 complete: True if and only if this host is finished in the test plan
79 blocked: True if and only if the host is blocked (not executing tests)
jamesren3e9f6092010-03-11 21:32:10 +000080 added_by_label: True if and only if the host was added because of a host
81 label (as opposed to being explicitly added)
showard26b7ec72009-12-21 22:43:57 +000082 """
83 host = dbmodels.ForeignKey(afe_models.Host)
84 complete = dbmodels.BooleanField(default=False)
85 blocked = dbmodels.BooleanField(default=False)
jamesren3e9f6092010-03-11 21:32:10 +000086 added_by_label = dbmodels.BooleanField(default=False)
jamesrenc3940222010-02-19 21:57:37 +000087
showard26b7ec72009-12-21 22:43:57 +000088 class Meta:
89 db_table = 'planner_hosts'
90
91
jamesrenc3940222010-02-19 21:57:37 +000092 def status(self):
93 if self.complete:
jamesren3e9f6092010-03-11 21:32:10 +000094 return model_attributes.HostStatus.FINISHED
jamesrenc3940222010-02-19 21:57:37 +000095 if self.blocked:
jamesren3e9f6092010-03-11 21:32:10 +000096 return model_attributes.HostStatus.BLOCKED
97 return model_attributes.HostStatus.RUNNING
jamesrenc3940222010-02-19 21:57:37 +000098
99
showard26b7ec72009-12-21 22:43:57 +0000100 def _get_details_unicode(self):
jamesren128d4182010-02-19 21:57:58 +0000101 return 'Host: %s' % self.host.hostname
showard26b7ec72009-12-21 22:43:57 +0000102
103
jamesren3e9f6092010-03-11 21:32:10 +0000104class ControlFile(model_logic.ModelWithHash,
105 model_logic.ModelExtensions):
showard26b7ec72009-12-21 22:43:57 +0000106 """A control file. Immutable once added to the table
107
108 Required:
109 contents: The text of the control file
110
111 Others:
jamesren3e9f6092010-03-11 21:32:10 +0000112 the_hash: The SHA1 hash of the control file, for duplicate detection
113 and fast search
showard26b7ec72009-12-21 22:43:57 +0000114 """
115 contents = dbmodels.TextField()
116
117 class Meta:
118 db_table = 'planner_test_control_files'
119
120
121 @classmethod
122 def _compute_hash(cls, **kwargs):
lmr6f80e7a2010-02-04 03:18:28 +0000123 return utils.hash('sha1', kwargs['contents']).hexdigest()
showard26b7ec72009-12-21 22:43:57 +0000124
125
126 def __unicode__(self):
127 return u'Control file id %s (SHA1: %s)' % (self.id, self.control_hash)
128
129
jamesren3e9f6092010-03-11 21:32:10 +0000130class TestConfig(ModelWithPlan, model_logic.ModelExtensions):
jamesrenb852bce2010-04-07 20:36:13 +0000131 """A configuration for a planned test
showard26b7ec72009-12-21 22:43:57 +0000132
133 Required:
jamesrenc3940222010-02-19 21:57:37 +0000134 alias: The name to give this test within the plan. Unique with plan id
jamesren9a6f5f62010-05-05 22:55:54 +0000135 control_file: The control file to run
jamesren3e9f6092010-03-11 21:32:10 +0000136 is_server: True if this control file is a server-side test
showard26b7ec72009-12-21 22:43:57 +0000137 execution_order: An integer describing when this test should be run in
138 the test plan
jamesrenc3940222010-02-19 21:57:37 +0000139 estimated_runtime: Time in hours that the test is expected to run. Will
140 be automatically generated (on the frontend) for
141 tests in Autotest.
jamesrendbeebf82010-04-08 22:58:26 +0000142 skipped_hosts: Hosts that are going to skip this test.
showard26b7ec72009-12-21 22:43:57 +0000143 """
jamesrenc3940222010-02-19 21:57:37 +0000144 alias = dbmodels.CharField(max_length=255)
showard26b7ec72009-12-21 22:43:57 +0000145 control_file = dbmodels.ForeignKey(ControlFile)
jamesren3e9f6092010-03-11 21:32:10 +0000146 is_server = dbmodels.BooleanField(default=True)
showard26b7ec72009-12-21 22:43:57 +0000147 execution_order = dbmodels.IntegerField(blank=True)
jamesrenc3940222010-02-19 21:57:37 +0000148 estimated_runtime = dbmodels.IntegerField()
jamesrendbeebf82010-04-08 22:58:26 +0000149 skipped_hosts = dbmodels.ManyToManyField(
150 afe_models.Host, db_table='planner_test_configs_skipped_hosts')
showard26b7ec72009-12-21 22:43:57 +0000151
152 class Meta:
jamesren3e9f6092010-03-11 21:32:10 +0000153 db_table = 'planner_test_configs'
showard26b7ec72009-12-21 22:43:57 +0000154 ordering = ('execution_order',)
jamesrenc3940222010-02-19 21:57:37 +0000155 unique_together = (('plan', 'alias'),)
showard26b7ec72009-12-21 22:43:57 +0000156
157
158 def _get_details_unicode(self):
jamesren3e9f6092010-03-11 21:32:10 +0000159 return 'Planned test config - Control file id %s' % self.control_file.id
showard26b7ec72009-12-21 22:43:57 +0000160
161
jamesren3e9f6092010-03-11 21:32:10 +0000162class Job(ModelWithPlan, model_logic.ModelExtensions):
showard26b7ec72009-12-21 22:43:57 +0000163 """Represents an Autotest job initiated for a test plan
164
165 Required:
jamesren3e9f6092010-03-11 21:32:10 +0000166 test: The TestConfig associated with this Job
showard26b7ec72009-12-21 22:43:57 +0000167 afe_job: The Autotest job
168 """
jamesren3e9f6092010-03-11 21:32:10 +0000169 test_config = dbmodels.ForeignKey(TestConfig)
showard26b7ec72009-12-21 22:43:57 +0000170 afe_job = dbmodels.ForeignKey(afe_models.Job)
jamesren4be631f2010-04-08 23:01:22 +0000171 requires_rerun = dbmodels.BooleanField(default=False)
showard26b7ec72009-12-21 22:43:57 +0000172
173 class Meta:
174 db_table = 'planner_test_jobs'
175
176
jamesren06e59f12010-05-24 17:05:06 +0000177 def active(self):
178 for hqe in self.afe_job.hostqueueentry_set.all():
179 if not hqe.complete:
180 return True
181 return False
182
183
184 def all_tests_passed(self):
185 if self.active():
186 return False
187
188 Status = host_queue_entry_states.Status
189 if self.afe_job.hostqueueentry_set.exclude(status=Status.COMPLETED):
190 return False
191
192 tko_tests = tko_models.Test.objects.filter(
193 job__afe_job_id=self.afe_job.id)
194 for tko_test in tko_tests:
195 if tko_test.status.word != 'GOOD':
196 return False
197 return True
198
199
showard26b7ec72009-12-21 22:43:57 +0000200 def _get_details_unicode(self):
jamesren128d4182010-02-19 21:57:58 +0000201 return 'AFE job %s' % self.afe_job.id
showard26b7ec72009-12-21 22:43:57 +0000202
203
204class Bug(dbmodels.Model):
205 """Represents a bug ID
206
207 Required:
208 external_uid: External unique ID for the bug
209 """
210 external_uid = dbmodels.CharField(max_length=255, unique=True)
211
212 class Meta:
213 db_table = 'planner_bugs'
214
215
216 def __unicode__(self):
217 return u'Bug external ID %s' % self.external_uid
218
219
jamesren3e9f6092010-03-11 21:32:10 +0000220class TestRun(ModelWithPlan, model_logic.ModelExtensions):
showard26b7ec72009-12-21 22:43:57 +0000221 """An individual test run from an Autotest job for the test plan.
222
223 Each Job object may have multiple TestRun objects associated with it.
224
225 Required:
226 test_job: The Job object associated with this TestRun
227 tko_test: The TKO Test associated with this TestRun
228 status: One of 'Active', 'Passed', 'Failed'
229 finalized: True if and only if the TestRun is ready to be shown in
230 triage
231 invalidated: True if and only if a user has decided to invalidate this
232 TestRun's results
233 seen: True if and only if a user has marked this TestRun as "seen"
234 triaged: True if and only if the TestRun no longer requires any user
235 intervention
236
237 Optional:
238 bugs: Bugs filed that a relevant to this run
239 """
showard26b7ec72009-12-21 22:43:57 +0000240 test_job = dbmodels.ForeignKey(Job)
241 tko_test = dbmodels.ForeignKey(tko_models.Test)
jamesrenc3940222010-02-19 21:57:37 +0000242 host = dbmodels.ForeignKey(Host)
jamesren3e9f6092010-03-11 21:32:10 +0000243 status = dbmodels.CharField(
244 max_length=16,
245 choices=model_attributes.TestRunStatus.choices(),
246 default=model_attributes.TestRunStatus.ACTIVE)
showard26b7ec72009-12-21 22:43:57 +0000247 finalized = dbmodels.BooleanField(default=False)
248 seen = dbmodels.BooleanField(default=False)
249 triaged = dbmodels.BooleanField(default=False)
jamesren4be631f2010-04-08 23:01:22 +0000250 invalidated = dbmodels.BooleanField(default=False)
showard26b7ec72009-12-21 22:43:57 +0000251
252 bugs = dbmodels.ManyToManyField(Bug, null=True,
253 db_table='planner_test_run_bugs')
254
255 class Meta:
256 db_table = 'planner_test_runs'
jamesren3e9f6092010-03-11 21:32:10 +0000257 unique_together = (('plan', 'test_job', 'tko_test', 'host'),)
showard26b7ec72009-12-21 22:43:57 +0000258
259
260 def _get_details_unicode(self):
261 return 'Test Run: %s' % self.id
262
263
264class DataType(dbmodels.Model):
265 """Encodes the data model types
266
267 For use in the history table, to identify the type of object that was
268 changed.
269
270 Required:
271 name: The name of the data type
272 db_table: The name of the database table that stores this type
273 """
274 name = dbmodels.CharField(max_length=255)
275 db_table = dbmodels.CharField(max_length=255)
276
277 class Meta:
278 db_table = 'planner_data_types'
279
280
281 def __unicode__(self):
282 return u'Data type %s (stored in table %s)' % (self.name, self.db_table)
283
284
285class History(ModelWithPlan):
286 """Represents a history action
287
288 Required:
289 action_id: An arbitrary ID that uniquely identifies the user action
290 related to the history entry. One user action may result in
291 multiple history entries
292 user: The user who initiated the change
293 data_type: The type of object that was changed
294 object_id: Value of the primary key field for the changed object
295 old_object_repr: A string representation of the object before the change
296 new_object_repr: A string representation of the object after the change
297
298 Others:
299 time: A timestamp. Automatically generated.
300 """
301 action_id = dbmodels.IntegerField()
302 user = dbmodels.ForeignKey(afe_models.User)
303 data_type = dbmodels.ForeignKey(DataType)
304 object_id = dbmodels.IntegerField()
305 old_object_repr = dbmodels.TextField(blank=True)
306 new_object_repr = dbmodels.TextField(blank=True)
307
308 time = dbmodels.DateTimeField(auto_now_add=True)
309
310 class Meta:
311 db_table = 'planner_history'
312
313
314 def _get_details_unicode(self):
315 return 'History entry: %s => %s' % (self.old_object_repr,
316 self.new_object_repr)
317
318
319class SavedObject(dbmodels.Model):
320 """A saved object that can be recalled at certain points in the UI
321
322 Required:
323 user: The creator of the object
324 object_type: One of 'support', 'triage', 'autoprocess', 'custom_query'
325 name: The name given to the object
326 encoded_object: The actual object
327 """
showard26b7ec72009-12-21 22:43:57 +0000328 user = dbmodels.ForeignKey(afe_models.User)
jamesren3e9f6092010-03-11 21:32:10 +0000329 object_type = dbmodels.CharField(
330 max_length=16,
331 choices=model_attributes.SavedObjectType.choices(),
332 db_column='type')
showard26b7ec72009-12-21 22:43:57 +0000333 name = dbmodels.CharField(max_length=255)
334 encoded_object = dbmodels.TextField()
335
336 class Meta:
337 db_table = 'planner_saved_objects'
338 unique_together = ('user', 'object_type', 'name')
339
340
341 def __unicode__(self):
342 return u'Saved %s object: %s, by %s' % (self.object_type, self.name,
343 self.user.login)
344
345
346class CustomQuery(ModelWithPlan):
347 """A custom SQL query for the triage page
348
349 Required:
350 query: the SQL WHERE clause to attach to the main query
351 """
352 query = dbmodels.TextField()
353
354 class Meta:
355 db_table = 'planner_custom_queries'
356
357
358 def _get_details_unicode(self):
359 return 'Custom Query: %s' % self.query
360
361
362class KeyVal(model_logic.ModelWithHash):
363 """Represents a keyval. Immutable once added to the table.
364
365 Required:
366 key: The key
367 value: The value
368
369 Others:
jamesren3e9f6092010-03-11 21:32:10 +0000370 the_hash: The result of SHA1(SHA1(key) ++ value), for duplicate
371 detection and fast search.
showard26b7ec72009-12-21 22:43:57 +0000372 """
373 key = dbmodels.CharField(max_length=1024)
374 value = dbmodels.CharField(max_length=1024)
375
376 class Meta:
377 db_table = 'planner_keyvals'
378
379
380 @classmethod
381 def _compute_hash(cls, **kwargs):
lmr6f80e7a2010-02-04 03:18:28 +0000382 round1 = utils.hash('sha1', kwargs['key']).hexdigest()
383 return utils.hash('sha1', round1 + kwargs['value']).hexdigest()
showard26b7ec72009-12-21 22:43:57 +0000384
385
386 def __unicode__(self):
387 return u'Keyval: %s = %s' % (self.key, self.value)
388
389
390class AutoProcess(ModelWithPlan):
391 """An autoprocessing directive to perform on test runs that enter triage
392
393 Required:
394 condition: A SQL WHERE clause. The autoprocessing will be applied if the
395 test run matches this condition
396 enabled: If this is False, this autoprocessing entry will not be applied
397
398 Optional:
399 labels: Labels to apply to the TKO test
400 keyvals: Keyval overrides to apply to the TKO test
401 bugs: Bugs filed that a relevant to this run
402 reason_override: Override for the AFE reason
403 """
404 condition = dbmodels.TextField()
405 enabled = dbmodels.BooleanField(default=False)
406
407 labels = dbmodels.ManyToManyField(tko_models.TestLabel, null=True,
408 db_table='planner_autoprocess_labels')
409 keyvals = dbmodels.ManyToManyField(KeyVal, null=True,
410 db_table='planner_autoprocess_keyvals')
411 bugs = dbmodels.ManyToManyField(Bug, null=True,
412 db_table='planner_autoprocess_bugs')
413 reason_override = dbmodels.CharField(max_length=255, null=True, blank=True)
414
415 class Meta:
416 db_table = 'planner_autoprocess'
417
418
419 def _get_details_unicode(self):
420 return 'Autoprocessing condition: %s' % self.condition
jamesren9a6f5f62010-05-05 22:55:54 +0000421
422
423class AdditionalParameter(ModelWithPlan):
424 """
425 Allows parameters to be passed to the execution engine for test configs
426
427 If this object matches a hostname by regex, it will apply the associated
428 parameters at their applicable locations.
429
430 Required:
431 hostname_regex: A regular expression, for matching on the hostname
432 param_type: Currently only 'Verify' (and site-specific values) allowed
433 application_order: The order in which to apply this parameter.
434 Parameters are attempted in the order specified here,
435 and stop when the first match is found
436 """
437 hostname_regex = dbmodels.CharField(max_length=255)
438 param_type = dbmodels.CharField(
439 max_length=32,
440 choices=model_attributes.AdditionalParameterType.choices())
441 application_order = dbmodels.IntegerField(blank=True)
442
443 class Meta:
444 db_table = 'planner_additional_parameters'
445 unique_together = ('plan', 'hostname_regex', 'param_type')
446
447
448 @classmethod
449 def find_applicable_additional_parameter(cls, plan, hostname, param_type):
450 """
451 Finds the first AdditionalParameter that matches the given arguments
452 """
453 params = cls.objects.filter(
454 plan=plan, param_type=param_type).order_by('application_order')
455 for param in params:
456 if re.match(param.hostname_regex, hostname):
457 return param
458 return None
459
460
461 def _get_details_unicode(self):
462 return 'Additional %s parameters, regex: %s' % (self.param_type,
463 self.hostname_regex)
464
465
466class AdditionalParameterValue(dbmodels.Model):
467 """
468 The actual values for the additional parameters
469
470 Required:
471 additional_parameter: The associated AdditionalParameter
472 key: The name of the parameter
473 value: The value of the parameter
474 """
475 additional_parameter = dbmodels.ForeignKey(AdditionalParameter)
476 key = dbmodels.CharField(max_length=255)
477 value = dbmodels.CharField(max_length=255)
478
479 class Meta:
480 db_table = 'planner_additional_parameter_values'
481 unique_together = ('additional_parameter', 'key')
482
483
484 def __unicode__(self):
485 return u'Value for parameter %d: %s=%s' % (self.additional_parameter.id,
486 self.key, self.value)