blob: 3b1db8cb8ed67e5d969473433c8d48f4dae1fbca [file] [log] [blame]
showardfb2a7fa2008-07-17 17:04:12 +00001from datetime import datetime
showard7c785282008-05-29 19:45:12 +00002from django.db import models as dbmodels, connection
showardcafd16e2009-05-29 18:37:49 +00003import common
4from autotest_lib.frontend.afe import model_logic
5from autotest_lib.frontend import settings, thread_local
showardb1e51872008-10-07 11:08:18 +00006from autotest_lib.client.common_lib import enum, host_protections, global_config
showard2bab8f42008-11-12 18:15:22 +00007from autotest_lib.client.common_lib import debug
8
9logger = debug.get_logger()
mblighe8819cd2008-02-15 16:48:40 +000010
showard0fc38302008-10-23 00:44:07 +000011# job options and user preferences
12RebootBefore = enum.Enum('Never', 'If dirty', 'Always')
13DEFAULT_REBOOT_BEFORE = RebootBefore.IF_DIRTY
14RebootAfter = enum.Enum('Never', 'If all tests passed', 'Always')
15DEFAULT_REBOOT_AFTER = RebootBefore.ALWAYS
mblighe8819cd2008-02-15 16:48:40 +000016
showard89f84db2009-03-12 20:39:13 +000017
mblighe8819cd2008-02-15 16:48:40 +000018class AclAccessViolation(Exception):
jadmanski0afbb632008-06-06 21:10:57 +000019 """\
20 Raised when an operation is attempted with proper permissions as
21 dictated by ACLs.
22 """
mblighe8819cd2008-02-15 16:48:40 +000023
24
showard205fd602009-03-21 00:17:35 +000025class AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model):
showard89f84db2009-03-12 20:39:13 +000026 """\
27 An atomic group defines a collection of hosts which must only be scheduled
28 all at once. Any host with a label having an atomic group will only be
29 scheduled for a job at the same time as other hosts sharing that label.
30
31 Required:
32 name: A name for this atomic group. ex: 'rack23' or 'funky_net'
33 max_number_of_machines: The maximum number of machines that will be
34 scheduled at once when scheduling jobs to this atomic group.
35 The job.synch_count is considered the minimum.
36
37 Optional:
38 description: Arbitrary text description of this group's purpose.
39 """
40 name = dbmodels.CharField(maxlength=255, unique=True)
41 description = dbmodels.TextField(blank=True)
42 max_number_of_machines = dbmodels.IntegerField(default=1)
showard205fd602009-03-21 00:17:35 +000043 invalid = dbmodels.BooleanField(default=False,
44 editable=settings.FULL_ADMIN)
showard89f84db2009-03-12 20:39:13 +000045
showard89f84db2009-03-12 20:39:13 +000046 name_field = 'name'
showard205fd602009-03-21 00:17:35 +000047 objects = model_logic.ExtendedManager()
48 valid_objects = model_logic.ValidObjectsManager()
49
50
showard29f7cd22009-04-29 21:16:24 +000051 def enqueue_job(self, job, is_template=False):
showardc92da832009-04-07 18:14:34 +000052 """Enqueue a job on an associated atomic group of hosts."""
showard29f7cd22009-04-29 21:16:24 +000053 queue_entry = HostQueueEntry.create(atomic_group=self, job=job,
54 is_template=is_template)
showardc92da832009-04-07 18:14:34 +000055 queue_entry.save()
56
57
showard205fd602009-03-21 00:17:35 +000058 def clean_object(self):
59 self.label_set.clear()
showard89f84db2009-03-12 20:39:13 +000060
61
62 class Meta:
63 db_table = 'atomic_groups'
64
65 class Admin:
66 list_display = ('name', 'description', 'max_number_of_machines')
showard205fd602009-03-21 00:17:35 +000067 # see Host.Admin
68 manager = model_logic.ValidObjectsManager()
69
70 def __str__(self):
71 return self.name
showard89f84db2009-03-12 20:39:13 +000072
73
showard7c785282008-05-29 19:45:12 +000074class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000075 """\
76 Required:
showard89f84db2009-03-12 20:39:13 +000077 name: label name
mblighe8819cd2008-02-15 16:48:40 +000078
jadmanski0afbb632008-06-06 21:10:57 +000079 Optional:
showard89f84db2009-03-12 20:39:13 +000080 kernel_config: URL/path to kernel config for jobs run on this label.
81 platform: If True, this is a platform label (defaults to False).
82 only_if_needed: If True, a Host with this label can only be used if that
83 label is requested by the job/test (either as the meta_host or
84 in the job_dependencies).
85 atomic_group: The atomic group associated with this label.
jadmanski0afbb632008-06-06 21:10:57 +000086 """
87 name = dbmodels.CharField(maxlength=255, unique=True)
88 kernel_config = dbmodels.CharField(maxlength=255, blank=True)
89 platform = dbmodels.BooleanField(default=False)
90 invalid = dbmodels.BooleanField(default=False,
91 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +000092 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +000093
jadmanski0afbb632008-06-06 21:10:57 +000094 name_field = 'name'
95 objects = model_logic.ExtendedManager()
96 valid_objects = model_logic.ValidObjectsManager()
showard89f84db2009-03-12 20:39:13 +000097 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
98
mbligh5244cbb2008-04-24 20:39:52 +000099
jadmanski0afbb632008-06-06 21:10:57 +0000100 def clean_object(self):
101 self.host_set.clear()
showard01a51672009-05-29 18:42:37 +0000102 self.test_set.clear()
mblighe8819cd2008-02-15 16:48:40 +0000103
104
showard29f7cd22009-04-29 21:16:24 +0000105 def enqueue_job(self, job, atomic_group=None, is_template=False):
showardc92da832009-04-07 18:14:34 +0000106 """Enqueue a job on any host of this label."""
showard29f7cd22009-04-29 21:16:24 +0000107 queue_entry = HostQueueEntry.create(meta_host=self, job=job,
108 is_template=is_template,
109 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000110 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000111
112
jadmanski0afbb632008-06-06 21:10:57 +0000113 class Meta:
114 db_table = 'labels'
mblighe8819cd2008-02-15 16:48:40 +0000115
jadmanski0afbb632008-06-06 21:10:57 +0000116 class Admin:
117 list_display = ('name', 'kernel_config')
118 # see Host.Admin
119 manager = model_logic.ValidObjectsManager()
mblighe8819cd2008-02-15 16:48:40 +0000120
jadmanski0afbb632008-06-06 21:10:57 +0000121 def __str__(self):
122 return self.name
mblighe8819cd2008-02-15 16:48:40 +0000123
124
showardfb2a7fa2008-07-17 17:04:12 +0000125class User(dbmodels.Model, model_logic.ModelExtensions):
126 """\
127 Required:
128 login :user login name
129
130 Optional:
131 access_level: 0=User (default), 1=Admin, 100=Root
132 """
133 ACCESS_ROOT = 100
134 ACCESS_ADMIN = 1
135 ACCESS_USER = 0
136
137 login = dbmodels.CharField(maxlength=255, unique=True)
138 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
139
showard0fc38302008-10-23 00:44:07 +0000140 # user preferences
141 reboot_before = dbmodels.SmallIntegerField(choices=RebootBefore.choices(),
142 blank=True,
143 default=DEFAULT_REBOOT_BEFORE)
144 reboot_after = dbmodels.SmallIntegerField(choices=RebootAfter.choices(),
145 blank=True,
146 default=DEFAULT_REBOOT_AFTER)
showard97db5ba2008-11-12 18:18:02 +0000147 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000148
showardfb2a7fa2008-07-17 17:04:12 +0000149 name_field = 'login'
150 objects = model_logic.ExtendedManager()
151
152
153 def save(self):
154 # is this a new object being saved for the first time?
155 first_time = (self.id is None)
156 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000157 if user and not user.is_superuser() and user.login != self.login:
158 raise AclAccessViolation("You cannot modify user " + self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000159 super(User, self).save()
160 if first_time:
161 everyone = AclGroup.objects.get(name='Everyone')
162 everyone.users.add(self)
163
164
165 def is_superuser(self):
166 return self.access_level >= self.ACCESS_ROOT
167
168
169 class Meta:
170 db_table = 'users'
171
172 class Admin:
173 list_display = ('login', 'access_level')
174 search_fields = ('login',)
175
176 def __str__(self):
177 return self.login
178
179
showardf8b19042009-05-12 17:22:49 +0000180class Host(model_logic.ModelWithInvalid, dbmodels.Model,
181 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000182 """\
183 Required:
184 hostname
mblighe8819cd2008-02-15 16:48:40 +0000185
jadmanski0afbb632008-06-06 21:10:57 +0000186 optional:
showard21baa452008-10-21 00:08:39 +0000187 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000188
jadmanski0afbb632008-06-06 21:10:57 +0000189 Internal:
190 synch_id: currently unused
191 status: string describing status of host
showard21baa452008-10-21 00:08:39 +0000192 invalid: true if the host has been deleted
193 protection: indicates what can be done to this host during repair
194 locked_by: user that locked the host, or null if the host is unlocked
195 lock_time: DateTime at which the host was locked
196 dirty: true if the host has been used without being rebooted
jadmanski0afbb632008-06-06 21:10:57 +0000197 """
198 Status = enum.Enum('Verifying', 'Running', 'Ready', 'Repairing',
showard45ae8192008-11-05 19:32:53 +0000199 'Repair Failed', 'Dead', 'Cleaning', 'Pending',
showard6d7b2ff2009-06-10 00:16:47 +0000200 string_values=True)
mblighe8819cd2008-02-15 16:48:40 +0000201
jadmanski0afbb632008-06-06 21:10:57 +0000202 hostname = dbmodels.CharField(maxlength=255, unique=True)
203 labels = dbmodels.ManyToManyField(Label, blank=True,
204 filter_interface=dbmodels.HORIZONTAL)
205 locked = dbmodels.BooleanField(default=False)
206 synch_id = dbmodels.IntegerField(blank=True, null=True,
207 editable=settings.FULL_ADMIN)
208 status = dbmodels.CharField(maxlength=255, default=Status.READY,
209 choices=Status.choices(),
210 editable=settings.FULL_ADMIN)
211 invalid = dbmodels.BooleanField(default=False,
212 editable=settings.FULL_ADMIN)
showardd5afc2f2008-08-12 17:19:44 +0000213 protection = dbmodels.SmallIntegerField(null=False, blank=True,
showarddf062562008-07-03 19:56:37 +0000214 choices=host_protections.choices,
215 default=host_protections.default)
showardfb2a7fa2008-07-17 17:04:12 +0000216 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
217 lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False)
showard21baa452008-10-21 00:08:39 +0000218 dirty = dbmodels.BooleanField(default=True, editable=settings.FULL_ADMIN)
mblighe8819cd2008-02-15 16:48:40 +0000219
jadmanski0afbb632008-06-06 21:10:57 +0000220 name_field = 'hostname'
221 objects = model_logic.ExtendedManager()
222 valid_objects = model_logic.ValidObjectsManager()
mbligh5244cbb2008-04-24 20:39:52 +0000223
showard2bab8f42008-11-12 18:15:22 +0000224
225 def __init__(self, *args, **kwargs):
226 super(Host, self).__init__(*args, **kwargs)
227 self._record_attributes(['status'])
228
229
showardb8471e32008-07-03 19:51:08 +0000230 @staticmethod
231 def create_one_time_host(hostname):
232 query = Host.objects.filter(hostname=hostname)
233 if query.count() == 0:
234 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000235 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000236 else:
237 host = query[0]
238 if not host.invalid:
239 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000240 'hostname' : '%s already exists in the autotest DB. '
241 'Select it rather than entering it as a one time '
242 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000243 })
244 host.clean_object()
showarde65d2af2008-07-30 23:34:21 +0000245 AclGroup.objects.get(name='Everyone').hosts.add(host)
showardb8471e32008-07-03 19:51:08 +0000246 host.status = Host.Status.READY
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()
250 return host
mbligh5244cbb2008-04-24 20:39:52 +0000251
showard1ff7b2e2009-05-15 23:17:18 +0000252
jadmanski0afbb632008-06-06 21:10:57 +0000253 def clean_object(self):
254 self.aclgroup_set.clear()
255 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000256
257
jadmanski0afbb632008-06-06 21:10:57 +0000258 def save(self):
259 # extra spaces in the hostname can be a sneaky source of errors
260 self.hostname = self.hostname.strip()
261 # is this a new object being saved for the first time?
262 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000263 if not first_time:
264 AclGroup.check_for_acl_violation_hosts([self])
showardfb2a7fa2008-07-17 17:04:12 +0000265 if self.locked and not self.locked_by:
266 self.locked_by = thread_local.get_user()
267 self.lock_time = datetime.now()
showard21baa452008-10-21 00:08:39 +0000268 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000269 elif not self.locked and self.locked_by:
270 self.locked_by = None
271 self.lock_time = None
jadmanski0afbb632008-06-06 21:10:57 +0000272 super(Host, self).save()
273 if first_time:
274 everyone = AclGroup.objects.get(name='Everyone')
275 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000276 self._check_for_updated_attributes()
277
mblighe8819cd2008-02-15 16:48:40 +0000278
showardb8471e32008-07-03 19:51:08 +0000279 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000280 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000281 for queue_entry in self.hostqueueentry_set.all():
282 queue_entry.deleted = True
showard9a1f2e12008-10-02 11:14:29 +0000283 queue_entry.abort(thread_local.get_user())
showardb8471e32008-07-03 19:51:08 +0000284 super(Host, self).delete()
285
mblighe8819cd2008-02-15 16:48:40 +0000286
showard2bab8f42008-11-12 18:15:22 +0000287 def on_attribute_changed(self, attribute, old_value):
288 assert attribute == 'status'
showard67831ae2009-01-16 03:07:38 +0000289 logger.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000290
291
showard29f7cd22009-04-29 21:16:24 +0000292 def enqueue_job(self, job, atomic_group=None, is_template=False):
showardc92da832009-04-07 18:14:34 +0000293 """Enqueue a job on this host."""
showard29f7cd22009-04-29 21:16:24 +0000294 queue_entry = HostQueueEntry.create(host=self, job=job,
295 is_template=is_template,
296 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000297 # allow recovery of dead hosts from the frontend
298 if not self.active_queue_entry() and self.is_dead():
299 self.status = Host.Status.READY
300 self.save()
301 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000302
showard08f981b2008-06-24 21:59:03 +0000303 block = IneligibleHostQueue(job=job, host=self)
304 block.save()
305
mblighe8819cd2008-02-15 16:48:40 +0000306
jadmanski0afbb632008-06-06 21:10:57 +0000307 def platform(self):
308 # TODO(showard): slighly hacky?
309 platforms = self.labels.filter(platform=True)
310 if len(platforms) == 0:
311 return None
312 return platforms[0]
313 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000314
315
showardcafd16e2009-05-29 18:37:49 +0000316 @classmethod
317 def check_no_platform(cls, hosts):
318 Host.objects.populate_relationships(hosts, Label, 'label_list')
319 errors = []
320 for host in hosts:
321 platforms = [label.name for label in host.label_list
322 if label.platform]
323 if platforms:
324 # do a join, just in case this host has multiple platforms,
325 # we'll be able to see it
326 errors.append('Host %s already has a platform: %s' % (
327 host.hostname, ', '.join(platforms)))
328 if errors:
329 raise model_logic.ValidationError({'labels': '; '.join(errors)})
330
331
jadmanski0afbb632008-06-06 21:10:57 +0000332 def is_dead(self):
333 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000334
335
jadmanski0afbb632008-06-06 21:10:57 +0000336 def active_queue_entry(self):
337 active = list(self.hostqueueentry_set.filter(active=True))
338 if not active:
339 return None
340 assert len(active) == 1, ('More than one active entry for '
341 'host ' + self.hostname)
342 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000343
344
showardf8b19042009-05-12 17:22:49 +0000345 def _get_attribute_model_and_args(self, attribute):
346 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000347
348
jadmanski0afbb632008-06-06 21:10:57 +0000349 class Meta:
350 db_table = 'hosts'
mblighe8819cd2008-02-15 16:48:40 +0000351
jadmanski0afbb632008-06-06 21:10:57 +0000352 class Admin:
353 # TODO(showard) - showing platform requires a SQL query for
354 # each row (since labels are many-to-many) - should we remove
355 # it?
356 list_display = ('hostname', 'platform', 'locked', 'status')
showarddf062562008-07-03 19:56:37 +0000357 list_filter = ('labels', 'locked', 'protection')
jadmanski0afbb632008-06-06 21:10:57 +0000358 search_fields = ('hostname', 'status')
359 # undocumented Django feature - if you set manager here, the
360 # admin code will use it, otherwise it'll use a default Manager
361 manager = model_logic.ValidObjectsManager()
mblighe8819cd2008-02-15 16:48:40 +0000362
jadmanski0afbb632008-06-06 21:10:57 +0000363 def __str__(self):
364 return self.hostname
mblighe8819cd2008-02-15 16:48:40 +0000365
366
showard0957a842009-05-11 19:25:08 +0000367class HostAttribute(dbmodels.Model):
368 """Arbitrary keyvals associated with hosts."""
369 host = dbmodels.ForeignKey(Host)
370 attribute = dbmodels.CharField(maxlength=90)
371 value = dbmodels.CharField(maxlength=300)
372
373 objects = model_logic.ExtendedManager()
374
375 class Meta:
376 db_table = 'host_attributes'
377
378
showard7c785282008-05-29 19:45:12 +0000379class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000380 """\
381 Required:
showard909c7a62008-07-15 21:52:38 +0000382 author: author name
383 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000384 name: test name
showard909c7a62008-07-15 21:52:38 +0000385 time: short, medium, long
386 test_class: This describes the class for your the test belongs in.
387 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000388 test_type: Client or Server
389 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000390 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
391 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000392 i.e. if sync_count = 2 it is a sync job that requires two
393 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000394 Optional:
showard909c7a62008-07-15 21:52:38 +0000395 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000396 dependency_labels: many-to-many relationship with labels corresponding to
397 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000398 experimental: If this is set to True production servers will ignore the test
399 run_verify: Whether or not the scheduler should run the verify stage
jadmanski0afbb632008-06-06 21:10:57 +0000400 """
showard909c7a62008-07-15 21:52:38 +0000401 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
jadmanski0afbb632008-06-06 21:10:57 +0000402 # TODO(showard) - this should be merged with Job.ControlType (but right
403 # now they use opposite values)
404 Types = enum.Enum('Client', 'Server', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000405
jadmanski0afbb632008-06-06 21:10:57 +0000406 name = dbmodels.CharField(maxlength=255, unique=True)
showard909c7a62008-07-15 21:52:38 +0000407 author = dbmodels.CharField(maxlength=255)
408 test_class = dbmodels.CharField(maxlength=255)
409 test_category = dbmodels.CharField(maxlength=255)
410 dependencies = dbmodels.CharField(maxlength=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000411 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000412 experimental = dbmodels.BooleanField(default=True)
413 run_verify = dbmodels.BooleanField(default=True)
414 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
415 default=TestTime.MEDIUM)
jadmanski0afbb632008-06-06 21:10:57 +0000416 test_type = dbmodels.SmallIntegerField(choices=Types.choices())
showard909c7a62008-07-15 21:52:38 +0000417 sync_count = dbmodels.IntegerField(default=1)
showard909c7a62008-07-15 21:52:38 +0000418 path = dbmodels.CharField(maxlength=255, unique=True)
showard989f25d2008-10-01 11:38:11 +0000419 dependency_labels = dbmodels.ManyToManyField(
420 Label, blank=True, filter_interface=dbmodels.HORIZONTAL)
mblighe8819cd2008-02-15 16:48:40 +0000421
jadmanski0afbb632008-06-06 21:10:57 +0000422 name_field = 'name'
423 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000424
425
jadmanski0afbb632008-06-06 21:10:57 +0000426 class Meta:
427 db_table = 'autotests'
mblighe8819cd2008-02-15 16:48:40 +0000428
jadmanski0afbb632008-06-06 21:10:57 +0000429 class Admin:
430 fields = (
431 (None, {'fields' :
showard909c7a62008-07-15 21:52:38 +0000432 ('name', 'author', 'test_category', 'test_class',
showard2bab8f42008-11-12 18:15:22 +0000433 'test_time', 'sync_count', 'test_type', 'sync_count',
showard909c7a62008-07-15 21:52:38 +0000434 'path', 'dependencies', 'experimental', 'run_verify',
435 'description')}),
jadmanski0afbb632008-06-06 21:10:57 +0000436 )
showard2bab8f42008-11-12 18:15:22 +0000437 list_display = ('name', 'test_type', 'description', 'sync_count')
jadmanski0afbb632008-06-06 21:10:57 +0000438 search_fields = ('name',)
mblighe8819cd2008-02-15 16:48:40 +0000439
jadmanski0afbb632008-06-06 21:10:57 +0000440 def __str__(self):
441 return self.name
mblighe8819cd2008-02-15 16:48:40 +0000442
443
showard2b9a88b2008-06-13 20:55:03 +0000444class Profiler(dbmodels.Model, model_logic.ModelExtensions):
445 """\
446 Required:
447 name: profiler name
448 test_type: Client or Server
449
450 Optional:
451 description: arbirary text description
452 """
453 name = dbmodels.CharField(maxlength=255, unique=True)
454 description = dbmodels.TextField(blank=True)
455
456 name_field = 'name'
457 objects = model_logic.ExtendedManager()
458
459
460 class Meta:
461 db_table = 'profilers'
462
463 class Admin:
464 list_display = ('name', 'description')
465 search_fields = ('name',)
466
467 def __str__(self):
468 return self.name
469
470
showard7c785282008-05-29 19:45:12 +0000471class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000472 """\
473 Required:
474 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000475
jadmanski0afbb632008-06-06 21:10:57 +0000476 Optional:
477 description: arbitrary description of group
478 """
479 name = dbmodels.CharField(maxlength=255, unique=True)
480 description = dbmodels.CharField(maxlength=255, blank=True)
showard04f2cd82008-07-25 20:53:31 +0000481 users = dbmodels.ManyToManyField(User, blank=True,
jadmanski0afbb632008-06-06 21:10:57 +0000482 filter_interface=dbmodels.HORIZONTAL)
483 hosts = dbmodels.ManyToManyField(Host,
484 filter_interface=dbmodels.HORIZONTAL)
mblighe8819cd2008-02-15 16:48:40 +0000485
jadmanski0afbb632008-06-06 21:10:57 +0000486 name_field = 'name'
487 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000488
showard08f981b2008-06-24 21:59:03 +0000489 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000490 def check_for_acl_violation_hosts(hosts):
491 user = thread_local.get_user()
492 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000493 return
showard3dd47c22008-07-10 00:41:36 +0000494 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000495 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000496 for host in hosts:
497 # Check if the user has access to this host,
498 # but only if it is not a metahost
499 if (isinstance(host, Host)
500 and int(host.id) not in accessible_host_ids):
501 raise AclAccessViolation("You do not have access to %s"
502 % str(host))
503
showard9dbdcda2008-10-14 17:34:36 +0000504
505 @staticmethod
showarddc817512008-11-12 18:16:41 +0000506 def check_abort_permissions(queue_entries):
507 """
508 look for queue entries that aren't abortable, meaning
509 * the job isn't owned by this user, and
510 * the machine isn't ACL-accessible, or
511 * the machine is in the "Everyone" ACL
512 """
showard9dbdcda2008-10-14 17:34:36 +0000513 user = thread_local.get_user()
514 if user.is_superuser():
515 return
showarddc817512008-11-12 18:16:41 +0000516 not_owned = queue_entries.exclude(job__owner=user.login)
517 # I do this using ID sets instead of just Django filters because
518 # filtering on M2M fields is broken in Django 0.96. It's better in 1.0.
519 accessible_ids = set(
520 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000521 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000522 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000523 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000524 cannot_abort = [entry for entry in not_owned.select_related()
525 if entry.id not in accessible_ids
526 or entry.id in public_ids]
527 if len(cannot_abort) == 0:
528 return
529 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000530 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000531 for entry in cannot_abort)
532 raise AclAccessViolation('You cannot abort the following job entries: '
533 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000534
535
showard3dd47c22008-07-10 00:41:36 +0000536 def check_for_acl_violation_acl_group(self):
537 user = thread_local.get_user()
538 if user.is_superuser():
539 return None
540 if not user in self.users.all():
541 raise AclAccessViolation("You do not have access to %s"
542 % self.name)
543
544 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000545 def on_host_membership_change():
546 everyone = AclGroup.objects.get(name='Everyone')
547
showard3dd47c22008-07-10 00:41:36 +0000548 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000549 # TODO(showard): this is a bit of a hack, since the fact that this query
550 # works is kind of a coincidence of Django internals. This trick
551 # doesn't work in general (on all foreign key relationships). I'll
552 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000553 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000554 everyone.hosts.add(*orphaned_hosts.distinct())
555
556 # find hosts in both Everyone and another ACL group, and remove them
557 # from Everyone
558 hosts_in_everyone = Host.valid_objects.filter_custom_join(
showardd9ac4452009-02-07 02:04:37 +0000559 '_everyone', aclgroup__name='Everyone')
560 acled_hosts = hosts_in_everyone.exclude(aclgroup__name='Everyone')
showard08f981b2008-06-24 21:59:03 +0000561 everyone.hosts.remove(*acled_hosts.distinct())
562
563
564 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000565 if (self.name == 'Everyone'):
566 raise AclAccessViolation("You cannot delete 'Everyone'!")
567 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000568 super(AclGroup, self).delete()
569 self.on_host_membership_change()
570
571
showard04f2cd82008-07-25 20:53:31 +0000572 def add_current_user_if_empty(self):
573 if not self.users.count():
574 self.users.add(thread_local.get_user())
575
576
showard08f981b2008-06-24 21:59:03 +0000577 # if you have a model attribute called "Manipulator", Django will
578 # automatically insert it into the beginning of the superclass list
579 # for the model's manipulators
580 class Manipulator(object):
581 """
582 Custom manipulator to get notification when ACLs are changed through
583 the admin interface.
584 """
585 def save(self, new_data):
showard3dd47c22008-07-10 00:41:36 +0000586 user = thread_local.get_user()
showard31c570e2008-07-23 19:29:17 +0000587 if hasattr(self, 'original_object'):
588 if (not user.is_superuser()
589 and self.original_object.name == 'Everyone'):
590 raise AclAccessViolation("You cannot modify 'Everyone'!")
591 self.original_object.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000592 obj = super(AclGroup.Manipulator, self).save(new_data)
showard04f2cd82008-07-25 20:53:31 +0000593 if not hasattr(self, 'original_object'):
594 obj.users.add(thread_local.get_user())
595 obj.add_current_user_if_empty()
showard08f981b2008-06-24 21:59:03 +0000596 obj.on_host_membership_change()
597 return obj
showardeb3be4d2008-04-21 20:59:26 +0000598
jadmanski0afbb632008-06-06 21:10:57 +0000599 class Meta:
600 db_table = 'acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000601
jadmanski0afbb632008-06-06 21:10:57 +0000602 class Admin:
603 list_display = ('name', 'description')
604 search_fields = ('name',)
mblighe8819cd2008-02-15 16:48:40 +0000605
jadmanski0afbb632008-06-06 21:10:57 +0000606 def __str__(self):
607 return self.name
mblighe8819cd2008-02-15 16:48:40 +0000608
mblighe8819cd2008-02-15 16:48:40 +0000609
showard7c785282008-05-29 19:45:12 +0000610class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +0000611 'Custom manager to provide efficient status counts querying.'
612 def get_status_counts(self, job_ids):
613 """\
614 Returns a dictionary mapping the given job IDs to their status
615 count dictionaries.
616 """
617 if not job_ids:
618 return {}
619 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
620 cursor = connection.cursor()
621 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +0000622 SELECT job_id, status, aborted, complete, COUNT(*)
jadmanski0afbb632008-06-06 21:10:57 +0000623 FROM host_queue_entries
624 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +0000625 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +0000626 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +0000627 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +0000628 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +0000629 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +0000630 full_status = HostQueueEntry.compute_full_status(status, aborted,
631 complete)
showardb6d16622009-05-26 19:35:29 +0000632 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +0000633 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +0000634 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +0000635
636
showard7c785282008-05-29 19:45:12 +0000637class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000638 """\
639 owner: username of job owner
640 name: job name (does not have to be unique)
641 priority: Low, Medium, High, Urgent (or 0-3)
642 control_file: contents of control file
643 control_type: Client or Server
644 created_on: date of job creation
645 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +0000646 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +0000647 run_verify: Whether or not to run the verify phase
showard12f3e322009-05-13 21:27:42 +0000648 timeout: hours from queuing time until job times out
649 max_runtime_hrs: hours from job starting time until job times out
showard542e8402008-09-19 20:16:18 +0000650 email_list: list of people to email on completion delimited by any of:
651 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +0000652 dependency_labels: many-to-many relationship with labels corresponding to
653 job dependencies
showard21baa452008-10-21 00:08:39 +0000654 reboot_before: Never, If dirty, or Always
655 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +0000656 parse_failed_repair: if True, a failed repair launched by this job will have
657 its results parsed as part of the job.
jadmanski0afbb632008-06-06 21:10:57 +0000658 """
showardb1e51872008-10-07 11:08:18 +0000659 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
660 'AUTOTEST_WEB', 'job_timeout_default', default=240)
showard12f3e322009-05-13 21:27:42 +0000661 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
662 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
showarda1e74b32009-05-12 17:32:04 +0000663 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
664 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
665 default=False)
showardb1e51872008-10-07 11:08:18 +0000666
jadmanski0afbb632008-06-06 21:10:57 +0000667 Priority = enum.Enum('Low', 'Medium', 'High', 'Urgent')
668 ControlType = enum.Enum('Server', 'Client', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000669
jadmanski0afbb632008-06-06 21:10:57 +0000670 owner = dbmodels.CharField(maxlength=255)
671 name = dbmodels.CharField(maxlength=255)
672 priority = dbmodels.SmallIntegerField(choices=Priority.choices(),
673 blank=True, # to allow 0
674 default=Priority.MEDIUM)
675 control_file = dbmodels.TextField()
676 control_type = dbmodels.SmallIntegerField(choices=ControlType.choices(),
showardb1e51872008-10-07 11:08:18 +0000677 blank=True, # to allow 0
678 default=ControlType.CLIENT)
showard68c7aa02008-10-09 16:49:11 +0000679 created_on = dbmodels.DateTimeField()
showard2bab8f42008-11-12 18:15:22 +0000680 synch_count = dbmodels.IntegerField(null=True, default=1)
showardb1e51872008-10-07 11:08:18 +0000681 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
showard9976ce92008-10-15 20:28:13 +0000682 run_verify = dbmodels.BooleanField(default=True)
showard542e8402008-09-19 20:16:18 +0000683 email_list = dbmodels.CharField(maxlength=250, blank=True)
showard989f25d2008-10-01 11:38:11 +0000684 dependency_labels = dbmodels.ManyToManyField(
685 Label, blank=True, filter_interface=dbmodels.HORIZONTAL)
showard21baa452008-10-21 00:08:39 +0000686 reboot_before = dbmodels.SmallIntegerField(choices=RebootBefore.choices(),
687 blank=True,
showard0fc38302008-10-23 00:44:07 +0000688 default=DEFAULT_REBOOT_BEFORE)
showard21baa452008-10-21 00:08:39 +0000689 reboot_after = dbmodels.SmallIntegerField(choices=RebootAfter.choices(),
690 blank=True,
showard0fc38302008-10-23 00:44:07 +0000691 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +0000692 parse_failed_repair = dbmodels.BooleanField(
693 default=DEFAULT_PARSE_FAILED_REPAIR)
showard12f3e322009-05-13 21:27:42 +0000694 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
mblighe8819cd2008-02-15 16:48:40 +0000695
696
jadmanski0afbb632008-06-06 21:10:57 +0000697 # custom manager
698 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +0000699
700
jadmanski0afbb632008-06-06 21:10:57 +0000701 def is_server_job(self):
702 return self.control_type == self.ControlType.SERVER
mblighe8819cd2008-02-15 16:48:40 +0000703
704
jadmanski0afbb632008-06-06 21:10:57 +0000705 @classmethod
showarda1e74b32009-05-12 17:32:04 +0000706 def create(cls, owner, options, hosts):
jadmanski0afbb632008-06-06 21:10:57 +0000707 """\
708 Creates a job by taking some information (the listed args)
709 and filling in the rest of the necessary information.
710 """
showard3dd47c22008-07-10 00:41:36 +0000711 AclGroup.check_for_acl_violation_hosts(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000712 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +0000713 owner=owner,
714 name=options['name'],
715 priority=options['priority'],
716 control_file=options['control_file'],
717 control_type=options['control_type'],
718 synch_count=options.get('synch_count'),
719 timeout=options.get('timeout'),
showard12f3e322009-05-13 21:27:42 +0000720 max_runtime_hrs=options.get('max_runtime_hrs'),
showarda1e74b32009-05-12 17:32:04 +0000721 run_verify=options.get('run_verify'),
722 email_list=options.get('email_list'),
723 reboot_before=options.get('reboot_before'),
724 reboot_after=options.get('reboot_after'),
725 parse_failed_repair=options.get('parse_failed_repair'),
showard68c7aa02008-10-09 16:49:11 +0000726 created_on=datetime.now())
mblighe8819cd2008-02-15 16:48:40 +0000727
showarda1e74b32009-05-12 17:32:04 +0000728 job.dependency_labels = options['dependencies']
jadmanski0afbb632008-06-06 21:10:57 +0000729 return job
mblighe8819cd2008-02-15 16:48:40 +0000730
731
showard29f7cd22009-04-29 21:16:24 +0000732 def queue(self, hosts, atomic_group=None, is_template=False):
showardc92da832009-04-07 18:14:34 +0000733 """Enqueue a job on the given hosts."""
734 if atomic_group and not hosts:
735 # No hosts or labels are required to queue an atomic group
736 # Job. However, if they are given, we respect them below.
showard29f7cd22009-04-29 21:16:24 +0000737 atomic_group.enqueue_job(self, is_template=is_template)
jadmanski0afbb632008-06-06 21:10:57 +0000738 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +0000739 host.enqueue_job(self, atomic_group=atomic_group,
740 is_template=is_template)
741
742
743 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
744 rec = RecurringRun(job=self, start_date=start_date,
745 loop_period=loop_period,
746 loop_count=loop_count,
747 owner=User.objects.get(login=owner))
748 rec.save()
749 return rec.id
mblighe8819cd2008-02-15 16:48:40 +0000750
751
jadmanski0afbb632008-06-06 21:10:57 +0000752 def user(self):
753 try:
754 return User.objects.get(login=self.owner)
755 except self.DoesNotExist:
756 return None
mblighe8819cd2008-02-15 16:48:40 +0000757
758
showard98863972008-10-29 21:14:56 +0000759 def abort(self, aborted_by):
760 for queue_entry in self.hostqueueentry_set.all():
761 queue_entry.abort(aborted_by)
762
763
jadmanski0afbb632008-06-06 21:10:57 +0000764 class Meta:
765 db_table = 'jobs'
mblighe8819cd2008-02-15 16:48:40 +0000766
jadmanski0afbb632008-06-06 21:10:57 +0000767 if settings.FULL_ADMIN:
768 class Admin:
769 list_display = ('id', 'owner', 'name', 'control_type')
mblighe8819cd2008-02-15 16:48:40 +0000770
jadmanski0afbb632008-06-06 21:10:57 +0000771 def __str__(self):
772 return '%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +0000773
774
showard7c785282008-05-29 19:45:12 +0000775class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000776 job = dbmodels.ForeignKey(Job)
777 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +0000778
jadmanski0afbb632008-06-06 21:10:57 +0000779 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000780
jadmanski0afbb632008-06-06 21:10:57 +0000781 class Meta:
782 db_table = 'ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +0000783
jadmanski0afbb632008-06-06 21:10:57 +0000784 if settings.FULL_ADMIN:
785 class Admin:
786 list_display = ('id', 'job', 'host')
mblighe8819cd2008-02-15 16:48:40 +0000787
788
showard7c785282008-05-29 19:45:12 +0000789class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
showarda3ab0d52008-11-03 19:03:47 +0000790 Status = enum.Enum('Queued', 'Starting', 'Verifying', 'Pending', 'Running',
showardd3dc1992009-04-22 21:01:40 +0000791 'Gathering', 'Parsing', 'Aborted', 'Completed',
showard29f7cd22009-04-29 21:16:24 +0000792 'Failed', 'Stopped', 'Template', string_values=True)
showard2bab8f42008-11-12 18:15:22 +0000793 ACTIVE_STATUSES = (Status.STARTING, Status.VERIFYING, Status.PENDING,
showardd3dc1992009-04-22 21:01:40 +0000794 Status.RUNNING, Status.GATHERING)
showardc85c21b2008-11-24 22:17:37 +0000795 COMPLETE_STATUSES = (Status.ABORTED, Status.COMPLETED, Status.FAILED,
showard29f7cd22009-04-29 21:16:24 +0000796 Status.STOPPED, Status.TEMPLATE)
showarda3ab0d52008-11-03 19:03:47 +0000797
jadmanski0afbb632008-06-06 21:10:57 +0000798 job = dbmodels.ForeignKey(Job)
799 host = dbmodels.ForeignKey(Host, blank=True, null=True)
jadmanski0afbb632008-06-06 21:10:57 +0000800 status = dbmodels.CharField(maxlength=255)
801 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
802 db_column='meta_host')
803 active = dbmodels.BooleanField(default=False)
804 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +0000805 deleted = dbmodels.BooleanField(default=False)
showard2bab8f42008-11-12 18:15:22 +0000806 execution_subdir = dbmodels.CharField(maxlength=255, blank=True, default='')
showard89f84db2009-03-12 20:39:13 +0000807 # If atomic_group is set, this is a virtual HostQueueEntry that will
808 # be expanded into many actual hosts within the group at schedule time.
809 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +0000810 aborted = dbmodels.BooleanField(default=False)
showard12f3e322009-05-13 21:27:42 +0000811 started_on = dbmodels.DateTimeField(null=True)
mblighe8819cd2008-02-15 16:48:40 +0000812
jadmanski0afbb632008-06-06 21:10:57 +0000813 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000814
mblighe8819cd2008-02-15 16:48:40 +0000815
showard2bab8f42008-11-12 18:15:22 +0000816 def __init__(self, *args, **kwargs):
817 super(HostQueueEntry, self).__init__(*args, **kwargs)
818 self._record_attributes(['status'])
819
820
showard29f7cd22009-04-29 21:16:24 +0000821 @classmethod
822 def create(cls, job, host=None, meta_host=None, atomic_group=None,
823 is_template=False):
824 if is_template:
825 status = cls.Status.TEMPLATE
826 else:
827 status = cls.Status.QUEUED
828
829 return cls(job=job, host=host, meta_host=meta_host,
830 atomic_group=atomic_group, status=status)
831
832
showard2bab8f42008-11-12 18:15:22 +0000833 def save(self):
834 self._set_active_and_complete()
835 super(HostQueueEntry, self).save()
836 self._check_for_updated_attributes()
837
838
showard3f15eed2008-11-14 22:40:48 +0000839 def host_or_metahost_name(self):
840 if self.host:
841 return self.host.hostname
842 else:
843 assert self.meta_host
844 return self.meta_host.name
845
846
showard2bab8f42008-11-12 18:15:22 +0000847 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +0000848 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +0000849 self.active, self.complete = True, False
850 elif self.status in self.COMPLETE_STATUSES:
851 self.active, self.complete = False, True
852 else:
853 self.active, self.complete = False, False
854
855
856 def on_attribute_changed(self, attribute, old_value):
857 assert attribute == 'status'
showard67831ae2009-01-16 03:07:38 +0000858 logger.info('%s/%d (%d) -> %s' % (self.host, self.job.id, self.id,
showarda1e74b32009-05-12 17:32:04 +0000859 self.status))
showard2bab8f42008-11-12 18:15:22 +0000860
861
jadmanski0afbb632008-06-06 21:10:57 +0000862 def is_meta_host_entry(self):
863 'True if this is a entry has a meta_host instead of a host.'
864 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +0000865
showarda3ab0d52008-11-03 19:03:47 +0000866
showard4c119042008-09-29 19:16:18 +0000867 def log_abort(self, user):
showard98863972008-10-29 21:14:56 +0000868 if user is None:
869 # automatic system abort (i.e. job timeout)
870 return
showard4c119042008-09-29 19:16:18 +0000871 abort_log = AbortedHostQueueEntry(queue_entry=self, aborted_by=user)
872 abort_log.save()
873
showarda3ab0d52008-11-03 19:03:47 +0000874
showard4c119042008-09-29 19:16:18 +0000875 def abort(self, user):
showarda3ab0d52008-11-03 19:03:47 +0000876 # this isn't completely immune to race conditions since it's not atomic,
877 # but it should be safe given the scheduler's behavior.
showardd3dc1992009-04-22 21:01:40 +0000878 if not self.complete and not self.aborted:
showard4c119042008-09-29 19:16:18 +0000879 self.log_abort(user)
showardd3dc1992009-04-22 21:01:40 +0000880 self.aborted = True
showarda3ab0d52008-11-03 19:03:47 +0000881 self.save()
mblighe8819cd2008-02-15 16:48:40 +0000882
showardd3dc1992009-04-22 21:01:40 +0000883
884 @classmethod
885 def compute_full_status(cls, status, aborted, complete):
886 if aborted and not complete:
887 return 'Aborted (%s)' % status
888 return status
889
890
891 def full_status(self):
892 return self.compute_full_status(self.status, self.aborted,
893 self.complete)
894
895
896 def _postprocess_object_dict(self, object_dict):
897 object_dict['full_status'] = self.full_status()
898
899
jadmanski0afbb632008-06-06 21:10:57 +0000900 class Meta:
901 db_table = 'host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +0000902
showard12f3e322009-05-13 21:27:42 +0000903
jadmanski0afbb632008-06-06 21:10:57 +0000904 if settings.FULL_ADMIN:
905 class Admin:
906 list_display = ('id', 'job', 'host', 'status',
907 'meta_host')
showard4c119042008-09-29 19:16:18 +0000908
909
showard12f3e322009-05-13 21:27:42 +0000910 def __str__(self):
911 hostname = None
912 if self.host:
913 hostname = self.host.hostname
914 return "%s/%d (%d)" % (hostname, self.job.id, self.id)
915
916
showard4c119042008-09-29 19:16:18 +0000917class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
918 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
919 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +0000920 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +0000921
922 objects = model_logic.ExtendedManager()
923
showard68c7aa02008-10-09 16:49:11 +0000924
925 def save(self):
926 self.aborted_on = datetime.now()
927 super(AbortedHostQueueEntry, self).save()
928
showard4c119042008-09-29 19:16:18 +0000929 class Meta:
930 db_table = 'aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +0000931
932
933class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
934 """\
935 job: job to use as a template
936 owner: owner of the instantiated template
937 start_date: Run the job at scheduled date
938 loop_period: Re-run (loop) the job periodically
939 (in every loop_period seconds)
940 loop_count: Re-run (loop) count
941 """
942
943 job = dbmodels.ForeignKey(Job)
944 owner = dbmodels.ForeignKey(User)
945 start_date = dbmodels.DateTimeField()
946 loop_period = dbmodels.IntegerField(blank=True)
947 loop_count = dbmodels.IntegerField(blank=True)
948
949 objects = model_logic.ExtendedManager()
950
951 class Meta:
952 db_table = 'recurring_run'
953
954 def __str__(self):
955 return 'RecurringRun(job %s, start %s, period %s, count %s)' % (
956 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +0000957
958
959class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
960 """\
961 Tasks to run on hosts at the next time they are in the Ready state. Use this
962 for high-priority tasks, such as forced repair or forced reinstall.
963
964 host: host to run this task on
965 task: special task to run (currently only Reverify)
966 time_requested: date and time the request for this task was made
967 is_active: task is currently running
968 is_complete: task has finished running
969 """
970 Task = enum.Enum('Reverify', string_values=True)
971
972 host = dbmodels.ForeignKey(Host, blank=False, null=False)
973 task = dbmodels.CharField(maxlength=64, choices=Task.choices(),
974 blank=False, null=False)
975 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
976 null=False)
977 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
978 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
979
980 objects = model_logic.ExtendedManager()
981
982
983 @classmethod
984 def schedule_special_task(cls, hosts, task):
985 """\
986 Schedules hosts for a special task
987 """
988 for host in hosts:
989 special_task = SpecialTask(host=host, task=task)
990 special_task.save()
991
992
993 class Meta:
994 db_table = 'special_tasks'
995
996 def __str__(self):
997 result = 'Special Task (host %s, task %s, time %s)' % (
998 self.host, self.task, self.time_requested)
999 if self.is_complete:
1000 result += ' (completed)'
1001 elif self.is_active:
1002 result += ' (active)'
1003
1004 return result