blob: eceb1f54cca8fa88134558dcceb4d0cd46888cfc [file] [log] [blame]
showardd1195652009-12-08 22:21:02 +00001import logging, os
showardfb2a7fa2008-07-17 17:04:12 +00002from datetime import datetime
showard7c785282008-05-29 19:45:12 +00003from django.db import models as dbmodels, connection
jamesren35a70222010-02-16 19:30:46 +00004from xml.sax import saxutils
showardcafd16e2009-05-29 18:37:49 +00005import common
jamesrendd855242010-03-02 22:23:44 +00006from autotest_lib.frontend.afe import model_logic, model_attributes
showardcafd16e2009-05-29 18:37:49 +00007from autotest_lib.frontend import settings, thread_local
showardb1e51872008-10-07 11:08:18 +00008from autotest_lib.client.common_lib import enum, host_protections, global_config
showardeaa408e2009-09-11 18:45:31 +00009from autotest_lib.client.common_lib import host_queue_entry_states
mblighe8819cd2008-02-15 16:48:40 +000010
showard0fc38302008-10-23 00:44:07 +000011# job options and user preferences
jamesrendd855242010-03-02 22:23:44 +000012DEFAULT_REBOOT_BEFORE = model_attributes.RebootBefore.IF_DIRTY
13DEFAULT_REBOOT_AFTER = model_attributes.RebootBefore.ALWAYS
mblighe8819cd2008-02-15 16:48:40 +000014
showard89f84db2009-03-12 20:39:13 +000015
mblighe8819cd2008-02-15 16:48:40 +000016class AclAccessViolation(Exception):
jadmanski0afbb632008-06-06 21:10:57 +000017 """\
18 Raised when an operation is attempted with proper permissions as
19 dictated by ACLs.
20 """
mblighe8819cd2008-02-15 16:48:40 +000021
22
showard205fd602009-03-21 00:17:35 +000023class AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model):
showard89f84db2009-03-12 20:39:13 +000024 """\
25 An atomic group defines a collection of hosts which must only be scheduled
26 all at once. Any host with a label having an atomic group will only be
27 scheduled for a job at the same time as other hosts sharing that label.
28
29 Required:
30 name: A name for this atomic group. ex: 'rack23' or 'funky_net'
31 max_number_of_machines: The maximum number of machines that will be
32 scheduled at once when scheduling jobs to this atomic group.
33 The job.synch_count is considered the minimum.
34
35 Optional:
36 description: Arbitrary text description of this group's purpose.
37 """
showarda5288b42009-07-28 20:06:08 +000038 name = dbmodels.CharField(max_length=255, unique=True)
showard89f84db2009-03-12 20:39:13 +000039 description = dbmodels.TextField(blank=True)
showarde9450c92009-06-30 01:58:52 +000040 # This magic value is the default to simplify the scheduler logic.
41 # It must be "large". The common use of atomic groups is to want all
42 # machines in the group to be used, limits on which subset used are
43 # often chosen via dependency labels.
44 INFINITE_MACHINES = 333333333
45 max_number_of_machines = dbmodels.IntegerField(default=INFINITE_MACHINES)
showard205fd602009-03-21 00:17:35 +000046 invalid = dbmodels.BooleanField(default=False,
showarda5288b42009-07-28 20:06:08 +000047 editable=settings.FULL_ADMIN)
showard89f84db2009-03-12 20:39:13 +000048
showard89f84db2009-03-12 20:39:13 +000049 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +000050 objects = model_logic.ModelWithInvalidManager()
showard205fd602009-03-21 00:17:35 +000051 valid_objects = model_logic.ValidObjectsManager()
52
53
showard29f7cd22009-04-29 21:16:24 +000054 def enqueue_job(self, job, is_template=False):
showardc92da832009-04-07 18:14:34 +000055 """Enqueue a job on an associated atomic group of hosts."""
showard29f7cd22009-04-29 21:16:24 +000056 queue_entry = HostQueueEntry.create(atomic_group=self, job=job,
57 is_template=is_template)
showardc92da832009-04-07 18:14:34 +000058 queue_entry.save()
59
60
showard205fd602009-03-21 00:17:35 +000061 def clean_object(self):
62 self.label_set.clear()
showard89f84db2009-03-12 20:39:13 +000063
64
65 class Meta:
showardeab66ce2009-12-23 00:03:56 +000066 db_table = 'afe_atomic_groups'
showard89f84db2009-03-12 20:39:13 +000067
showard205fd602009-03-21 00:17:35 +000068
showarda5288b42009-07-28 20:06:08 +000069 def __unicode__(self):
70 return unicode(self.name)
showard89f84db2009-03-12 20:39:13 +000071
72
showard7c785282008-05-29 19:45:12 +000073class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000074 """\
75 Required:
showard89f84db2009-03-12 20:39:13 +000076 name: label name
mblighe8819cd2008-02-15 16:48:40 +000077
jadmanski0afbb632008-06-06 21:10:57 +000078 Optional:
showard89f84db2009-03-12 20:39:13 +000079 kernel_config: URL/path to kernel config for jobs run on this label.
80 platform: If True, this is a platform label (defaults to False).
81 only_if_needed: If True, a Host with this label can only be used if that
82 label is requested by the job/test (either as the meta_host or
83 in the job_dependencies).
84 atomic_group: The atomic group associated with this label.
jadmanski0afbb632008-06-06 21:10:57 +000085 """
showarda5288b42009-07-28 20:06:08 +000086 name = dbmodels.CharField(max_length=255, unique=True)
87 kernel_config = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +000088 platform = dbmodels.BooleanField(default=False)
89 invalid = dbmodels.BooleanField(default=False,
90 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +000091 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +000092
jadmanski0afbb632008-06-06 21:10:57 +000093 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +000094 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +000095 valid_objects = model_logic.ValidObjectsManager()
showard89f84db2009-03-12 20:39:13 +000096 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
97
mbligh5244cbb2008-04-24 20:39:52 +000098
jadmanski0afbb632008-06-06 21:10:57 +000099 def clean_object(self):
100 self.host_set.clear()
showard01a51672009-05-29 18:42:37 +0000101 self.test_set.clear()
mblighe8819cd2008-02-15 16:48:40 +0000102
103
showard29f7cd22009-04-29 21:16:24 +0000104 def enqueue_job(self, job, atomic_group=None, is_template=False):
showardc92da832009-04-07 18:14:34 +0000105 """Enqueue a job on any host of this label."""
showard29f7cd22009-04-29 21:16:24 +0000106 queue_entry = HostQueueEntry.create(meta_host=self, job=job,
107 is_template=is_template,
108 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000109 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000110
111
jadmanski0afbb632008-06-06 21:10:57 +0000112 class Meta:
showardeab66ce2009-12-23 00:03:56 +0000113 db_table = 'afe_labels'
mblighe8819cd2008-02-15 16:48:40 +0000114
showarda5288b42009-07-28 20:06:08 +0000115 def __unicode__(self):
116 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000117
118
showardfb2a7fa2008-07-17 17:04:12 +0000119class User(dbmodels.Model, model_logic.ModelExtensions):
120 """\
121 Required:
122 login :user login name
123
124 Optional:
125 access_level: 0=User (default), 1=Admin, 100=Root
126 """
127 ACCESS_ROOT = 100
128 ACCESS_ADMIN = 1
129 ACCESS_USER = 0
130
showard64a95952010-01-13 21:27:16 +0000131 AUTOTEST_SYSTEM = 'autotest_system'
132
showarda5288b42009-07-28 20:06:08 +0000133 login = dbmodels.CharField(max_length=255, unique=True)
showardfb2a7fa2008-07-17 17:04:12 +0000134 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
135
showard0fc38302008-10-23 00:44:07 +0000136 # user preferences
jamesrendd855242010-03-02 22:23:44 +0000137 reboot_before = dbmodels.SmallIntegerField(
138 choices=model_attributes.RebootBefore.choices(), blank=True,
139 default=DEFAULT_REBOOT_BEFORE)
140 reboot_after = dbmodels.SmallIntegerField(
141 choices=model_attributes.RebootAfter.choices(), blank=True,
142 default=DEFAULT_REBOOT_AFTER)
showard97db5ba2008-11-12 18:18:02 +0000143 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000144
showardfb2a7fa2008-07-17 17:04:12 +0000145 name_field = 'login'
146 objects = model_logic.ExtendedManager()
147
148
showarda5288b42009-07-28 20:06:08 +0000149 def save(self, *args, **kwargs):
showardfb2a7fa2008-07-17 17:04:12 +0000150 # is this a new object being saved for the first time?
151 first_time = (self.id is None)
152 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000153 if user and not user.is_superuser() and user.login != self.login:
154 raise AclAccessViolation("You cannot modify user " + self.login)
showarda5288b42009-07-28 20:06:08 +0000155 super(User, self).save(*args, **kwargs)
showardfb2a7fa2008-07-17 17:04:12 +0000156 if first_time:
157 everyone = AclGroup.objects.get(name='Everyone')
158 everyone.users.add(self)
159
160
161 def is_superuser(self):
162 return self.access_level >= self.ACCESS_ROOT
163
164
showard64a95952010-01-13 21:27:16 +0000165 @classmethod
166 def current_user(cls):
167 user = thread_local.get_user()
168 if user is None:
showardcfcdd802010-01-15 00:16:33 +0000169 user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM)
showard64a95952010-01-13 21:27:16 +0000170 user.access_level = cls.ACCESS_ROOT
171 user.save()
172 return user
173
174
showardfb2a7fa2008-07-17 17:04:12 +0000175 class Meta:
showardeab66ce2009-12-23 00:03:56 +0000176 db_table = 'afe_users'
showardfb2a7fa2008-07-17 17:04:12 +0000177
showarda5288b42009-07-28 20:06:08 +0000178 def __unicode__(self):
179 return unicode(self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000180
181
showardf8b19042009-05-12 17:22:49 +0000182class Host(model_logic.ModelWithInvalid, dbmodels.Model,
183 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000184 """\
185 Required:
186 hostname
mblighe8819cd2008-02-15 16:48:40 +0000187
jadmanski0afbb632008-06-06 21:10:57 +0000188 optional:
showard21baa452008-10-21 00:08:39 +0000189 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000190
jadmanski0afbb632008-06-06 21:10:57 +0000191 Internal:
192 synch_id: currently unused
193 status: string describing status of host
showard21baa452008-10-21 00:08:39 +0000194 invalid: true if the host has been deleted
195 protection: indicates what can be done to this host during repair
196 locked_by: user that locked the host, or null if the host is unlocked
197 lock_time: DateTime at which the host was locked
198 dirty: true if the host has been used without being rebooted
jadmanski0afbb632008-06-06 21:10:57 +0000199 """
200 Status = enum.Enum('Verifying', 'Running', 'Ready', 'Repairing',
jamesren121eee62010-04-13 19:10:12 +0000201 'Repair Failed', 'Cleaning', 'Pending',
showard6d7b2ff2009-06-10 00:16:47 +0000202 string_values=True)
showard4d233752010-01-20 19:06:40 +0000203 Protection = host_protections.Protection
mblighe8819cd2008-02-15 16:48:40 +0000204
showarda5288b42009-07-28 20:06:08 +0000205 hostname = dbmodels.CharField(max_length=255, unique=True)
showardeab66ce2009-12-23 00:03:56 +0000206 labels = dbmodels.ManyToManyField(Label, blank=True,
207 db_table='afe_hosts_labels')
jadmanski0afbb632008-06-06 21:10:57 +0000208 locked = dbmodels.BooleanField(default=False)
209 synch_id = dbmodels.IntegerField(blank=True, null=True,
210 editable=settings.FULL_ADMIN)
showarda5288b42009-07-28 20:06:08 +0000211 status = dbmodels.CharField(max_length=255, default=Status.READY,
jadmanski0afbb632008-06-06 21:10:57 +0000212 choices=Status.choices(),
213 editable=settings.FULL_ADMIN)
214 invalid = dbmodels.BooleanField(default=False,
215 editable=settings.FULL_ADMIN)
showardd5afc2f2008-08-12 17:19:44 +0000216 protection = dbmodels.SmallIntegerField(null=False, blank=True,
showarddf062562008-07-03 19:56:37 +0000217 choices=host_protections.choices,
218 default=host_protections.default)
showardfb2a7fa2008-07-17 17:04:12 +0000219 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
220 lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False)
showard21baa452008-10-21 00:08:39 +0000221 dirty = dbmodels.BooleanField(default=True, editable=settings.FULL_ADMIN)
mblighe8819cd2008-02-15 16:48:40 +0000222
jadmanski0afbb632008-06-06 21:10:57 +0000223 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000224 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000225 valid_objects = model_logic.ValidObjectsManager()
mbligh5244cbb2008-04-24 20:39:52 +0000226
showard2bab8f42008-11-12 18:15:22 +0000227
228 def __init__(self, *args, **kwargs):
229 super(Host, self).__init__(*args, **kwargs)
230 self._record_attributes(['status'])
231
232
showardb8471e32008-07-03 19:51:08 +0000233 @staticmethod
234 def create_one_time_host(hostname):
235 query = Host.objects.filter(hostname=hostname)
236 if query.count() == 0:
237 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000238 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000239 else:
240 host = query[0]
241 if not host.invalid:
242 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000243 'hostname' : '%s already exists in the autotest DB. '
244 'Select it rather than entering it as a one time '
245 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000246 })
showard1ab512b2008-07-30 23:39:04 +0000247 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000248 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000249 host.save()
showard2924b0a2009-06-18 23:16:15 +0000250 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000251 return host
mbligh5244cbb2008-04-24 20:39:52 +0000252
showard1ff7b2e2009-05-15 23:17:18 +0000253
showardafd97de2009-10-01 18:45:09 +0000254 def resurrect_object(self, old_object):
255 super(Host, self).resurrect_object(old_object)
256 # invalid hosts can be in use by the scheduler (as one-time hosts), so
257 # don't change the status
258 self.status = old_object.status
259
260
jadmanski0afbb632008-06-06 21:10:57 +0000261 def clean_object(self):
262 self.aclgroup_set.clear()
263 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000264
265
showarda5288b42009-07-28 20:06:08 +0000266 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000267 # extra spaces in the hostname can be a sneaky source of errors
268 self.hostname = self.hostname.strip()
269 # is this a new object being saved for the first time?
270 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000271 if not first_time:
272 AclGroup.check_for_acl_violation_hosts([self])
showardfb2a7fa2008-07-17 17:04:12 +0000273 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000274 self.locked_by = User.current_user()
showardfb2a7fa2008-07-17 17:04:12 +0000275 self.lock_time = datetime.now()
showard21baa452008-10-21 00:08:39 +0000276 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000277 elif not self.locked and self.locked_by:
278 self.locked_by = None
279 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000280 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000281 if first_time:
282 everyone = AclGroup.objects.get(name='Everyone')
283 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000284 self._check_for_updated_attributes()
285
mblighe8819cd2008-02-15 16:48:40 +0000286
showardb8471e32008-07-03 19:51:08 +0000287 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000288 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000289 for queue_entry in self.hostqueueentry_set.all():
290 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000291 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000292 super(Host, self).delete()
293
mblighe8819cd2008-02-15 16:48:40 +0000294
showard2bab8f42008-11-12 18:15:22 +0000295 def on_attribute_changed(self, attribute, old_value):
296 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000297 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000298
299
showard29f7cd22009-04-29 21:16:24 +0000300 def enqueue_job(self, job, atomic_group=None, is_template=False):
showardc92da832009-04-07 18:14:34 +0000301 """Enqueue a job on this host."""
showard29f7cd22009-04-29 21:16:24 +0000302 queue_entry = HostQueueEntry.create(host=self, job=job,
303 is_template=is_template,
304 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000305 # allow recovery of dead hosts from the frontend
306 if not self.active_queue_entry() and self.is_dead():
307 self.status = Host.Status.READY
308 self.save()
309 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000310
showard08f981b2008-06-24 21:59:03 +0000311 block = IneligibleHostQueue(job=job, host=self)
312 block.save()
313
mblighe8819cd2008-02-15 16:48:40 +0000314
jadmanski0afbb632008-06-06 21:10:57 +0000315 def platform(self):
316 # TODO(showard): slighly hacky?
317 platforms = self.labels.filter(platform=True)
318 if len(platforms) == 0:
319 return None
320 return platforms[0]
321 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000322
323
showardcafd16e2009-05-29 18:37:49 +0000324 @classmethod
325 def check_no_platform(cls, hosts):
326 Host.objects.populate_relationships(hosts, Label, 'label_list')
327 errors = []
328 for host in hosts:
329 platforms = [label.name for label in host.label_list
330 if label.platform]
331 if platforms:
332 # do a join, just in case this host has multiple platforms,
333 # we'll be able to see it
334 errors.append('Host %s already has a platform: %s' % (
335 host.hostname, ', '.join(platforms)))
336 if errors:
337 raise model_logic.ValidationError({'labels': '; '.join(errors)})
338
339
jadmanski0afbb632008-06-06 21:10:57 +0000340 def is_dead(self):
341 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000342
343
jadmanski0afbb632008-06-06 21:10:57 +0000344 def active_queue_entry(self):
345 active = list(self.hostqueueentry_set.filter(active=True))
346 if not active:
347 return None
348 assert len(active) == 1, ('More than one active entry for '
349 'host ' + self.hostname)
350 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000351
352
showardf8b19042009-05-12 17:22:49 +0000353 def _get_attribute_model_and_args(self, attribute):
354 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000355
356
jadmanski0afbb632008-06-06 21:10:57 +0000357 class Meta:
showardeab66ce2009-12-23 00:03:56 +0000358 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000359
showarda5288b42009-07-28 20:06:08 +0000360 def __unicode__(self):
361 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000362
363
showard0957a842009-05-11 19:25:08 +0000364class HostAttribute(dbmodels.Model):
365 """Arbitrary keyvals associated with hosts."""
366 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000367 attribute = dbmodels.CharField(max_length=90)
368 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000369
370 objects = model_logic.ExtendedManager()
371
372 class Meta:
showardeab66ce2009-12-23 00:03:56 +0000373 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000374
375
showard7c785282008-05-29 19:45:12 +0000376class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000377 """\
378 Required:
showard909c7a62008-07-15 21:52:38 +0000379 author: author name
380 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000381 name: test name
showard909c7a62008-07-15 21:52:38 +0000382 time: short, medium, long
383 test_class: This describes the class for your the test belongs in.
384 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000385 test_type: Client or Server
386 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000387 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
388 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000389 i.e. if sync_count = 2 it is a sync job that requires two
390 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000391 Optional:
showard909c7a62008-07-15 21:52:38 +0000392 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000393 dependency_labels: many-to-many relationship with labels corresponding to
394 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000395 experimental: If this is set to True production servers will ignore the test
396 run_verify: Whether or not the scheduler should run the verify stage
jadmanski0afbb632008-06-06 21:10:57 +0000397 """
showard909c7a62008-07-15 21:52:38 +0000398 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
jamesrencd7a81a2010-04-21 20:39:08 +0000399 TestTypes = model_attributes.TestTypes
jadmanski0afbb632008-06-06 21:10:57 +0000400 # TODO(showard) - this should be merged with Job.ControlType (but right
401 # now they use opposite values)
mblighe8819cd2008-02-15 16:48:40 +0000402
showarda5288b42009-07-28 20:06:08 +0000403 name = dbmodels.CharField(max_length=255, unique=True)
404 author = dbmodels.CharField(max_length=255)
405 test_class = dbmodels.CharField(max_length=255)
406 test_category = dbmodels.CharField(max_length=255)
407 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000408 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000409 experimental = dbmodels.BooleanField(default=True)
410 run_verify = dbmodels.BooleanField(default=True)
411 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
412 default=TestTime.MEDIUM)
jamesrencd7a81a2010-04-21 20:39:08 +0000413 test_type = dbmodels.SmallIntegerField(choices=TestTypes.choices())
showard909c7a62008-07-15 21:52:38 +0000414 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000415 path = dbmodels.CharField(max_length=255, unique=True)
mblighe8819cd2008-02-15 16:48:40 +0000416
showardeab66ce2009-12-23 00:03:56 +0000417 dependency_labels = (
418 dbmodels.ManyToManyField(Label, blank=True,
419 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000420 name_field = 'name'
421 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000422
423
jamesren35a70222010-02-16 19:30:46 +0000424 def admin_description(self):
425 escaped_description = saxutils.escape(self.description)
426 return '<span style="white-space:pre">%s</span>' % escaped_description
427 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000428 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000429
430
jadmanski0afbb632008-06-06 21:10:57 +0000431 class Meta:
showardeab66ce2009-12-23 00:03:56 +0000432 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000433
showarda5288b42009-07-28 20:06:08 +0000434 def __unicode__(self):
435 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000436
437
showard2b9a88b2008-06-13 20:55:03 +0000438class Profiler(dbmodels.Model, model_logic.ModelExtensions):
439 """\
440 Required:
441 name: profiler name
442 test_type: Client or Server
443
444 Optional:
445 description: arbirary text description
446 """
showarda5288b42009-07-28 20:06:08 +0000447 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000448 description = dbmodels.TextField(blank=True)
449
450 name_field = 'name'
451 objects = model_logic.ExtendedManager()
452
453
454 class Meta:
showardeab66ce2009-12-23 00:03:56 +0000455 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000456
showarda5288b42009-07-28 20:06:08 +0000457 def __unicode__(self):
458 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000459
460
showard7c785282008-05-29 19:45:12 +0000461class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000462 """\
463 Required:
464 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000465
jadmanski0afbb632008-06-06 21:10:57 +0000466 Optional:
467 description: arbitrary description of group
468 """
showarda5288b42009-07-28 20:06:08 +0000469 name = dbmodels.CharField(max_length=255, unique=True)
470 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000471 users = dbmodels.ManyToManyField(User, blank=False,
472 db_table='afe_acl_groups_users')
473 hosts = dbmodels.ManyToManyField(Host, blank=True,
474 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000475
jadmanski0afbb632008-06-06 21:10:57 +0000476 name_field = 'name'
477 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000478
showard08f981b2008-06-24 21:59:03 +0000479 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000480 def check_for_acl_violation_hosts(hosts):
showard64a95952010-01-13 21:27:16 +0000481 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000482 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000483 return
showard3dd47c22008-07-10 00:41:36 +0000484 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000485 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000486 for host in hosts:
487 # Check if the user has access to this host,
showard98ead172009-06-22 18:13:24 +0000488 # but only if it is not a metahost or a one-time-host
489 no_access = (isinstance(host, Host)
490 and not host.invalid
491 and int(host.id) not in accessible_host_ids)
492 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000493 raise AclAccessViolation("%s does not have access to %s" %
494 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000495
showard9dbdcda2008-10-14 17:34:36 +0000496
497 @staticmethod
showarddc817512008-11-12 18:16:41 +0000498 def check_abort_permissions(queue_entries):
499 """
500 look for queue entries that aren't abortable, meaning
501 * the job isn't owned by this user, and
502 * the machine isn't ACL-accessible, or
503 * the machine is in the "Everyone" ACL
504 """
showard64a95952010-01-13 21:27:16 +0000505 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000506 if user.is_superuser():
507 return
showarddc817512008-11-12 18:16:41 +0000508 not_owned = queue_entries.exclude(job__owner=user.login)
509 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000510 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
511 # 1.0.
512 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000513 accessible_ids = set(
514 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000515 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000516 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000517 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000518 cannot_abort = [entry for entry in not_owned.select_related()
519 if entry.id not in accessible_ids
520 or entry.id in public_ids]
521 if len(cannot_abort) == 0:
522 return
523 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000524 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000525 for entry in cannot_abort)
526 raise AclAccessViolation('You cannot abort the following job entries: '
527 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000528
529
showard3dd47c22008-07-10 00:41:36 +0000530 def check_for_acl_violation_acl_group(self):
showard64a95952010-01-13 21:27:16 +0000531 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000532 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000533 return
534 if self.name == 'Everyone':
535 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000536 if not user in self.users.all():
537 raise AclAccessViolation("You do not have access to %s"
538 % self.name)
539
540 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000541 def on_host_membership_change():
542 everyone = AclGroup.objects.get(name='Everyone')
543
showard3dd47c22008-07-10 00:41:36 +0000544 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000545 # TODO(showard): this is a bit of a hack, since the fact that this query
546 # works is kind of a coincidence of Django internals. This trick
547 # doesn't work in general (on all foreign key relationships). I'll
548 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000549 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000550 everyone.hosts.add(*orphaned_hosts.distinct())
551
552 # find hosts in both Everyone and another ACL group, and remove them
553 # from Everyone
showarda5288b42009-07-28 20:06:08 +0000554 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
555 acled_hosts = set()
556 for host in hosts_in_everyone:
557 # Has an ACL group other than Everyone
558 if host.aclgroup_set.count() > 1:
559 acled_hosts.add(host)
560 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +0000561
562
563 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000564 if (self.name == 'Everyone'):
565 raise AclAccessViolation("You cannot delete 'Everyone'!")
566 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000567 super(AclGroup, self).delete()
568 self.on_host_membership_change()
569
570
showard04f2cd82008-07-25 20:53:31 +0000571 def add_current_user_if_empty(self):
572 if not self.users.count():
showard64a95952010-01-13 21:27:16 +0000573 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +0000574
575
showard8cbaf1e2009-09-08 16:27:04 +0000576 def perform_after_save(self, change):
577 if not change:
showard64a95952010-01-13 21:27:16 +0000578 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +0000579 self.add_current_user_if_empty()
580 self.on_host_membership_change()
581
582
583 def save(self, *args, **kwargs):
584 change = bool(self.id)
585 if change:
586 # Check the original object for an ACL violation
587 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
588 super(AclGroup, self).save(*args, **kwargs)
589 self.perform_after_save(change)
590
showardeb3be4d2008-04-21 20:59:26 +0000591
jadmanski0afbb632008-06-06 21:10:57 +0000592 class Meta:
showardeab66ce2009-12-23 00:03:56 +0000593 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000594
showarda5288b42009-07-28 20:06:08 +0000595 def __unicode__(self):
596 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000597
mblighe8819cd2008-02-15 16:48:40 +0000598
showard7c785282008-05-29 19:45:12 +0000599class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +0000600 'Custom manager to provide efficient status counts querying.'
601 def get_status_counts(self, job_ids):
602 """\
603 Returns a dictionary mapping the given job IDs to their status
604 count dictionaries.
605 """
606 if not job_ids:
607 return {}
608 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
609 cursor = connection.cursor()
610 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +0000611 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +0000612 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +0000613 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +0000614 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +0000615 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +0000616 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +0000617 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +0000618 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +0000619 full_status = HostQueueEntry.compute_full_status(status, aborted,
620 complete)
showardb6d16622009-05-26 19:35:29 +0000621 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +0000622 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +0000623 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +0000624
625
showard7c785282008-05-29 19:45:12 +0000626class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000627 """\
628 owner: username of job owner
629 name: job name (does not have to be unique)
630 priority: Low, Medium, High, Urgent (or 0-3)
631 control_file: contents of control file
632 control_type: Client or Server
633 created_on: date of job creation
634 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +0000635 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +0000636 run_verify: Whether or not to run the verify phase
showard12f3e322009-05-13 21:27:42 +0000637 timeout: hours from queuing time until job times out
638 max_runtime_hrs: hours from job starting time until job times out
showard542e8402008-09-19 20:16:18 +0000639 email_list: list of people to email on completion delimited by any of:
640 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +0000641 dependency_labels: many-to-many relationship with labels corresponding to
642 job dependencies
showard21baa452008-10-21 00:08:39 +0000643 reboot_before: Never, If dirty, or Always
644 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +0000645 parse_failed_repair: if True, a failed repair launched by this job will have
646 its results parsed as part of the job.
jadmanski0afbb632008-06-06 21:10:57 +0000647 """
showardb1e51872008-10-07 11:08:18 +0000648 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
649 'AUTOTEST_WEB', 'job_timeout_default', default=240)
showard12f3e322009-05-13 21:27:42 +0000650 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
651 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
showarda1e74b32009-05-12 17:32:04 +0000652 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
653 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
654 default=False)
showardb1e51872008-10-07 11:08:18 +0000655
jadmanski0afbb632008-06-06 21:10:57 +0000656 Priority = enum.Enum('Low', 'Medium', 'High', 'Urgent')
657 ControlType = enum.Enum('Server', 'Client', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000658
showarda5288b42009-07-28 20:06:08 +0000659 owner = dbmodels.CharField(max_length=255)
660 name = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +0000661 priority = dbmodels.SmallIntegerField(choices=Priority.choices(),
662 blank=True, # to allow 0
663 default=Priority.MEDIUM)
664 control_file = dbmodels.TextField()
665 control_type = dbmodels.SmallIntegerField(choices=ControlType.choices(),
showardb1e51872008-10-07 11:08:18 +0000666 blank=True, # to allow 0
667 default=ControlType.CLIENT)
showard68c7aa02008-10-09 16:49:11 +0000668 created_on = dbmodels.DateTimeField()
showard2bab8f42008-11-12 18:15:22 +0000669 synch_count = dbmodels.IntegerField(null=True, default=1)
showardb1e51872008-10-07 11:08:18 +0000670 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
showard9976ce92008-10-15 20:28:13 +0000671 run_verify = dbmodels.BooleanField(default=True)
showarda5288b42009-07-28 20:06:08 +0000672 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000673 dependency_labels = (
674 dbmodels.ManyToManyField(Label, blank=True,
675 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +0000676 reboot_before = dbmodels.SmallIntegerField(
677 choices=model_attributes.RebootBefore.choices(), blank=True,
678 default=DEFAULT_REBOOT_BEFORE)
679 reboot_after = dbmodels.SmallIntegerField(
680 choices=model_attributes.RebootAfter.choices(), blank=True,
681 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +0000682 parse_failed_repair = dbmodels.BooleanField(
683 default=DEFAULT_PARSE_FAILED_REPAIR)
showard12f3e322009-05-13 21:27:42 +0000684 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
mblighe8819cd2008-02-15 16:48:40 +0000685
686
jadmanski0afbb632008-06-06 21:10:57 +0000687 # custom manager
688 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +0000689
690
jadmanski0afbb632008-06-06 21:10:57 +0000691 def is_server_job(self):
692 return self.control_type == self.ControlType.SERVER
mblighe8819cd2008-02-15 16:48:40 +0000693
694
jadmanski0afbb632008-06-06 21:10:57 +0000695 @classmethod
showarda1e74b32009-05-12 17:32:04 +0000696 def create(cls, owner, options, hosts):
jadmanski0afbb632008-06-06 21:10:57 +0000697 """\
698 Creates a job by taking some information (the listed args)
699 and filling in the rest of the necessary information.
700 """
showard3dd47c22008-07-10 00:41:36 +0000701 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +0000702
703 user = User.current_user()
704 if options.get('reboot_before') is None:
705 options['reboot_before'] = user.get_reboot_before_display()
706 if options.get('reboot_after') is None:
707 options['reboot_after'] = user.get_reboot_after_display()
708
jadmanski0afbb632008-06-06 21:10:57 +0000709 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +0000710 owner=owner,
711 name=options['name'],
712 priority=options['priority'],
713 control_file=options['control_file'],
714 control_type=options['control_type'],
715 synch_count=options.get('synch_count'),
716 timeout=options.get('timeout'),
showard12f3e322009-05-13 21:27:42 +0000717 max_runtime_hrs=options.get('max_runtime_hrs'),
showarda1e74b32009-05-12 17:32:04 +0000718 run_verify=options.get('run_verify'),
719 email_list=options.get('email_list'),
720 reboot_before=options.get('reboot_before'),
721 reboot_after=options.get('reboot_after'),
722 parse_failed_repair=options.get('parse_failed_repair'),
showard68c7aa02008-10-09 16:49:11 +0000723 created_on=datetime.now())
mblighe8819cd2008-02-15 16:48:40 +0000724
showarda1e74b32009-05-12 17:32:04 +0000725 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +0000726
jamesrend8b6e172010-04-16 23:45:00 +0000727 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +0000728 for key, value in options['keyvals'].iteritems():
729 JobKeyval.objects.create(job=job, key=key, value=value)
730
jadmanski0afbb632008-06-06 21:10:57 +0000731 return job
mblighe8819cd2008-02-15 16:48:40 +0000732
733
showard29f7cd22009-04-29 21:16:24 +0000734 def queue(self, hosts, atomic_group=None, is_template=False):
showardc92da832009-04-07 18:14:34 +0000735 """Enqueue a job on the given hosts."""
showarda9545c02009-12-18 22:44:26 +0000736 if not hosts:
737 if atomic_group:
738 # No hosts or labels are required to queue an atomic group
739 # Job. However, if they are given, we respect them below.
740 atomic_group.enqueue_job(self, is_template=is_template)
741 else:
742 # hostless job
743 entry = HostQueueEntry.create(job=self, is_template=is_template)
744 entry.save()
745 return
746
jadmanski0afbb632008-06-06 21:10:57 +0000747 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +0000748 host.enqueue_job(self, atomic_group=atomic_group,
749 is_template=is_template)
750
751
752 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
753 rec = RecurringRun(job=self, start_date=start_date,
754 loop_period=loop_period,
755 loop_count=loop_count,
756 owner=User.objects.get(login=owner))
757 rec.save()
758 return rec.id
mblighe8819cd2008-02-15 16:48:40 +0000759
760
jadmanski0afbb632008-06-06 21:10:57 +0000761 def user(self):
762 try:
763 return User.objects.get(login=self.owner)
764 except self.DoesNotExist:
765 return None
mblighe8819cd2008-02-15 16:48:40 +0000766
767
showard64a95952010-01-13 21:27:16 +0000768 def abort(self):
showard98863972008-10-29 21:14:56 +0000769 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +0000770 queue_entry.abort()
showard98863972008-10-29 21:14:56 +0000771
772
showardd1195652009-12-08 22:21:02 +0000773 def tag(self):
774 return '%s-%s' % (self.id, self.owner)
775
776
showardc1a98d12010-01-15 00:22:22 +0000777 def keyval_dict(self):
778 return dict((keyval.key, keyval.value)
779 for keyval in self.jobkeyval_set.all())
780
781
jadmanski0afbb632008-06-06 21:10:57 +0000782 class Meta:
showardeab66ce2009-12-23 00:03:56 +0000783 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +0000784
showarda5288b42009-07-28 20:06:08 +0000785 def __unicode__(self):
786 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +0000787
788
showardc1a98d12010-01-15 00:22:22 +0000789class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
790 """Keyvals associated with jobs"""
791 job = dbmodels.ForeignKey(Job)
792 key = dbmodels.CharField(max_length=90)
793 value = dbmodels.CharField(max_length=300)
794
795 objects = model_logic.ExtendedManager()
796
797 class Meta:
798 db_table = 'afe_job_keyvals'
799
800
showard7c785282008-05-29 19:45:12 +0000801class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000802 job = dbmodels.ForeignKey(Job)
803 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +0000804
jadmanski0afbb632008-06-06 21:10:57 +0000805 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000806
jadmanski0afbb632008-06-06 21:10:57 +0000807 class Meta:
showardeab66ce2009-12-23 00:03:56 +0000808 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +0000809
mblighe8819cd2008-02-15 16:48:40 +0000810
showard7c785282008-05-29 19:45:12 +0000811class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
showardeaa408e2009-09-11 18:45:31 +0000812 Status = host_queue_entry_states.Status
813 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +0000814 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
showarda3ab0d52008-11-03 19:03:47 +0000815
jadmanski0afbb632008-06-06 21:10:57 +0000816 job = dbmodels.ForeignKey(Job)
817 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +0000818 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +0000819 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
820 db_column='meta_host')
821 active = dbmodels.BooleanField(default=False)
822 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +0000823 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +0000824 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
825 default='')
showard89f84db2009-03-12 20:39:13 +0000826 # If atomic_group is set, this is a virtual HostQueueEntry that will
827 # be expanded into many actual hosts within the group at schedule time.
828 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +0000829 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +0000830 started_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +0000831
jadmanski0afbb632008-06-06 21:10:57 +0000832 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000833
mblighe8819cd2008-02-15 16:48:40 +0000834
showard2bab8f42008-11-12 18:15:22 +0000835 def __init__(self, *args, **kwargs):
836 super(HostQueueEntry, self).__init__(*args, **kwargs)
837 self._record_attributes(['status'])
838
839
showard29f7cd22009-04-29 21:16:24 +0000840 @classmethod
841 def create(cls, job, host=None, meta_host=None, atomic_group=None,
842 is_template=False):
843 if is_template:
844 status = cls.Status.TEMPLATE
845 else:
846 status = cls.Status.QUEUED
847
848 return cls(job=job, host=host, meta_host=meta_host,
849 atomic_group=atomic_group, status=status)
850
851
showarda5288b42009-07-28 20:06:08 +0000852 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +0000853 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +0000854 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +0000855 self._check_for_updated_attributes()
856
857
showardc0ac3a72009-07-08 21:14:45 +0000858 def execution_path(self):
859 """
860 Path to this entry's results (relative to the base results directory).
861 """
showardd1195652009-12-08 22:21:02 +0000862 return os.path.join(self.job.tag(), self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +0000863
864
showard3f15eed2008-11-14 22:40:48 +0000865 def host_or_metahost_name(self):
866 if self.host:
867 return self.host.hostname
showard7890e792009-07-28 20:10:20 +0000868 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +0000869 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +0000870 else:
871 assert self.atomic_group, "no host, meta_host or atomic group!"
872 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +0000873
874
showard2bab8f42008-11-12 18:15:22 +0000875 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +0000876 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +0000877 self.active, self.complete = True, False
878 elif self.status in self.COMPLETE_STATUSES:
879 self.active, self.complete = False, True
880 else:
881 self.active, self.complete = False, False
882
883
884 def on_attribute_changed(self, attribute, old_value):
885 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000886 logging.info('%s/%d (%d) -> %s' % (self.host, self.job.id, self.id,
887 self.status))
showard2bab8f42008-11-12 18:15:22 +0000888
889
jadmanski0afbb632008-06-06 21:10:57 +0000890 def is_meta_host_entry(self):
891 'True if this is a entry has a meta_host instead of a host.'
892 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +0000893
showarda3ab0d52008-11-03 19:03:47 +0000894
showard4c119042008-09-29 19:16:18 +0000895 def log_abort(self, user):
896 abort_log = AbortedHostQueueEntry(queue_entry=self, aborted_by=user)
897 abort_log.save()
898
showarda3ab0d52008-11-03 19:03:47 +0000899
showard64a95952010-01-13 21:27:16 +0000900 def abort(self):
showarda3ab0d52008-11-03 19:03:47 +0000901 # this isn't completely immune to race conditions since it's not atomic,
902 # but it should be safe given the scheduler's behavior.
showardd3dc1992009-04-22 21:01:40 +0000903 if not self.complete and not self.aborted:
showard64a95952010-01-13 21:27:16 +0000904 self.log_abort(User.current_user())
showardd3dc1992009-04-22 21:01:40 +0000905 self.aborted = True
showarda3ab0d52008-11-03 19:03:47 +0000906 self.save()
mblighe8819cd2008-02-15 16:48:40 +0000907
showardd3dc1992009-04-22 21:01:40 +0000908
909 @classmethod
910 def compute_full_status(cls, status, aborted, complete):
911 if aborted and not complete:
912 return 'Aborted (%s)' % status
913 return status
914
915
916 def full_status(self):
917 return self.compute_full_status(self.status, self.aborted,
918 self.complete)
919
920
921 def _postprocess_object_dict(self, object_dict):
922 object_dict['full_status'] = self.full_status()
923
924
jadmanski0afbb632008-06-06 21:10:57 +0000925 class Meta:
showardeab66ce2009-12-23 00:03:56 +0000926 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +0000927
showard12f3e322009-05-13 21:27:42 +0000928
showard4c119042008-09-29 19:16:18 +0000929
showarda5288b42009-07-28 20:06:08 +0000930 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +0000931 hostname = None
932 if self.host:
933 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +0000934 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +0000935
936
showard4c119042008-09-29 19:16:18 +0000937class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
938 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
939 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +0000940 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +0000941
942 objects = model_logic.ExtendedManager()
943
showard68c7aa02008-10-09 16:49:11 +0000944
showarda5288b42009-07-28 20:06:08 +0000945 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +0000946 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +0000947 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +0000948
showard4c119042008-09-29 19:16:18 +0000949 class Meta:
showardeab66ce2009-12-23 00:03:56 +0000950 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +0000951
952
953class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
954 """\
955 job: job to use as a template
956 owner: owner of the instantiated template
957 start_date: Run the job at scheduled date
958 loop_period: Re-run (loop) the job periodically
959 (in every loop_period seconds)
960 loop_count: Re-run (loop) count
961 """
962
963 job = dbmodels.ForeignKey(Job)
964 owner = dbmodels.ForeignKey(User)
965 start_date = dbmodels.DateTimeField()
966 loop_period = dbmodels.IntegerField(blank=True)
967 loop_count = dbmodels.IntegerField(blank=True)
968
969 objects = model_logic.ExtendedManager()
970
971 class Meta:
showardeab66ce2009-12-23 00:03:56 +0000972 db_table = 'afe_recurring_run'
showard29f7cd22009-04-29 21:16:24 +0000973
showarda5288b42009-07-28 20:06:08 +0000974 def __unicode__(self):
975 return u'RecurringRun(job %s, start %s, period %s, count %s)' % (
showard29f7cd22009-04-29 21:16:24 +0000976 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +0000977
978
979class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
980 """\
981 Tasks to run on hosts at the next time they are in the Ready state. Use this
982 for high-priority tasks, such as forced repair or forced reinstall.
983
984 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +0000985 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +0000986 time_requested: date and time the request for this task was made
987 is_active: task is currently running
988 is_complete: task has finished running
showard2fe3f1d2009-07-06 20:19:11 +0000989 time_started: date and time the task started
showard2fe3f1d2009-07-06 20:19:11 +0000990 queue_entry: Host queue entry waiting on this task (or None, if task was not
991 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +0000992 """
showard2fe3f1d2009-07-06 20:19:11 +0000993 Task = enum.Enum('Verify', 'Cleanup', 'Repair', string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +0000994
995 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +0000996 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +0000997 blank=False, null=False)
showard9bb960b2009-11-19 01:02:11 +0000998 requested_by = dbmodels.ForeignKey(User, blank=True, null=True)
showard6d7b2ff2009-06-10 00:16:47 +0000999 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
1000 null=False)
1001 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
1002 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00001003 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00001004 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00001005 success = dbmodels.BooleanField(default=False, blank=False, null=False)
showard6d7b2ff2009-06-10 00:16:47 +00001006
1007 objects = model_logic.ExtendedManager()
1008
1009
showard9bb960b2009-11-19 01:02:11 +00001010 def save(self, **kwargs):
1011 if self.queue_entry:
1012 self.requested_by = User.objects.get(
1013 login=self.queue_entry.job.owner)
1014 super(SpecialTask, self).save(**kwargs)
1015
1016
showarded2afea2009-07-07 20:54:07 +00001017 def execution_path(self):
showardc0ac3a72009-07-08 21:14:45 +00001018 """@see HostQueueEntry.execution_path()"""
showarded2afea2009-07-07 20:54:07 +00001019 return 'hosts/%s/%s-%s' % (self.host.hostname, self.id,
1020 self.task.lower())
1021
1022
showardc0ac3a72009-07-08 21:14:45 +00001023 # property to emulate HostQueueEntry.status
1024 @property
1025 def status(self):
1026 """
1027 Return a host queue entry status appropriate for this task. Although
1028 SpecialTasks are not HostQueueEntries, it is helpful to the user to
1029 present similar statuses.
1030 """
1031 if self.is_complete:
showarde60e44e2009-11-13 20:45:38 +00001032 if self.success:
1033 return HostQueueEntry.Status.COMPLETED
1034 return HostQueueEntry.Status.FAILED
showardc0ac3a72009-07-08 21:14:45 +00001035 if self.is_active:
1036 return HostQueueEntry.Status.RUNNING
1037 return HostQueueEntry.Status.QUEUED
1038
1039
1040 # property to emulate HostQueueEntry.started_on
1041 @property
1042 def started_on(self):
1043 return self.time_started
1044
1045
showard6d7b2ff2009-06-10 00:16:47 +00001046 @classmethod
showardc5103442010-01-15 00:20:26 +00001047 def schedule_special_task(cls, host, task):
showard474d1362009-08-20 23:32:01 +00001048 """
showardc5103442010-01-15 00:20:26 +00001049 Schedules a special task on a host if the task is not already scheduled.
showard6d7b2ff2009-06-10 00:16:47 +00001050 """
showardc5103442010-01-15 00:20:26 +00001051 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
1052 is_active=False,
1053 is_complete=False)
1054 if existing_tasks:
1055 return existing_tasks[0]
1056
1057 special_task = SpecialTask(host=host, task=task,
1058 requested_by=User.current_user())
1059 special_task.save()
1060 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00001061
1062
showarded2afea2009-07-07 20:54:07 +00001063 def activate(self):
showard474d1362009-08-20 23:32:01 +00001064 """
1065 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00001066 """
showard97446882009-07-20 22:37:28 +00001067 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00001068 self.is_active = True
1069 self.time_started = datetime.now()
1070 self.save()
1071
1072
showarde60e44e2009-11-13 20:45:38 +00001073 def finish(self, success):
showard474d1362009-08-20 23:32:01 +00001074 """
showard2fe3f1d2009-07-06 20:19:11 +00001075 Sets a task as completed
1076 """
showard97446882009-07-20 22:37:28 +00001077 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00001078 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00001079 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00001080 self.success = success
showard2fe3f1d2009-07-06 20:19:11 +00001081 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00001082
1083
1084 class Meta:
showardeab66ce2009-12-23 00:03:56 +00001085 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00001086
showard474d1362009-08-20 23:32:01 +00001087
showarda5288b42009-07-28 20:06:08 +00001088 def __unicode__(self):
1089 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00001090 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00001091 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00001092 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00001093 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00001094 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00001095
1096 return result