blob: b0711327ac650629c89e10d9106df79cf52fcba0 [file] [log] [blame]
showardf1175bb2009-06-17 19:34:36 +00001import logging
showardfb2a7fa2008-07-17 17:04:12 +00002from datetime import datetime
showard7c785282008-05-29 19:45:12 +00003from django.db import models as dbmodels, connection
showardcafd16e2009-05-29 18:37:49 +00004import common
5from autotest_lib.frontend.afe import model_logic
6from autotest_lib.frontend import settings, thread_local
showardb1e51872008-10-07 11:08:18 +00007from autotest_lib.client.common_lib import enum, host_protections, global_config
mblighe8819cd2008-02-15 16:48:40 +00008
showard0fc38302008-10-23 00:44:07 +00009# job options and user preferences
10RebootBefore = enum.Enum('Never', 'If dirty', 'Always')
11DEFAULT_REBOOT_BEFORE = RebootBefore.IF_DIRTY
12RebootAfter = enum.Enum('Never', 'If all tests passed', 'Always')
13DEFAULT_REBOOT_AFTER = 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 """
38 name = dbmodels.CharField(maxlength=255, unique=True)
39 description = dbmodels.TextField(blank=True)
40 max_number_of_machines = dbmodels.IntegerField(default=1)
showard205fd602009-03-21 00:17:35 +000041 invalid = dbmodels.BooleanField(default=False,
42 editable=settings.FULL_ADMIN)
showard89f84db2009-03-12 20:39:13 +000043
showard89f84db2009-03-12 20:39:13 +000044 name_field = 'name'
showard205fd602009-03-21 00:17:35 +000045 objects = model_logic.ExtendedManager()
46 valid_objects = model_logic.ValidObjectsManager()
47
48
showard29f7cd22009-04-29 21:16:24 +000049 def enqueue_job(self, job, is_template=False):
showardc92da832009-04-07 18:14:34 +000050 """Enqueue a job on an associated atomic group of hosts."""
showard29f7cd22009-04-29 21:16:24 +000051 queue_entry = HostQueueEntry.create(atomic_group=self, job=job,
52 is_template=is_template)
showardc92da832009-04-07 18:14:34 +000053 queue_entry.save()
54
55
showard205fd602009-03-21 00:17:35 +000056 def clean_object(self):
57 self.label_set.clear()
showard89f84db2009-03-12 20:39:13 +000058
59
60 class Meta:
61 db_table = 'atomic_groups'
62
63 class Admin:
64 list_display = ('name', 'description', 'max_number_of_machines')
showard205fd602009-03-21 00:17:35 +000065 # see Host.Admin
66 manager = model_logic.ValidObjectsManager()
67
68 def __str__(self):
69 return self.name
showard89f84db2009-03-12 20:39:13 +000070
71
showard7c785282008-05-29 19:45:12 +000072class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000073 """\
74 Required:
showard89f84db2009-03-12 20:39:13 +000075 name: label name
mblighe8819cd2008-02-15 16:48:40 +000076
jadmanski0afbb632008-06-06 21:10:57 +000077 Optional:
showard89f84db2009-03-12 20:39:13 +000078 kernel_config: URL/path to kernel config for jobs run on this label.
79 platform: If True, this is a platform label (defaults to False).
80 only_if_needed: If True, a Host with this label can only be used if that
81 label is requested by the job/test (either as the meta_host or
82 in the job_dependencies).
83 atomic_group: The atomic group associated with this label.
jadmanski0afbb632008-06-06 21:10:57 +000084 """
85 name = dbmodels.CharField(maxlength=255, unique=True)
86 kernel_config = dbmodels.CharField(maxlength=255, blank=True)
87 platform = dbmodels.BooleanField(default=False)
88 invalid = dbmodels.BooleanField(default=False,
89 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +000090 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +000091
jadmanski0afbb632008-06-06 21:10:57 +000092 name_field = 'name'
93 objects = model_logic.ExtendedManager()
94 valid_objects = model_logic.ValidObjectsManager()
showard89f84db2009-03-12 20:39:13 +000095 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
96
mbligh5244cbb2008-04-24 20:39:52 +000097
jadmanski0afbb632008-06-06 21:10:57 +000098 def clean_object(self):
99 self.host_set.clear()
showard01a51672009-05-29 18:42:37 +0000100 self.test_set.clear()
mblighe8819cd2008-02-15 16:48:40 +0000101
102
showard29f7cd22009-04-29 21:16:24 +0000103 def enqueue_job(self, job, atomic_group=None, is_template=False):
showardc92da832009-04-07 18:14:34 +0000104 """Enqueue a job on any host of this label."""
showard29f7cd22009-04-29 21:16:24 +0000105 queue_entry = HostQueueEntry.create(meta_host=self, job=job,
106 is_template=is_template,
107 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000108 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000109
110
jadmanski0afbb632008-06-06 21:10:57 +0000111 class Meta:
112 db_table = 'labels'
mblighe8819cd2008-02-15 16:48:40 +0000113
jadmanski0afbb632008-06-06 21:10:57 +0000114 class Admin:
115 list_display = ('name', 'kernel_config')
116 # see Host.Admin
117 manager = model_logic.ValidObjectsManager()
mblighe8819cd2008-02-15 16:48:40 +0000118
jadmanski0afbb632008-06-06 21:10:57 +0000119 def __str__(self):
120 return self.name
mblighe8819cd2008-02-15 16:48:40 +0000121
122
showardfb2a7fa2008-07-17 17:04:12 +0000123class User(dbmodels.Model, model_logic.ModelExtensions):
124 """\
125 Required:
126 login :user login name
127
128 Optional:
129 access_level: 0=User (default), 1=Admin, 100=Root
130 """
131 ACCESS_ROOT = 100
132 ACCESS_ADMIN = 1
133 ACCESS_USER = 0
134
135 login = dbmodels.CharField(maxlength=255, unique=True)
136 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
137
showard0fc38302008-10-23 00:44:07 +0000138 # user preferences
139 reboot_before = dbmodels.SmallIntegerField(choices=RebootBefore.choices(),
140 blank=True,
141 default=DEFAULT_REBOOT_BEFORE)
142 reboot_after = dbmodels.SmallIntegerField(choices=RebootAfter.choices(),
143 blank=True,
144 default=DEFAULT_REBOOT_AFTER)
showard97db5ba2008-11-12 18:18:02 +0000145 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000146
showardfb2a7fa2008-07-17 17:04:12 +0000147 name_field = 'login'
148 objects = model_logic.ExtendedManager()
149
150
151 def save(self):
152 # is this a new object being saved for the first time?
153 first_time = (self.id is None)
154 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000155 if user and not user.is_superuser() and user.login != self.login:
156 raise AclAccessViolation("You cannot modify user " + self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000157 super(User, self).save()
158 if first_time:
159 everyone = AclGroup.objects.get(name='Everyone')
160 everyone.users.add(self)
161
162
163 def is_superuser(self):
164 return self.access_level >= self.ACCESS_ROOT
165
166
167 class Meta:
168 db_table = 'users'
169
170 class Admin:
171 list_display = ('login', 'access_level')
172 search_fields = ('login',)
173
174 def __str__(self):
175 return self.login
176
177
showardf8b19042009-05-12 17:22:49 +0000178class Host(model_logic.ModelWithInvalid, dbmodels.Model,
179 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000180 """\
181 Required:
182 hostname
mblighe8819cd2008-02-15 16:48:40 +0000183
jadmanski0afbb632008-06-06 21:10:57 +0000184 optional:
showard21baa452008-10-21 00:08:39 +0000185 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000186
jadmanski0afbb632008-06-06 21:10:57 +0000187 Internal:
188 synch_id: currently unused
189 status: string describing status of host
showard21baa452008-10-21 00:08:39 +0000190 invalid: true if the host has been deleted
191 protection: indicates what can be done to this host during repair
192 locked_by: user that locked the host, or null if the host is unlocked
193 lock_time: DateTime at which the host was locked
194 dirty: true if the host has been used without being rebooted
jadmanski0afbb632008-06-06 21:10:57 +0000195 """
196 Status = enum.Enum('Verifying', 'Running', 'Ready', 'Repairing',
showard45ae8192008-11-05 19:32:53 +0000197 'Repair Failed', 'Dead', 'Cleaning', 'Pending',
showard6d7b2ff2009-06-10 00:16:47 +0000198 string_values=True)
mblighe8819cd2008-02-15 16:48:40 +0000199
jadmanski0afbb632008-06-06 21:10:57 +0000200 hostname = dbmodels.CharField(maxlength=255, unique=True)
201 labels = dbmodels.ManyToManyField(Label, blank=True,
202 filter_interface=dbmodels.HORIZONTAL)
203 locked = dbmodels.BooleanField(default=False)
204 synch_id = dbmodels.IntegerField(blank=True, null=True,
205 editable=settings.FULL_ADMIN)
206 status = dbmodels.CharField(maxlength=255, default=Status.READY,
207 choices=Status.choices(),
208 editable=settings.FULL_ADMIN)
209 invalid = dbmodels.BooleanField(default=False,
210 editable=settings.FULL_ADMIN)
showardd5afc2f2008-08-12 17:19:44 +0000211 protection = dbmodels.SmallIntegerField(null=False, blank=True,
showarddf062562008-07-03 19:56:37 +0000212 choices=host_protections.choices,
213 default=host_protections.default)
showardfb2a7fa2008-07-17 17:04:12 +0000214 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
215 lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False)
showard21baa452008-10-21 00:08:39 +0000216 dirty = dbmodels.BooleanField(default=True, editable=settings.FULL_ADMIN)
mblighe8819cd2008-02-15 16:48:40 +0000217
jadmanski0afbb632008-06-06 21:10:57 +0000218 name_field = 'hostname'
219 objects = model_logic.ExtendedManager()
220 valid_objects = model_logic.ValidObjectsManager()
mbligh5244cbb2008-04-24 20:39:52 +0000221
showard2bab8f42008-11-12 18:15:22 +0000222
223 def __init__(self, *args, **kwargs):
224 super(Host, self).__init__(*args, **kwargs)
225 self._record_attributes(['status'])
226
227
showardb8471e32008-07-03 19:51:08 +0000228 @staticmethod
229 def create_one_time_host(hostname):
230 query = Host.objects.filter(hostname=hostname)
231 if query.count() == 0:
232 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000233 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000234 else:
235 host = query[0]
236 if not host.invalid:
237 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000238 'hostname' : '%s already exists in the autotest DB. '
239 'Select it rather than entering it as a one time '
240 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000241 })
242 host.clean_object()
showarde65d2af2008-07-30 23:34:21 +0000243 AclGroup.objects.get(name='Everyone').hosts.add(host)
showardb8471e32008-07-03 19:51:08 +0000244 host.status = Host.Status.READY
showard1ab512b2008-07-30 23:39:04 +0000245 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000246 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000247 host.save()
248 return host
mbligh5244cbb2008-04-24 20:39:52 +0000249
showard1ff7b2e2009-05-15 23:17:18 +0000250
jadmanski0afbb632008-06-06 21:10:57 +0000251 def clean_object(self):
252 self.aclgroup_set.clear()
253 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000254
255
jadmanski0afbb632008-06-06 21:10:57 +0000256 def save(self):
257 # extra spaces in the hostname can be a sneaky source of errors
258 self.hostname = self.hostname.strip()
259 # is this a new object being saved for the first time?
260 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000261 if not first_time:
262 AclGroup.check_for_acl_violation_hosts([self])
showardfb2a7fa2008-07-17 17:04:12 +0000263 if self.locked and not self.locked_by:
264 self.locked_by = thread_local.get_user()
265 self.lock_time = datetime.now()
showard21baa452008-10-21 00:08:39 +0000266 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000267 elif not self.locked and self.locked_by:
268 self.locked_by = None
269 self.lock_time = None
jadmanski0afbb632008-06-06 21:10:57 +0000270 super(Host, self).save()
271 if first_time:
272 everyone = AclGroup.objects.get(name='Everyone')
273 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000274 self._check_for_updated_attributes()
275
mblighe8819cd2008-02-15 16:48:40 +0000276
showardb8471e32008-07-03 19:51:08 +0000277 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000278 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000279 for queue_entry in self.hostqueueentry_set.all():
280 queue_entry.deleted = True
showard9a1f2e12008-10-02 11:14:29 +0000281 queue_entry.abort(thread_local.get_user())
showardb8471e32008-07-03 19:51:08 +0000282 super(Host, self).delete()
283
mblighe8819cd2008-02-15 16:48:40 +0000284
showard2bab8f42008-11-12 18:15:22 +0000285 def on_attribute_changed(self, attribute, old_value):
286 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000287 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000288
289
showard29f7cd22009-04-29 21:16:24 +0000290 def enqueue_job(self, job, atomic_group=None, is_template=False):
showardc92da832009-04-07 18:14:34 +0000291 """Enqueue a job on this host."""
showard29f7cd22009-04-29 21:16:24 +0000292 queue_entry = HostQueueEntry.create(host=self, job=job,
293 is_template=is_template,
294 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000295 # allow recovery of dead hosts from the frontend
296 if not self.active_queue_entry() and self.is_dead():
297 self.status = Host.Status.READY
298 self.save()
299 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000300
showard08f981b2008-06-24 21:59:03 +0000301 block = IneligibleHostQueue(job=job, host=self)
302 block.save()
303
mblighe8819cd2008-02-15 16:48:40 +0000304
jadmanski0afbb632008-06-06 21:10:57 +0000305 def platform(self):
306 # TODO(showard): slighly hacky?
307 platforms = self.labels.filter(platform=True)
308 if len(platforms) == 0:
309 return None
310 return platforms[0]
311 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000312
313
showardcafd16e2009-05-29 18:37:49 +0000314 @classmethod
315 def check_no_platform(cls, hosts):
316 Host.objects.populate_relationships(hosts, Label, 'label_list')
317 errors = []
318 for host in hosts:
319 platforms = [label.name for label in host.label_list
320 if label.platform]
321 if platforms:
322 # do a join, just in case this host has multiple platforms,
323 # we'll be able to see it
324 errors.append('Host %s already has a platform: %s' % (
325 host.hostname, ', '.join(platforms)))
326 if errors:
327 raise model_logic.ValidationError({'labels': '; '.join(errors)})
328
329
jadmanski0afbb632008-06-06 21:10:57 +0000330 def is_dead(self):
331 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000332
333
jadmanski0afbb632008-06-06 21:10:57 +0000334 def active_queue_entry(self):
335 active = list(self.hostqueueentry_set.filter(active=True))
336 if not active:
337 return None
338 assert len(active) == 1, ('More than one active entry for '
339 'host ' + self.hostname)
340 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000341
342
showardf8b19042009-05-12 17:22:49 +0000343 def _get_attribute_model_and_args(self, attribute):
344 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000345
346
jadmanski0afbb632008-06-06 21:10:57 +0000347 class Meta:
348 db_table = 'hosts'
mblighe8819cd2008-02-15 16:48:40 +0000349
jadmanski0afbb632008-06-06 21:10:57 +0000350 class Admin:
351 # TODO(showard) - showing platform requires a SQL query for
352 # each row (since labels are many-to-many) - should we remove
353 # it?
354 list_display = ('hostname', 'platform', 'locked', 'status')
showarddf062562008-07-03 19:56:37 +0000355 list_filter = ('labels', 'locked', 'protection')
jadmanski0afbb632008-06-06 21:10:57 +0000356 search_fields = ('hostname', 'status')
357 # undocumented Django feature - if you set manager here, the
358 # admin code will use it, otherwise it'll use a default Manager
359 manager = model_logic.ValidObjectsManager()
mblighe8819cd2008-02-15 16:48:40 +0000360
jadmanski0afbb632008-06-06 21:10:57 +0000361 def __str__(self):
362 return self.hostname
mblighe8819cd2008-02-15 16:48:40 +0000363
364
showard0957a842009-05-11 19:25:08 +0000365class HostAttribute(dbmodels.Model):
366 """Arbitrary keyvals associated with hosts."""
367 host = dbmodels.ForeignKey(Host)
368 attribute = dbmodels.CharField(maxlength=90)
369 value = dbmodels.CharField(maxlength=300)
370
371 objects = model_logic.ExtendedManager()
372
373 class Meta:
374 db_table = 'host_attributes'
375
376
showard7c785282008-05-29 19:45:12 +0000377class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000378 """\
379 Required:
showard909c7a62008-07-15 21:52:38 +0000380 author: author name
381 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000382 name: test name
showard909c7a62008-07-15 21:52:38 +0000383 time: short, medium, long
384 test_class: This describes the class for your the test belongs in.
385 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000386 test_type: Client or Server
387 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000388 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
389 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000390 i.e. if sync_count = 2 it is a sync job that requires two
391 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000392 Optional:
showard909c7a62008-07-15 21:52:38 +0000393 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000394 dependency_labels: many-to-many relationship with labels corresponding to
395 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000396 experimental: If this is set to True production servers will ignore the test
397 run_verify: Whether or not the scheduler should run the verify stage
jadmanski0afbb632008-06-06 21:10:57 +0000398 """
showard909c7a62008-07-15 21:52:38 +0000399 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
jadmanski0afbb632008-06-06 21:10:57 +0000400 # TODO(showard) - this should be merged with Job.ControlType (but right
401 # now they use opposite values)
402 Types = enum.Enum('Client', 'Server', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000403
jadmanski0afbb632008-06-06 21:10:57 +0000404 name = dbmodels.CharField(maxlength=255, unique=True)
showard909c7a62008-07-15 21:52:38 +0000405 author = dbmodels.CharField(maxlength=255)
406 test_class = dbmodels.CharField(maxlength=255)
407 test_category = dbmodels.CharField(maxlength=255)
408 dependencies = dbmodels.CharField(maxlength=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000409 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000410 experimental = dbmodels.BooleanField(default=True)
411 run_verify = dbmodels.BooleanField(default=True)
412 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
413 default=TestTime.MEDIUM)
jadmanski0afbb632008-06-06 21:10:57 +0000414 test_type = dbmodels.SmallIntegerField(choices=Types.choices())
showard909c7a62008-07-15 21:52:38 +0000415 sync_count = dbmodels.IntegerField(default=1)
showard909c7a62008-07-15 21:52:38 +0000416 path = dbmodels.CharField(maxlength=255, unique=True)
showard989f25d2008-10-01 11:38:11 +0000417 dependency_labels = dbmodels.ManyToManyField(
418 Label, blank=True, filter_interface=dbmodels.HORIZONTAL)
mblighe8819cd2008-02-15 16:48:40 +0000419
jadmanski0afbb632008-06-06 21:10:57 +0000420 name_field = 'name'
421 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000422
423
jadmanski0afbb632008-06-06 21:10:57 +0000424 class Meta:
425 db_table = 'autotests'
mblighe8819cd2008-02-15 16:48:40 +0000426
jadmanski0afbb632008-06-06 21:10:57 +0000427 class Admin:
428 fields = (
429 (None, {'fields' :
showard909c7a62008-07-15 21:52:38 +0000430 ('name', 'author', 'test_category', 'test_class',
showard2bab8f42008-11-12 18:15:22 +0000431 'test_time', 'sync_count', 'test_type', 'sync_count',
showard909c7a62008-07-15 21:52:38 +0000432 'path', 'dependencies', 'experimental', 'run_verify',
433 'description')}),
jadmanski0afbb632008-06-06 21:10:57 +0000434 )
showard2bab8f42008-11-12 18:15:22 +0000435 list_display = ('name', 'test_type', 'description', 'sync_count')
jadmanski0afbb632008-06-06 21:10:57 +0000436 search_fields = ('name',)
mblighe8819cd2008-02-15 16:48:40 +0000437
jadmanski0afbb632008-06-06 21:10:57 +0000438 def __str__(self):
439 return self.name
mblighe8819cd2008-02-15 16:48:40 +0000440
441
showard2b9a88b2008-06-13 20:55:03 +0000442class Profiler(dbmodels.Model, model_logic.ModelExtensions):
443 """\
444 Required:
445 name: profiler name
446 test_type: Client or Server
447
448 Optional:
449 description: arbirary text description
450 """
451 name = dbmodels.CharField(maxlength=255, unique=True)
452 description = dbmodels.TextField(blank=True)
453
454 name_field = 'name'
455 objects = model_logic.ExtendedManager()
456
457
458 class Meta:
459 db_table = 'profilers'
460
461 class Admin:
462 list_display = ('name', 'description')
463 search_fields = ('name',)
464
465 def __str__(self):
466 return self.name
467
468
showard7c785282008-05-29 19:45:12 +0000469class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000470 """\
471 Required:
472 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000473
jadmanski0afbb632008-06-06 21:10:57 +0000474 Optional:
475 description: arbitrary description of group
476 """
477 name = dbmodels.CharField(maxlength=255, unique=True)
478 description = dbmodels.CharField(maxlength=255, blank=True)
showard04f2cd82008-07-25 20:53:31 +0000479 users = dbmodels.ManyToManyField(User, blank=True,
jadmanski0afbb632008-06-06 21:10:57 +0000480 filter_interface=dbmodels.HORIZONTAL)
481 hosts = dbmodels.ManyToManyField(Host,
482 filter_interface=dbmodels.HORIZONTAL)
mblighe8819cd2008-02-15 16:48:40 +0000483
jadmanski0afbb632008-06-06 21:10:57 +0000484 name_field = 'name'
485 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000486
showard08f981b2008-06-24 21:59:03 +0000487 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000488 def check_for_acl_violation_hosts(hosts):
489 user = thread_local.get_user()
490 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000491 return
showard3dd47c22008-07-10 00:41:36 +0000492 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000493 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000494 for host in hosts:
495 # Check if the user has access to this host,
496 # but only if it is not a metahost
497 if (isinstance(host, Host)
498 and int(host.id) not in accessible_host_ids):
499 raise AclAccessViolation("You do not have access to %s"
500 % str(host))
501
showard9dbdcda2008-10-14 17:34:36 +0000502
503 @staticmethod
showarddc817512008-11-12 18:16:41 +0000504 def check_abort_permissions(queue_entries):
505 """
506 look for queue entries that aren't abortable, meaning
507 * the job isn't owned by this user, and
508 * the machine isn't ACL-accessible, or
509 * the machine is in the "Everyone" ACL
510 """
showard9dbdcda2008-10-14 17:34:36 +0000511 user = thread_local.get_user()
512 if user.is_superuser():
513 return
showarddc817512008-11-12 18:16:41 +0000514 not_owned = queue_entries.exclude(job__owner=user.login)
515 # I do this using ID sets instead of just Django filters because
516 # filtering on M2M fields is broken in Django 0.96. It's better in 1.0.
517 accessible_ids = set(
518 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000519 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000520 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000521 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000522 cannot_abort = [entry for entry in not_owned.select_related()
523 if entry.id not in accessible_ids
524 or entry.id in public_ids]
525 if len(cannot_abort) == 0:
526 return
527 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000528 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000529 for entry in cannot_abort)
530 raise AclAccessViolation('You cannot abort the following job entries: '
531 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000532
533
showard3dd47c22008-07-10 00:41:36 +0000534 def check_for_acl_violation_acl_group(self):
535 user = thread_local.get_user()
536 if user.is_superuser():
537 return None
538 if not user in self.users.all():
539 raise AclAccessViolation("You do not have access to %s"
540 % self.name)
541
542 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000543 def on_host_membership_change():
544 everyone = AclGroup.objects.get(name='Everyone')
545
showard3dd47c22008-07-10 00:41:36 +0000546 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000547 # TODO(showard): this is a bit of a hack, since the fact that this query
548 # works is kind of a coincidence of Django internals. This trick
549 # doesn't work in general (on all foreign key relationships). I'll
550 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000551 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000552 everyone.hosts.add(*orphaned_hosts.distinct())
553
554 # find hosts in both Everyone and another ACL group, and remove them
555 # from Everyone
556 hosts_in_everyone = Host.valid_objects.filter_custom_join(
showardd9ac4452009-02-07 02:04:37 +0000557 '_everyone', aclgroup__name='Everyone')
558 acled_hosts = hosts_in_everyone.exclude(aclgroup__name='Everyone')
showard08f981b2008-06-24 21:59:03 +0000559 everyone.hosts.remove(*acled_hosts.distinct())
560
561
562 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000563 if (self.name == 'Everyone'):
564 raise AclAccessViolation("You cannot delete 'Everyone'!")
565 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000566 super(AclGroup, self).delete()
567 self.on_host_membership_change()
568
569
showard04f2cd82008-07-25 20:53:31 +0000570 def add_current_user_if_empty(self):
571 if not self.users.count():
572 self.users.add(thread_local.get_user())
573
574
showard08f981b2008-06-24 21:59:03 +0000575 # if you have a model attribute called "Manipulator", Django will
576 # automatically insert it into the beginning of the superclass list
577 # for the model's manipulators
578 class Manipulator(object):
579 """
580 Custom manipulator to get notification when ACLs are changed through
581 the admin interface.
582 """
583 def save(self, new_data):
showard3dd47c22008-07-10 00:41:36 +0000584 user = thread_local.get_user()
showard31c570e2008-07-23 19:29:17 +0000585 if hasattr(self, 'original_object'):
586 if (not user.is_superuser()
587 and self.original_object.name == 'Everyone'):
588 raise AclAccessViolation("You cannot modify 'Everyone'!")
589 self.original_object.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000590 obj = super(AclGroup.Manipulator, self).save(new_data)
showard04f2cd82008-07-25 20:53:31 +0000591 if not hasattr(self, 'original_object'):
592 obj.users.add(thread_local.get_user())
593 obj.add_current_user_if_empty()
showard08f981b2008-06-24 21:59:03 +0000594 obj.on_host_membership_change()
595 return obj
showardeb3be4d2008-04-21 20:59:26 +0000596
jadmanski0afbb632008-06-06 21:10:57 +0000597 class Meta:
598 db_table = 'acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000599
jadmanski0afbb632008-06-06 21:10:57 +0000600 class Admin:
601 list_display = ('name', 'description')
602 search_fields = ('name',)
mblighe8819cd2008-02-15 16:48:40 +0000603
jadmanski0afbb632008-06-06 21:10:57 +0000604 def __str__(self):
605 return self.name
mblighe8819cd2008-02-15 16:48:40 +0000606
mblighe8819cd2008-02-15 16:48:40 +0000607
showard7c785282008-05-29 19:45:12 +0000608class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +0000609 'Custom manager to provide efficient status counts querying.'
610 def get_status_counts(self, job_ids):
611 """\
612 Returns a dictionary mapping the given job IDs to their status
613 count dictionaries.
614 """
615 if not job_ids:
616 return {}
617 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
618 cursor = connection.cursor()
619 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +0000620 SELECT job_id, status, aborted, complete, COUNT(*)
jadmanski0afbb632008-06-06 21:10:57 +0000621 FROM host_queue_entries
622 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +0000623 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +0000624 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +0000625 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +0000626 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +0000627 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +0000628 full_status = HostQueueEntry.compute_full_status(status, aborted,
629 complete)
showardb6d16622009-05-26 19:35:29 +0000630 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +0000631 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +0000632 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +0000633
634
showard7c785282008-05-29 19:45:12 +0000635class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000636 """\
637 owner: username of job owner
638 name: job name (does not have to be unique)
639 priority: Low, Medium, High, Urgent (or 0-3)
640 control_file: contents of control file
641 control_type: Client or Server
642 created_on: date of job creation
643 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +0000644 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +0000645 run_verify: Whether or not to run the verify phase
showard12f3e322009-05-13 21:27:42 +0000646 timeout: hours from queuing time until job times out
647 max_runtime_hrs: hours from job starting time until job times out
showard542e8402008-09-19 20:16:18 +0000648 email_list: list of people to email on completion delimited by any of:
649 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +0000650 dependency_labels: many-to-many relationship with labels corresponding to
651 job dependencies
showard21baa452008-10-21 00:08:39 +0000652 reboot_before: Never, If dirty, or Always
653 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +0000654 parse_failed_repair: if True, a failed repair launched by this job will have
655 its results parsed as part of the job.
jadmanski0afbb632008-06-06 21:10:57 +0000656 """
showardb1e51872008-10-07 11:08:18 +0000657 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
658 'AUTOTEST_WEB', 'job_timeout_default', default=240)
showard12f3e322009-05-13 21:27:42 +0000659 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
660 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
showarda1e74b32009-05-12 17:32:04 +0000661 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
662 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
663 default=False)
showardb1e51872008-10-07 11:08:18 +0000664
jadmanski0afbb632008-06-06 21:10:57 +0000665 Priority = enum.Enum('Low', 'Medium', 'High', 'Urgent')
666 ControlType = enum.Enum('Server', 'Client', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000667
jadmanski0afbb632008-06-06 21:10:57 +0000668 owner = dbmodels.CharField(maxlength=255)
669 name = dbmodels.CharField(maxlength=255)
670 priority = dbmodels.SmallIntegerField(choices=Priority.choices(),
671 blank=True, # to allow 0
672 default=Priority.MEDIUM)
673 control_file = dbmodels.TextField()
674 control_type = dbmodels.SmallIntegerField(choices=ControlType.choices(),
showardb1e51872008-10-07 11:08:18 +0000675 blank=True, # to allow 0
676 default=ControlType.CLIENT)
showard68c7aa02008-10-09 16:49:11 +0000677 created_on = dbmodels.DateTimeField()
showard2bab8f42008-11-12 18:15:22 +0000678 synch_count = dbmodels.IntegerField(null=True, default=1)
showardb1e51872008-10-07 11:08:18 +0000679 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
showard9976ce92008-10-15 20:28:13 +0000680 run_verify = dbmodels.BooleanField(default=True)
showard542e8402008-09-19 20:16:18 +0000681 email_list = dbmodels.CharField(maxlength=250, blank=True)
showard989f25d2008-10-01 11:38:11 +0000682 dependency_labels = dbmodels.ManyToManyField(
683 Label, blank=True, filter_interface=dbmodels.HORIZONTAL)
showard21baa452008-10-21 00:08:39 +0000684 reboot_before = dbmodels.SmallIntegerField(choices=RebootBefore.choices(),
685 blank=True,
showard0fc38302008-10-23 00:44:07 +0000686 default=DEFAULT_REBOOT_BEFORE)
showard21baa452008-10-21 00:08:39 +0000687 reboot_after = dbmodels.SmallIntegerField(choices=RebootAfter.choices(),
688 blank=True,
showard0fc38302008-10-23 00:44:07 +0000689 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +0000690 parse_failed_repair = dbmodels.BooleanField(
691 default=DEFAULT_PARSE_FAILED_REPAIR)
showard12f3e322009-05-13 21:27:42 +0000692 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
mblighe8819cd2008-02-15 16:48:40 +0000693
694
jadmanski0afbb632008-06-06 21:10:57 +0000695 # custom manager
696 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +0000697
698
jadmanski0afbb632008-06-06 21:10:57 +0000699 def is_server_job(self):
700 return self.control_type == self.ControlType.SERVER
mblighe8819cd2008-02-15 16:48:40 +0000701
702
jadmanski0afbb632008-06-06 21:10:57 +0000703 @classmethod
showarda1e74b32009-05-12 17:32:04 +0000704 def create(cls, owner, options, hosts):
jadmanski0afbb632008-06-06 21:10:57 +0000705 """\
706 Creates a job by taking some information (the listed args)
707 and filling in the rest of the necessary information.
708 """
showard3dd47c22008-07-10 00:41:36 +0000709 AclGroup.check_for_acl_violation_hosts(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000710 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +0000711 owner=owner,
712 name=options['name'],
713 priority=options['priority'],
714 control_file=options['control_file'],
715 control_type=options['control_type'],
716 synch_count=options.get('synch_count'),
717 timeout=options.get('timeout'),
showard12f3e322009-05-13 21:27:42 +0000718 max_runtime_hrs=options.get('max_runtime_hrs'),
showarda1e74b32009-05-12 17:32:04 +0000719 run_verify=options.get('run_verify'),
720 email_list=options.get('email_list'),
721 reboot_before=options.get('reboot_before'),
722 reboot_after=options.get('reboot_after'),
723 parse_failed_repair=options.get('parse_failed_repair'),
showard68c7aa02008-10-09 16:49:11 +0000724 created_on=datetime.now())
mblighe8819cd2008-02-15 16:48:40 +0000725
showarda1e74b32009-05-12 17:32:04 +0000726 job.dependency_labels = options['dependencies']
jadmanski0afbb632008-06-06 21:10:57 +0000727 return job
mblighe8819cd2008-02-15 16:48:40 +0000728
729
showard29f7cd22009-04-29 21:16:24 +0000730 def queue(self, hosts, atomic_group=None, is_template=False):
showardc92da832009-04-07 18:14:34 +0000731 """Enqueue a job on the given hosts."""
732 if atomic_group and not hosts:
733 # No hosts or labels are required to queue an atomic group
734 # Job. However, if they are given, we respect them below.
showard29f7cd22009-04-29 21:16:24 +0000735 atomic_group.enqueue_job(self, is_template=is_template)
jadmanski0afbb632008-06-06 21:10:57 +0000736 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +0000737 host.enqueue_job(self, atomic_group=atomic_group,
738 is_template=is_template)
739
740
741 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
742 rec = RecurringRun(job=self, start_date=start_date,
743 loop_period=loop_period,
744 loop_count=loop_count,
745 owner=User.objects.get(login=owner))
746 rec.save()
747 return rec.id
mblighe8819cd2008-02-15 16:48:40 +0000748
749
jadmanski0afbb632008-06-06 21:10:57 +0000750 def user(self):
751 try:
752 return User.objects.get(login=self.owner)
753 except self.DoesNotExist:
754 return None
mblighe8819cd2008-02-15 16:48:40 +0000755
756
showard98863972008-10-29 21:14:56 +0000757 def abort(self, aborted_by):
758 for queue_entry in self.hostqueueentry_set.all():
759 queue_entry.abort(aborted_by)
760
761
jadmanski0afbb632008-06-06 21:10:57 +0000762 class Meta:
763 db_table = 'jobs'
mblighe8819cd2008-02-15 16:48:40 +0000764
jadmanski0afbb632008-06-06 21:10:57 +0000765 if settings.FULL_ADMIN:
766 class Admin:
767 list_display = ('id', 'owner', 'name', 'control_type')
mblighe8819cd2008-02-15 16:48:40 +0000768
jadmanski0afbb632008-06-06 21:10:57 +0000769 def __str__(self):
770 return '%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +0000771
772
showard7c785282008-05-29 19:45:12 +0000773class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000774 job = dbmodels.ForeignKey(Job)
775 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +0000776
jadmanski0afbb632008-06-06 21:10:57 +0000777 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000778
jadmanski0afbb632008-06-06 21:10:57 +0000779 class Meta:
780 db_table = 'ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +0000781
jadmanski0afbb632008-06-06 21:10:57 +0000782 if settings.FULL_ADMIN:
783 class Admin:
784 list_display = ('id', 'job', 'host')
mblighe8819cd2008-02-15 16:48:40 +0000785
786
showard7c785282008-05-29 19:45:12 +0000787class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
showarda3ab0d52008-11-03 19:03:47 +0000788 Status = enum.Enum('Queued', 'Starting', 'Verifying', 'Pending', 'Running',
showardd3dc1992009-04-22 21:01:40 +0000789 'Gathering', 'Parsing', 'Aborted', 'Completed',
showard29f7cd22009-04-29 21:16:24 +0000790 'Failed', 'Stopped', 'Template', string_values=True)
showard2bab8f42008-11-12 18:15:22 +0000791 ACTIVE_STATUSES = (Status.STARTING, Status.VERIFYING, Status.PENDING,
showardd3dc1992009-04-22 21:01:40 +0000792 Status.RUNNING, Status.GATHERING)
showardc85c21b2008-11-24 22:17:37 +0000793 COMPLETE_STATUSES = (Status.ABORTED, Status.COMPLETED, Status.FAILED,
showard29f7cd22009-04-29 21:16:24 +0000794 Status.STOPPED, Status.TEMPLATE)
showarda3ab0d52008-11-03 19:03:47 +0000795
jadmanski0afbb632008-06-06 21:10:57 +0000796 job = dbmodels.ForeignKey(Job)
797 host = dbmodels.ForeignKey(Host, blank=True, null=True)
jadmanski0afbb632008-06-06 21:10:57 +0000798 status = dbmodels.CharField(maxlength=255)
799 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
800 db_column='meta_host')
801 active = dbmodels.BooleanField(default=False)
802 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +0000803 deleted = dbmodels.BooleanField(default=False)
showard2bab8f42008-11-12 18:15:22 +0000804 execution_subdir = dbmodels.CharField(maxlength=255, blank=True, default='')
showard89f84db2009-03-12 20:39:13 +0000805 # If atomic_group is set, this is a virtual HostQueueEntry that will
806 # be expanded into many actual hosts within the group at schedule time.
807 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +0000808 aborted = dbmodels.BooleanField(default=False)
showard12f3e322009-05-13 21:27:42 +0000809 started_on = dbmodels.DateTimeField(null=True)
mblighe8819cd2008-02-15 16:48:40 +0000810
jadmanski0afbb632008-06-06 21:10:57 +0000811 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000812
mblighe8819cd2008-02-15 16:48:40 +0000813
showard2bab8f42008-11-12 18:15:22 +0000814 def __init__(self, *args, **kwargs):
815 super(HostQueueEntry, self).__init__(*args, **kwargs)
816 self._record_attributes(['status'])
817
818
showard29f7cd22009-04-29 21:16:24 +0000819 @classmethod
820 def create(cls, job, host=None, meta_host=None, atomic_group=None,
821 is_template=False):
822 if is_template:
823 status = cls.Status.TEMPLATE
824 else:
825 status = cls.Status.QUEUED
826
827 return cls(job=job, host=host, meta_host=meta_host,
828 atomic_group=atomic_group, status=status)
829
830
showard2bab8f42008-11-12 18:15:22 +0000831 def save(self):
832 self._set_active_and_complete()
833 super(HostQueueEntry, self).save()
834 self._check_for_updated_attributes()
835
836
showard3f15eed2008-11-14 22:40:48 +0000837 def host_or_metahost_name(self):
838 if self.host:
839 return self.host.hostname
840 else:
841 assert self.meta_host
842 return self.meta_host.name
843
844
showard2bab8f42008-11-12 18:15:22 +0000845 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +0000846 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +0000847 self.active, self.complete = True, False
848 elif self.status in self.COMPLETE_STATUSES:
849 self.active, self.complete = False, True
850 else:
851 self.active, self.complete = False, False
852
853
854 def on_attribute_changed(self, attribute, old_value):
855 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000856 logging.info('%s/%d (%d) -> %s' % (self.host, self.job.id, self.id,
857 self.status))
showard2bab8f42008-11-12 18:15:22 +0000858
859
jadmanski0afbb632008-06-06 21:10:57 +0000860 def is_meta_host_entry(self):
861 'True if this is a entry has a meta_host instead of a host.'
862 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +0000863
showarda3ab0d52008-11-03 19:03:47 +0000864
showard4c119042008-09-29 19:16:18 +0000865 def log_abort(self, user):
showard98863972008-10-29 21:14:56 +0000866 if user is None:
867 # automatic system abort (i.e. job timeout)
868 return
showard4c119042008-09-29 19:16:18 +0000869 abort_log = AbortedHostQueueEntry(queue_entry=self, aborted_by=user)
870 abort_log.save()
871
showarda3ab0d52008-11-03 19:03:47 +0000872
showard4c119042008-09-29 19:16:18 +0000873 def abort(self, user):
showarda3ab0d52008-11-03 19:03:47 +0000874 # this isn't completely immune to race conditions since it's not atomic,
875 # but it should be safe given the scheduler's behavior.
showardd3dc1992009-04-22 21:01:40 +0000876 if not self.complete and not self.aborted:
showard4c119042008-09-29 19:16:18 +0000877 self.log_abort(user)
showardd3dc1992009-04-22 21:01:40 +0000878 self.aborted = True
showarda3ab0d52008-11-03 19:03:47 +0000879 self.save()
mblighe8819cd2008-02-15 16:48:40 +0000880
showardd3dc1992009-04-22 21:01:40 +0000881
882 @classmethod
883 def compute_full_status(cls, status, aborted, complete):
884 if aborted and not complete:
885 return 'Aborted (%s)' % status
886 return status
887
888
889 def full_status(self):
890 return self.compute_full_status(self.status, self.aborted,
891 self.complete)
892
893
894 def _postprocess_object_dict(self, object_dict):
895 object_dict['full_status'] = self.full_status()
896
897
jadmanski0afbb632008-06-06 21:10:57 +0000898 class Meta:
899 db_table = 'host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +0000900
showard12f3e322009-05-13 21:27:42 +0000901
jadmanski0afbb632008-06-06 21:10:57 +0000902 if settings.FULL_ADMIN:
903 class Admin:
904 list_display = ('id', 'job', 'host', 'status',
905 'meta_host')
showard4c119042008-09-29 19:16:18 +0000906
907
showard12f3e322009-05-13 21:27:42 +0000908 def __str__(self):
909 hostname = None
910 if self.host:
911 hostname = self.host.hostname
912 return "%s/%d (%d)" % (hostname, self.job.id, self.id)
913
914
showard4c119042008-09-29 19:16:18 +0000915class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
916 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
917 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +0000918 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +0000919
920 objects = model_logic.ExtendedManager()
921
showard68c7aa02008-10-09 16:49:11 +0000922
923 def save(self):
924 self.aborted_on = datetime.now()
925 super(AbortedHostQueueEntry, self).save()
926
showard4c119042008-09-29 19:16:18 +0000927 class Meta:
928 db_table = 'aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +0000929
930
931class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
932 """\
933 job: job to use as a template
934 owner: owner of the instantiated template
935 start_date: Run the job at scheduled date
936 loop_period: Re-run (loop) the job periodically
937 (in every loop_period seconds)
938 loop_count: Re-run (loop) count
939 """
940
941 job = dbmodels.ForeignKey(Job)
942 owner = dbmodels.ForeignKey(User)
943 start_date = dbmodels.DateTimeField()
944 loop_period = dbmodels.IntegerField(blank=True)
945 loop_count = dbmodels.IntegerField(blank=True)
946
947 objects = model_logic.ExtendedManager()
948
949 class Meta:
950 db_table = 'recurring_run'
951
952 def __str__(self):
953 return 'RecurringRun(job %s, start %s, period %s, count %s)' % (
954 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +0000955
956
957class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
958 """\
959 Tasks to run on hosts at the next time they are in the Ready state. Use this
960 for high-priority tasks, such as forced repair or forced reinstall.
961
962 host: host to run this task on
963 task: special task to run (currently only Reverify)
964 time_requested: date and time the request for this task was made
965 is_active: task is currently running
966 is_complete: task has finished running
967 """
968 Task = enum.Enum('Reverify', string_values=True)
969
970 host = dbmodels.ForeignKey(Host, blank=False, null=False)
971 task = dbmodels.CharField(maxlength=64, choices=Task.choices(),
972 blank=False, null=False)
973 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
974 null=False)
975 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
976 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
977
978 objects = model_logic.ExtendedManager()
979
980
981 @classmethod
982 def schedule_special_task(cls, hosts, task):
983 """\
984 Schedules hosts for a special task
985 """
986 for host in hosts:
987 special_task = SpecialTask(host=host, task=task)
988 special_task.save()
989
990
991 class Meta:
992 db_table = 'special_tasks'
993
994 def __str__(self):
995 result = 'Special Task (host %s, task %s, time %s)' % (
996 self.host, self.task, self.time_requested)
997 if self.is_complete:
998 result += ' (completed)'
999 elif self.is_active:
1000 result += ' (active)'
1001
1002 return result