blob: b6192a4fd29cd4d3fb17e00838949efa84f83479 [file] [log] [blame]
showardfb2a7fa2008-07-17 17:04:12 +00001from datetime import datetime
showard7c785282008-05-29 19:45:12 +00002from django.db import models as dbmodels, connection
showarddf062562008-07-03 19:56:37 +00003from frontend.afe import model_logic
showard3dd47c22008-07-10 00:41:36 +00004from frontend import settings, thread_local
showardb1e51872008-10-07 11:08:18 +00005from autotest_lib.client.common_lib import enum, host_protections, global_config
mblighe8819cd2008-02-15 16:48:40 +00006
7
mblighe8819cd2008-02-15 16:48:40 +00008class AclAccessViolation(Exception):
jadmanski0afbb632008-06-06 21:10:57 +00009 """\
10 Raised when an operation is attempted with proper permissions as
11 dictated by ACLs.
12 """
mblighe8819cd2008-02-15 16:48:40 +000013
14
showard7c785282008-05-29 19:45:12 +000015class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000016 """\
17 Required:
18 name: label name
mblighe8819cd2008-02-15 16:48:40 +000019
jadmanski0afbb632008-06-06 21:10:57 +000020 Optional:
21 kernel_config: url/path to kernel config to use for jobs run on this
22 label
23 platform: if True, this is a platform label (defaults to False)
showard989f25d2008-10-01 11:38:11 +000024 only_if_needed: if True, a machine with this label can only be used if that
25 that label is requested by the job/test.
jadmanski0afbb632008-06-06 21:10:57 +000026 """
27 name = dbmodels.CharField(maxlength=255, unique=True)
28 kernel_config = dbmodels.CharField(maxlength=255, blank=True)
29 platform = dbmodels.BooleanField(default=False)
30 invalid = dbmodels.BooleanField(default=False,
31 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +000032 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +000033
jadmanski0afbb632008-06-06 21:10:57 +000034 name_field = 'name'
35 objects = model_logic.ExtendedManager()
36 valid_objects = model_logic.ValidObjectsManager()
mbligh5244cbb2008-04-24 20:39:52 +000037
jadmanski0afbb632008-06-06 21:10:57 +000038 def clean_object(self):
39 self.host_set.clear()
mblighe8819cd2008-02-15 16:48:40 +000040
41
jadmanski0afbb632008-06-06 21:10:57 +000042 def enqueue_job(self, job):
43 'Enqueue a job on any host of this label.'
44 queue_entry = HostQueueEntry(meta_host=self, job=job,
45 status=Job.Status.QUEUED,
46 priority=job.priority)
47 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +000048
49
jadmanski0afbb632008-06-06 21:10:57 +000050 class Meta:
51 db_table = 'labels'
mblighe8819cd2008-02-15 16:48:40 +000052
jadmanski0afbb632008-06-06 21:10:57 +000053 class Admin:
54 list_display = ('name', 'kernel_config')
55 # see Host.Admin
56 manager = model_logic.ValidObjectsManager()
mblighe8819cd2008-02-15 16:48:40 +000057
jadmanski0afbb632008-06-06 21:10:57 +000058 def __str__(self):
59 return self.name
mblighe8819cd2008-02-15 16:48:40 +000060
61
showardfb2a7fa2008-07-17 17:04:12 +000062class User(dbmodels.Model, model_logic.ModelExtensions):
63 """\
64 Required:
65 login :user login name
66
67 Optional:
68 access_level: 0=User (default), 1=Admin, 100=Root
69 """
70 ACCESS_ROOT = 100
71 ACCESS_ADMIN = 1
72 ACCESS_USER = 0
73
74 login = dbmodels.CharField(maxlength=255, unique=True)
75 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
76
77 name_field = 'login'
78 objects = model_logic.ExtendedManager()
79
80
81 def save(self):
82 # is this a new object being saved for the first time?
83 first_time = (self.id is None)
84 user = thread_local.get_user()
85 if user and not user.is_superuser():
86 raise AclAccessViolation("You cannot modify users!")
87 super(User, self).save()
88 if first_time:
89 everyone = AclGroup.objects.get(name='Everyone')
90 everyone.users.add(self)
91
92
93 def is_superuser(self):
94 return self.access_level >= self.ACCESS_ROOT
95
96
97 class Meta:
98 db_table = 'users'
99
100 class Admin:
101 list_display = ('login', 'access_level')
102 search_fields = ('login',)
103
104 def __str__(self):
105 return self.login
106
107
showard7c785282008-05-29 19:45:12 +0000108class Host(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +0000109 """\
110 Required:
111 hostname
mblighe8819cd2008-02-15 16:48:40 +0000112
jadmanski0afbb632008-06-06 21:10:57 +0000113 optional:
114 locked: host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000115
jadmanski0afbb632008-06-06 21:10:57 +0000116 Internal:
117 synch_id: currently unused
118 status: string describing status of host
119 """
120 Status = enum.Enum('Verifying', 'Running', 'Ready', 'Repairing',
121 'Repair Failed', 'Dead', 'Rebooting',
122 string_values=True)
mblighe8819cd2008-02-15 16:48:40 +0000123
jadmanski0afbb632008-06-06 21:10:57 +0000124 hostname = dbmodels.CharField(maxlength=255, unique=True)
125 labels = dbmodels.ManyToManyField(Label, blank=True,
126 filter_interface=dbmodels.HORIZONTAL)
127 locked = dbmodels.BooleanField(default=False)
128 synch_id = dbmodels.IntegerField(blank=True, null=True,
129 editable=settings.FULL_ADMIN)
130 status = dbmodels.CharField(maxlength=255, default=Status.READY,
131 choices=Status.choices(),
132 editable=settings.FULL_ADMIN)
133 invalid = dbmodels.BooleanField(default=False,
134 editable=settings.FULL_ADMIN)
showardd5afc2f2008-08-12 17:19:44 +0000135 protection = dbmodels.SmallIntegerField(null=False, blank=True,
showarddf062562008-07-03 19:56:37 +0000136 choices=host_protections.choices,
137 default=host_protections.default)
showardfb2a7fa2008-07-17 17:04:12 +0000138 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
139 lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False)
mblighe8819cd2008-02-15 16:48:40 +0000140
jadmanski0afbb632008-06-06 21:10:57 +0000141 name_field = 'hostname'
142 objects = model_logic.ExtendedManager()
143 valid_objects = model_logic.ValidObjectsManager()
mbligh5244cbb2008-04-24 20:39:52 +0000144
showardb8471e32008-07-03 19:51:08 +0000145 @staticmethod
146 def create_one_time_host(hostname):
147 query = Host.objects.filter(hostname=hostname)
148 if query.count() == 0:
149 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000150 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000151 else:
152 host = query[0]
153 if not host.invalid:
154 raise model_logic.ValidationError({
155 'hostname' : '%s already exists!' % hostname
156 })
157 host.clean_object()
showarde65d2af2008-07-30 23:34:21 +0000158 AclGroup.objects.get(name='Everyone').hosts.add(host)
showardb8471e32008-07-03 19:51:08 +0000159 host.status = Host.Status.READY
showard1ab512b2008-07-30 23:39:04 +0000160 host.protection = host_protections.Protection.DO_NOT_REPAIR
showardb8471e32008-07-03 19:51:08 +0000161 host.save()
162 return host
mbligh5244cbb2008-04-24 20:39:52 +0000163
jadmanski0afbb632008-06-06 21:10:57 +0000164 def clean_object(self):
165 self.aclgroup_set.clear()
166 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000167
168
jadmanski0afbb632008-06-06 21:10:57 +0000169 def save(self):
170 # extra spaces in the hostname can be a sneaky source of errors
171 self.hostname = self.hostname.strip()
172 # is this a new object being saved for the first time?
173 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000174 if not first_time:
175 AclGroup.check_for_acl_violation_hosts([self])
showardfb2a7fa2008-07-17 17:04:12 +0000176 if self.locked and not self.locked_by:
177 self.locked_by = thread_local.get_user()
178 self.lock_time = datetime.now()
179 elif not self.locked and self.locked_by:
180 self.locked_by = None
181 self.lock_time = None
jadmanski0afbb632008-06-06 21:10:57 +0000182 super(Host, self).save()
183 if first_time:
184 everyone = AclGroup.objects.get(name='Everyone')
185 everyone.hosts.add(self)
mblighe8819cd2008-02-15 16:48:40 +0000186
showardb8471e32008-07-03 19:51:08 +0000187 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000188 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000189 for queue_entry in self.hostqueueentry_set.all():
190 queue_entry.deleted = True
showard9a1f2e12008-10-02 11:14:29 +0000191 queue_entry.abort(thread_local.get_user())
showardb8471e32008-07-03 19:51:08 +0000192 super(Host, self).delete()
193
mblighe8819cd2008-02-15 16:48:40 +0000194
jadmanski0afbb632008-06-06 21:10:57 +0000195 def enqueue_job(self, job):
196 ' Enqueue a job on this host.'
197 queue_entry = HostQueueEntry(host=self, job=job,
198 status=Job.Status.QUEUED,
199 priority=job.priority)
200 # allow recovery of dead hosts from the frontend
201 if not self.active_queue_entry() and self.is_dead():
202 self.status = Host.Status.READY
203 self.save()
204 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000205
showard08f981b2008-06-24 21:59:03 +0000206 block = IneligibleHostQueue(job=job, host=self)
207 block.save()
208
mblighe8819cd2008-02-15 16:48:40 +0000209
jadmanski0afbb632008-06-06 21:10:57 +0000210 def platform(self):
211 # TODO(showard): slighly hacky?
212 platforms = self.labels.filter(platform=True)
213 if len(platforms) == 0:
214 return None
215 return platforms[0]
216 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000217
218
jadmanski0afbb632008-06-06 21:10:57 +0000219 def is_dead(self):
220 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000221
222
jadmanski0afbb632008-06-06 21:10:57 +0000223 def active_queue_entry(self):
224 active = list(self.hostqueueentry_set.filter(active=True))
225 if not active:
226 return None
227 assert len(active) == 1, ('More than one active entry for '
228 'host ' + self.hostname)
229 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000230
231
jadmanski0afbb632008-06-06 21:10:57 +0000232 class Meta:
233 db_table = 'hosts'
mblighe8819cd2008-02-15 16:48:40 +0000234
jadmanski0afbb632008-06-06 21:10:57 +0000235 class Admin:
236 # TODO(showard) - showing platform requires a SQL query for
237 # each row (since labels are many-to-many) - should we remove
238 # it?
239 list_display = ('hostname', 'platform', 'locked', 'status')
showarddf062562008-07-03 19:56:37 +0000240 list_filter = ('labels', 'locked', 'protection')
jadmanski0afbb632008-06-06 21:10:57 +0000241 search_fields = ('hostname', 'status')
242 # undocumented Django feature - if you set manager here, the
243 # admin code will use it, otherwise it'll use a default Manager
244 manager = model_logic.ValidObjectsManager()
mblighe8819cd2008-02-15 16:48:40 +0000245
jadmanski0afbb632008-06-06 21:10:57 +0000246 def __str__(self):
247 return self.hostname
mblighe8819cd2008-02-15 16:48:40 +0000248
249
showard7c785282008-05-29 19:45:12 +0000250class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000251 """\
252 Required:
showard909c7a62008-07-15 21:52:38 +0000253 author: author name
254 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000255 name: test name
showard909c7a62008-07-15 21:52:38 +0000256 time: short, medium, long
257 test_class: This describes the class for your the test belongs in.
258 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000259 test_type: Client or Server
260 path: path to pass to run_test()
261 synch_type: whether the test should run synchronously or asynchronously
showard909c7a62008-07-15 21:52:38 +0000262 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
263 async job. If it's >1 it's sync job for that number of machines
264 i.e. if sync_count = 2 it is a sync job that requires two
265 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000266 Optional:
showard909c7a62008-07-15 21:52:38 +0000267 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000268 dependency_labels: many-to-many relationship with labels corresponding to
269 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000270 experimental: If this is set to True production servers will ignore the test
271 run_verify: Whether or not the scheduler should run the verify stage
jadmanski0afbb632008-06-06 21:10:57 +0000272 """
showard909c7a62008-07-15 21:52:38 +0000273 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
jadmanski0afbb632008-06-06 21:10:57 +0000274 SynchType = enum.Enum('Asynchronous', 'Synchronous', start_value=1)
275 # TODO(showard) - this should be merged with Job.ControlType (but right
276 # now they use opposite values)
277 Types = enum.Enum('Client', 'Server', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000278
jadmanski0afbb632008-06-06 21:10:57 +0000279 name = dbmodels.CharField(maxlength=255, unique=True)
showard909c7a62008-07-15 21:52:38 +0000280 author = dbmodels.CharField(maxlength=255)
281 test_class = dbmodels.CharField(maxlength=255)
282 test_category = dbmodels.CharField(maxlength=255)
283 dependencies = dbmodels.CharField(maxlength=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000284 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000285 experimental = dbmodels.BooleanField(default=True)
286 run_verify = dbmodels.BooleanField(default=True)
287 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
288 default=TestTime.MEDIUM)
jadmanski0afbb632008-06-06 21:10:57 +0000289 test_type = dbmodels.SmallIntegerField(choices=Types.choices())
showard909c7a62008-07-15 21:52:38 +0000290 sync_count = dbmodels.IntegerField(default=1)
jadmanski0afbb632008-06-06 21:10:57 +0000291 synch_type = dbmodels.SmallIntegerField(choices=SynchType.choices(),
292 default=SynchType.ASYNCHRONOUS)
showard909c7a62008-07-15 21:52:38 +0000293 path = dbmodels.CharField(maxlength=255, unique=True)
showard989f25d2008-10-01 11:38:11 +0000294 dependency_labels = dbmodels.ManyToManyField(
295 Label, blank=True, filter_interface=dbmodels.HORIZONTAL)
mblighe8819cd2008-02-15 16:48:40 +0000296
jadmanski0afbb632008-06-06 21:10:57 +0000297 name_field = 'name'
298 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000299
300
jadmanski0afbb632008-06-06 21:10:57 +0000301 class Meta:
302 db_table = 'autotests'
mblighe8819cd2008-02-15 16:48:40 +0000303
jadmanski0afbb632008-06-06 21:10:57 +0000304 class Admin:
305 fields = (
306 (None, {'fields' :
showard909c7a62008-07-15 21:52:38 +0000307 ('name', 'author', 'test_category', 'test_class',
308 'test_time', 'synch_type', 'test_type', 'sync_count',
309 'path', 'dependencies', 'experimental', 'run_verify',
310 'description')}),
jadmanski0afbb632008-06-06 21:10:57 +0000311 )
showard909c7a62008-07-15 21:52:38 +0000312 list_display = ('name', 'test_type', 'description', 'synch_type')
jadmanski0afbb632008-06-06 21:10:57 +0000313 search_fields = ('name',)
mblighe8819cd2008-02-15 16:48:40 +0000314
jadmanski0afbb632008-06-06 21:10:57 +0000315 def __str__(self):
316 return self.name
mblighe8819cd2008-02-15 16:48:40 +0000317
318
showard2b9a88b2008-06-13 20:55:03 +0000319class Profiler(dbmodels.Model, model_logic.ModelExtensions):
320 """\
321 Required:
322 name: profiler name
323 test_type: Client or Server
324
325 Optional:
326 description: arbirary text description
327 """
328 name = dbmodels.CharField(maxlength=255, unique=True)
329 description = dbmodels.TextField(blank=True)
330
331 name_field = 'name'
332 objects = model_logic.ExtendedManager()
333
334
335 class Meta:
336 db_table = 'profilers'
337
338 class Admin:
339 list_display = ('name', 'description')
340 search_fields = ('name',)
341
342 def __str__(self):
343 return self.name
344
345
showard7c785282008-05-29 19:45:12 +0000346class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000347 """\
348 Required:
349 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000350
jadmanski0afbb632008-06-06 21:10:57 +0000351 Optional:
352 description: arbitrary description of group
353 """
354 name = dbmodels.CharField(maxlength=255, unique=True)
355 description = dbmodels.CharField(maxlength=255, blank=True)
showard04f2cd82008-07-25 20:53:31 +0000356 users = dbmodels.ManyToManyField(User, blank=True,
jadmanski0afbb632008-06-06 21:10:57 +0000357 filter_interface=dbmodels.HORIZONTAL)
358 hosts = dbmodels.ManyToManyField(Host,
359 filter_interface=dbmodels.HORIZONTAL)
mblighe8819cd2008-02-15 16:48:40 +0000360
jadmanski0afbb632008-06-06 21:10:57 +0000361 name_field = 'name'
362 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000363
showard08f981b2008-06-24 21:59:03 +0000364 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000365 def check_for_acl_violation_hosts(hosts):
366 user = thread_local.get_user()
367 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000368 return
showard3dd47c22008-07-10 00:41:36 +0000369 accessible_host_ids = set(
370 host.id for host in Host.objects.filter(acl_group__users=user))
371 for host in hosts:
372 # Check if the user has access to this host,
373 # but only if it is not a metahost
374 if (isinstance(host, Host)
375 and int(host.id) not in accessible_host_ids):
376 raise AclAccessViolation("You do not have access to %s"
377 % str(host))
378
showard9dbdcda2008-10-14 17:34:36 +0000379
380 @staticmethod
381 def check_for_acl_violation_queue_entries(queue_entries):
382 user = thread_local.get_user()
383 if user.is_superuser():
384 return
385 for queue_entry in queue_entries:
386 job = queue_entry.job
387 if user.login != job.owner:
388 raise AclAccessViolation('You cannot abort job %d owned by %s' %
389 (job.id, job.owner))
390
391
showard3dd47c22008-07-10 00:41:36 +0000392 def check_for_acl_violation_acl_group(self):
393 user = thread_local.get_user()
394 if user.is_superuser():
395 return None
396 if not user in self.users.all():
397 raise AclAccessViolation("You do not have access to %s"
398 % self.name)
399
400 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000401 def on_host_membership_change():
402 everyone = AclGroup.objects.get(name='Everyone')
403
showard3dd47c22008-07-10 00:41:36 +0000404 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000405 # TODO(showard): this is a bit of a hack, since the fact that this query
406 # works is kind of a coincidence of Django internals. This trick
407 # doesn't work in general (on all foreign key relationships). I'll
408 # replace it with a better technique when the need arises.
409 orphaned_hosts = Host.valid_objects.filter(acl_group__id__isnull=True)
410 everyone.hosts.add(*orphaned_hosts.distinct())
411
412 # find hosts in both Everyone and another ACL group, and remove them
413 # from Everyone
414 hosts_in_everyone = Host.valid_objects.filter_custom_join(
415 '_everyone', acl_group__name='Everyone')
416 acled_hosts = hosts_in_everyone.exclude(acl_group__name='Everyone')
417 everyone.hosts.remove(*acled_hosts.distinct())
418
419
420 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000421 if (self.name == 'Everyone'):
422 raise AclAccessViolation("You cannot delete 'Everyone'!")
423 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000424 super(AclGroup, self).delete()
425 self.on_host_membership_change()
426
427
showard04f2cd82008-07-25 20:53:31 +0000428 def add_current_user_if_empty(self):
429 if not self.users.count():
430 self.users.add(thread_local.get_user())
431
432
showard08f981b2008-06-24 21:59:03 +0000433 # if you have a model attribute called "Manipulator", Django will
434 # automatically insert it into the beginning of the superclass list
435 # for the model's manipulators
436 class Manipulator(object):
437 """
438 Custom manipulator to get notification when ACLs are changed through
439 the admin interface.
440 """
441 def save(self, new_data):
showard3dd47c22008-07-10 00:41:36 +0000442 user = thread_local.get_user()
showard31c570e2008-07-23 19:29:17 +0000443 if hasattr(self, 'original_object'):
444 if (not user.is_superuser()
445 and self.original_object.name == 'Everyone'):
446 raise AclAccessViolation("You cannot modify 'Everyone'!")
447 self.original_object.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000448 obj = super(AclGroup.Manipulator, self).save(new_data)
showard04f2cd82008-07-25 20:53:31 +0000449 if not hasattr(self, 'original_object'):
450 obj.users.add(thread_local.get_user())
451 obj.add_current_user_if_empty()
showard08f981b2008-06-24 21:59:03 +0000452 obj.on_host_membership_change()
453 return obj
showardeb3be4d2008-04-21 20:59:26 +0000454
jadmanski0afbb632008-06-06 21:10:57 +0000455 class Meta:
456 db_table = 'acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000457
jadmanski0afbb632008-06-06 21:10:57 +0000458 class Admin:
459 list_display = ('name', 'description')
460 search_fields = ('name',)
mblighe8819cd2008-02-15 16:48:40 +0000461
jadmanski0afbb632008-06-06 21:10:57 +0000462 def __str__(self):
463 return self.name
mblighe8819cd2008-02-15 16:48:40 +0000464
465# hack to make the column name in the many-to-many DB tables match the one
466# generated by ruby
467AclGroup._meta.object_name = 'acl_group'
468
469
showard7c785282008-05-29 19:45:12 +0000470class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +0000471 'Custom manager to provide efficient status counts querying.'
472 def get_status_counts(self, job_ids):
473 """\
474 Returns a dictionary mapping the given job IDs to their status
475 count dictionaries.
476 """
477 if not job_ids:
478 return {}
479 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
480 cursor = connection.cursor()
481 cursor.execute("""
482 SELECT job_id, status, COUNT(*)
483 FROM host_queue_entries
484 WHERE job_id IN %s
485 GROUP BY job_id, status
486 """ % id_list)
487 all_job_counts = {}
488 for job_id in job_ids:
489 all_job_counts[job_id] = {}
490 for job_id, status, count in cursor.fetchall():
491 all_job_counts[job_id][status] = count
492 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +0000493
494
showard989f25d2008-10-01 11:38:11 +0000495 def populate_dependencies(self, jobs):
showard9a1f2e12008-10-02 11:14:29 +0000496 if not jobs:
497 return
showard989f25d2008-10-01 11:38:11 +0000498 job_ids = ','.join(str(job['id']) for job in jobs)
499 cursor = connection.cursor()
500 cursor.execute("""
showard56e93772008-10-06 10:06:22 +0000501 SELECT jobs.id, labels.name
showard989f25d2008-10-01 11:38:11 +0000502 FROM jobs
503 INNER JOIN jobs_dependency_labels
504 ON jobs.id = jobs_dependency_labels.job_id
505 INNER JOIN labels ON jobs_dependency_labels.label_id = labels.id
506 WHERE jobs.id IN (%s)
showard989f25d2008-10-01 11:38:11 +0000507 """ % job_ids)
showard56e93772008-10-06 10:06:22 +0000508 job_dependencies = {}
509 for job_id, dependency in cursor.fetchall():
510 job_dependencies.setdefault(job_id, []).append(dependency)
showard989f25d2008-10-01 11:38:11 +0000511 for job in jobs:
showard56e93772008-10-06 10:06:22 +0000512 dependencies = ','.join(job_dependencies.get(job['id'], []))
513 job['dependencies'] = dependencies
showard989f25d2008-10-01 11:38:11 +0000514
515
showard7c785282008-05-29 19:45:12 +0000516class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000517 """\
518 owner: username of job owner
519 name: job name (does not have to be unique)
520 priority: Low, Medium, High, Urgent (or 0-3)
521 control_file: contents of control file
522 control_type: Client or Server
523 created_on: date of job creation
524 submitted_on: date of job submission
525 synch_type: Asynchronous or Synchronous (i.e. job must run on all hosts
526 simultaneously; used for server-side control files)
527 synch_count: ???
showard909c7a62008-07-15 21:52:38 +0000528 run_verify: Whether or not to run the verify phase
jadmanski0afbb632008-06-06 21:10:57 +0000529 synchronizing: for scheduler use
showard3bb499f2008-07-03 19:42:20 +0000530 timeout: hours until job times out
showard542e8402008-09-19 20:16:18 +0000531 email_list: list of people to email on completion delimited by any of:
532 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +0000533 dependency_labels: many-to-many relationship with labels corresponding to
534 job dependencies
jadmanski0afbb632008-06-06 21:10:57 +0000535 """
showardb1e51872008-10-07 11:08:18 +0000536 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
537 'AUTOTEST_WEB', 'job_timeout_default', default=240)
538
jadmanski0afbb632008-06-06 21:10:57 +0000539 Priority = enum.Enum('Low', 'Medium', 'High', 'Urgent')
540 ControlType = enum.Enum('Server', 'Client', start_value=1)
541 Status = enum.Enum('Created', 'Queued', 'Pending', 'Running',
542 'Completed', 'Abort', 'Aborting', 'Aborted',
showardd823b362008-07-24 16:35:46 +0000543 'Failed', 'Starting', string_values=True)
mblighe8819cd2008-02-15 16:48:40 +0000544
jadmanski0afbb632008-06-06 21:10:57 +0000545 owner = dbmodels.CharField(maxlength=255)
546 name = dbmodels.CharField(maxlength=255)
547 priority = dbmodels.SmallIntegerField(choices=Priority.choices(),
548 blank=True, # to allow 0
549 default=Priority.MEDIUM)
550 control_file = dbmodels.TextField()
551 control_type = dbmodels.SmallIntegerField(choices=ControlType.choices(),
showardb1e51872008-10-07 11:08:18 +0000552 blank=True, # to allow 0
553 default=ControlType.CLIENT)
showard68c7aa02008-10-09 16:49:11 +0000554 created_on = dbmodels.DateTimeField()
jadmanski0afbb632008-06-06 21:10:57 +0000555 synch_type = dbmodels.SmallIntegerField(
556 blank=True, null=True, choices=Test.SynchType.choices())
557 synch_count = dbmodels.IntegerField(blank=True, null=True)
558 synchronizing = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +0000559 run_verify = dbmodels.BooleanField(default=True)
showardb1e51872008-10-07 11:08:18 +0000560 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
showard542e8402008-09-19 20:16:18 +0000561 email_list = dbmodels.CharField(maxlength=250, blank=True)
showard989f25d2008-10-01 11:38:11 +0000562 dependency_labels = dbmodels.ManyToManyField(
563 Label, blank=True, filter_interface=dbmodels.HORIZONTAL)
mblighe8819cd2008-02-15 16:48:40 +0000564
565
jadmanski0afbb632008-06-06 21:10:57 +0000566 # custom manager
567 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +0000568
569
jadmanski0afbb632008-06-06 21:10:57 +0000570 def is_server_job(self):
571 return self.control_type == self.ControlType.SERVER
mblighe8819cd2008-02-15 16:48:40 +0000572
573
jadmanski0afbb632008-06-06 21:10:57 +0000574 @classmethod
575 def create(cls, owner, name, priority, control_file, control_type,
showard989f25d2008-10-01 11:38:11 +0000576 hosts, synch_type, timeout, run_verify, email_list,
577 dependencies):
jadmanski0afbb632008-06-06 21:10:57 +0000578 """\
579 Creates a job by taking some information (the listed args)
580 and filling in the rest of the necessary information.
581 """
showard3dd47c22008-07-10 00:41:36 +0000582 AclGroup.check_for_acl_violation_hosts(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000583 job = cls.add_object(
584 owner=owner, name=name, priority=priority,
585 control_file=control_file, control_type=control_type,
showard909c7a62008-07-15 21:52:38 +0000586 synch_type=synch_type, timeout=timeout,
showard68c7aa02008-10-09 16:49:11 +0000587 run_verify=run_verify, email_list=email_list,
588 created_on=datetime.now())
mblighe8819cd2008-02-15 16:48:40 +0000589
jadmanski0afbb632008-06-06 21:10:57 +0000590 if job.synch_type == Test.SynchType.SYNCHRONOUS:
591 job.synch_count = len(hosts)
592 else:
593 if len(hosts) == 0:
594 errors = {'hosts':
595 'asynchronous jobs require at least'
596 + ' one host to run on'}
597 raise model_logic.ValidationError(errors)
598 job.save()
showard989f25d2008-10-01 11:38:11 +0000599 job.dependency_labels = dependencies
jadmanski0afbb632008-06-06 21:10:57 +0000600 return job
mblighe8819cd2008-02-15 16:48:40 +0000601
602
jadmanski0afbb632008-06-06 21:10:57 +0000603 def queue(self, hosts):
604 'Enqueue a job on the given hosts.'
605 for host in hosts:
606 host.enqueue_job(self)
mblighe8819cd2008-02-15 16:48:40 +0000607
608
jadmanski0afbb632008-06-06 21:10:57 +0000609 def user(self):
610 try:
611 return User.objects.get(login=self.owner)
612 except self.DoesNotExist:
613 return None
mblighe8819cd2008-02-15 16:48:40 +0000614
615
jadmanski0afbb632008-06-06 21:10:57 +0000616 class Meta:
617 db_table = 'jobs'
mblighe8819cd2008-02-15 16:48:40 +0000618
jadmanski0afbb632008-06-06 21:10:57 +0000619 if settings.FULL_ADMIN:
620 class Admin:
621 list_display = ('id', 'owner', 'name', 'control_type')
mblighe8819cd2008-02-15 16:48:40 +0000622
jadmanski0afbb632008-06-06 21:10:57 +0000623 def __str__(self):
624 return '%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +0000625
626
showard7c785282008-05-29 19:45:12 +0000627class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000628 job = dbmodels.ForeignKey(Job)
629 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +0000630
jadmanski0afbb632008-06-06 21:10:57 +0000631 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000632
jadmanski0afbb632008-06-06 21:10:57 +0000633 class Meta:
634 db_table = 'ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +0000635
jadmanski0afbb632008-06-06 21:10:57 +0000636 if settings.FULL_ADMIN:
637 class Admin:
638 list_display = ('id', 'job', 'host')
mblighe8819cd2008-02-15 16:48:40 +0000639
640
showard7c785282008-05-29 19:45:12 +0000641class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000642 job = dbmodels.ForeignKey(Job)
643 host = dbmodels.ForeignKey(Host, blank=True, null=True)
644 priority = dbmodels.SmallIntegerField()
645 status = dbmodels.CharField(maxlength=255)
646 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
647 db_column='meta_host')
648 active = dbmodels.BooleanField(default=False)
649 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +0000650 deleted = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +0000651
jadmanski0afbb632008-06-06 21:10:57 +0000652 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000653
mblighe8819cd2008-02-15 16:48:40 +0000654
jadmanski0afbb632008-06-06 21:10:57 +0000655 def is_meta_host_entry(self):
656 'True if this is a entry has a meta_host instead of a host.'
657 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +0000658
showard4c119042008-09-29 19:16:18 +0000659 def log_abort(self, user):
660 abort_log = AbortedHostQueueEntry(queue_entry=self, aborted_by=user)
661 abort_log.save()
662
663 def abort(self, user):
showardb8471e32008-07-03 19:51:08 +0000664 if self.active:
665 self.status = Job.Status.ABORT
showard4c119042008-09-29 19:16:18 +0000666 self.log_abort(user)
showardb8471e32008-07-03 19:51:08 +0000667 elif not self.complete:
668 self.status = Job.Status.ABORTED
669 self.active = False
670 self.complete = True
showard4c119042008-09-29 19:16:18 +0000671 self.log_abort(user)
showardb8471e32008-07-03 19:51:08 +0000672 self.save()
mblighe8819cd2008-02-15 16:48:40 +0000673
jadmanski0afbb632008-06-06 21:10:57 +0000674 class Meta:
675 db_table = 'host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +0000676
jadmanski0afbb632008-06-06 21:10:57 +0000677 if settings.FULL_ADMIN:
678 class Admin:
679 list_display = ('id', 'job', 'host', 'status',
680 'meta_host')
showard4c119042008-09-29 19:16:18 +0000681
682
683class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
684 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
685 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +0000686 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +0000687
688 objects = model_logic.ExtendedManager()
689
showard68c7aa02008-10-09 16:49:11 +0000690
691 def save(self):
692 self.aborted_on = datetime.now()
693 super(AbortedHostQueueEntry, self).save()
694
showard4c119042008-09-29 19:16:18 +0000695 class Meta:
696 db_table = 'aborted_host_queue_entries'