blob: c53eb42380f98959da76d3f46a8c1f1aa520b86c [file] [log] [blame]
showardfb2a7fa2008-07-17 17:04:12 +00001from datetime import datetime
showard7c785282008-05-29 19:45:12 +00002from django.db import models as dbmodels, connection
showarddf062562008-07-03 19:56:37 +00003from frontend.afe import model_logic
showard3dd47c22008-07-10 00:41:36 +00004from frontend import settings, thread_local
showardb1e51872008-10-07 11:08:18 +00005from autotest_lib.client.common_lib import enum, host_protections, global_config
showard2bab8f42008-11-12 18:15:22 +00006from autotest_lib.client.common_lib import debug
7
8logger = debug.get_logger()
mblighe8819cd2008-02-15 16:48:40 +00009
showard0fc38302008-10-23 00:44:07 +000010# job options and user preferences
11RebootBefore = enum.Enum('Never', 'If dirty', 'Always')
12DEFAULT_REBOOT_BEFORE = RebootBefore.IF_DIRTY
13RebootAfter = enum.Enum('Never', 'If all tests passed', 'Always')
14DEFAULT_REBOOT_AFTER = RebootBefore.ALWAYS
mblighe8819cd2008-02-15 16:48:40 +000015
showard89f84db2009-03-12 20:39:13 +000016
mblighe8819cd2008-02-15 16:48:40 +000017class AclAccessViolation(Exception):
jadmanski0afbb632008-06-06 21:10:57 +000018 """\
19 Raised when an operation is attempted with proper permissions as
20 dictated by ACLs.
21 """
mblighe8819cd2008-02-15 16:48:40 +000022
23
showard205fd602009-03-21 00:17:35 +000024class AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model):
showard89f84db2009-03-12 20:39:13 +000025 """\
26 An atomic group defines a collection of hosts which must only be scheduled
27 all at once. Any host with a label having an atomic group will only be
28 scheduled for a job at the same time as other hosts sharing that label.
29
30 Required:
31 name: A name for this atomic group. ex: 'rack23' or 'funky_net'
32 max_number_of_machines: The maximum number of machines that will be
33 scheduled at once when scheduling jobs to this atomic group.
34 The job.synch_count is considered the minimum.
35
36 Optional:
37 description: Arbitrary text description of this group's purpose.
38 """
39 name = dbmodels.CharField(maxlength=255, unique=True)
40 description = dbmodels.TextField(blank=True)
41 max_number_of_machines = dbmodels.IntegerField(default=1)
showard205fd602009-03-21 00:17:35 +000042 invalid = dbmodels.BooleanField(default=False,
43 editable=settings.FULL_ADMIN)
showard89f84db2009-03-12 20:39:13 +000044
showard89f84db2009-03-12 20:39:13 +000045 name_field = 'name'
showard205fd602009-03-21 00:17:35 +000046 objects = model_logic.ExtendedManager()
47 valid_objects = model_logic.ValidObjectsManager()
48
49
showardc92da832009-04-07 18:14:34 +000050 def enqueue_job(self, job):
51 """Enqueue a job on an associated atomic group of hosts."""
52 queue_entry = HostQueueEntry(atomic_group=self, job=job,
53 status=HostQueueEntry.Status.QUEUED)
54 queue_entry.save()
55
56
showard205fd602009-03-21 00:17:35 +000057 def clean_object(self):
58 self.label_set.clear()
showard89f84db2009-03-12 20:39:13 +000059
60
61 class Meta:
62 db_table = 'atomic_groups'
63
64 class Admin:
65 list_display = ('name', 'description', 'max_number_of_machines')
showard205fd602009-03-21 00:17:35 +000066 # see Host.Admin
67 manager = model_logic.ValidObjectsManager()
68
69 def __str__(self):
70 return self.name
showard89f84db2009-03-12 20:39:13 +000071
72
showard7c785282008-05-29 19:45:12 +000073class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000074 """\
75 Required:
showard89f84db2009-03-12 20:39:13 +000076 name: label name
mblighe8819cd2008-02-15 16:48:40 +000077
jadmanski0afbb632008-06-06 21:10:57 +000078 Optional:
showard89f84db2009-03-12 20:39:13 +000079 kernel_config: URL/path to kernel config for jobs run on this label.
80 platform: If True, this is a platform label (defaults to False).
81 only_if_needed: If True, a Host with this label can only be used if that
82 label is requested by the job/test (either as the meta_host or
83 in the job_dependencies).
84 atomic_group: The atomic group associated with this label.
jadmanski0afbb632008-06-06 21:10:57 +000085 """
86 name = dbmodels.CharField(maxlength=255, unique=True)
87 kernel_config = dbmodels.CharField(maxlength=255, blank=True)
88 platform = dbmodels.BooleanField(default=False)
89 invalid = dbmodels.BooleanField(default=False,
90 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +000091 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +000092
jadmanski0afbb632008-06-06 21:10:57 +000093 name_field = 'name'
94 objects = model_logic.ExtendedManager()
95 valid_objects = model_logic.ValidObjectsManager()
showard89f84db2009-03-12 20:39:13 +000096 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
97
mbligh5244cbb2008-04-24 20:39:52 +000098
jadmanski0afbb632008-06-06 21:10:57 +000099 def clean_object(self):
100 self.host_set.clear()
mblighe8819cd2008-02-15 16:48:40 +0000101
102
showardc92da832009-04-07 18:14:34 +0000103 def enqueue_job(self, job, atomic_group=None):
104 """Enqueue a job on any host of this label."""
jadmanski0afbb632008-06-06 21:10:57 +0000105 queue_entry = HostQueueEntry(meta_host=self, job=job,
showardc92da832009-04-07 18:14:34 +0000106 status=HostQueueEntry.Status.QUEUED,
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
showard7c785282008-05-29 19:45:12 +0000178class Host(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +0000179 """\
180 Required:
181 hostname
mblighe8819cd2008-02-15 16:48:40 +0000182
jadmanski0afbb632008-06-06 21:10:57 +0000183 optional:
showard21baa452008-10-21 00:08:39 +0000184 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000185
jadmanski0afbb632008-06-06 21:10:57 +0000186 Internal:
187 synch_id: currently unused
188 status: string describing status of host
showard21baa452008-10-21 00:08:39 +0000189 invalid: true if the host has been deleted
190 protection: indicates what can be done to this host during repair
191 locked_by: user that locked the host, or null if the host is unlocked
192 lock_time: DateTime at which the host was locked
193 dirty: true if the host has been used without being rebooted
jadmanski0afbb632008-06-06 21:10:57 +0000194 """
195 Status = enum.Enum('Verifying', 'Running', 'Ready', 'Repairing',
showard45ae8192008-11-05 19:32:53 +0000196 'Repair Failed', 'Dead', 'Cleaning', 'Pending',
jadmanski0afbb632008-06-06 21:10:57 +0000197 string_values=True)
mblighe8819cd2008-02-15 16:48:40 +0000198
jadmanski0afbb632008-06-06 21:10:57 +0000199 hostname = dbmodels.CharField(maxlength=255, unique=True)
200 labels = dbmodels.ManyToManyField(Label, blank=True,
201 filter_interface=dbmodels.HORIZONTAL)
202 locked = dbmodels.BooleanField(default=False)
203 synch_id = dbmodels.IntegerField(blank=True, null=True,
204 editable=settings.FULL_ADMIN)
205 status = dbmodels.CharField(maxlength=255, default=Status.READY,
206 choices=Status.choices(),
207 editable=settings.FULL_ADMIN)
208 invalid = dbmodels.BooleanField(default=False,
209 editable=settings.FULL_ADMIN)
showardd5afc2f2008-08-12 17:19:44 +0000210 protection = dbmodels.SmallIntegerField(null=False, blank=True,
showarddf062562008-07-03 19:56:37 +0000211 choices=host_protections.choices,
212 default=host_protections.default)
showardfb2a7fa2008-07-17 17:04:12 +0000213 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
214 lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False)
showard21baa452008-10-21 00:08:39 +0000215 dirty = dbmodels.BooleanField(default=True, editable=settings.FULL_ADMIN)
mblighe8819cd2008-02-15 16:48:40 +0000216
jadmanski0afbb632008-06-06 21:10:57 +0000217 name_field = 'hostname'
218 objects = model_logic.ExtendedManager()
219 valid_objects = model_logic.ValidObjectsManager()
mbligh5244cbb2008-04-24 20:39:52 +0000220
showard2bab8f42008-11-12 18:15:22 +0000221
222 def __init__(self, *args, **kwargs):
223 super(Host, self).__init__(*args, **kwargs)
224 self._record_attributes(['status'])
225
226
showardb8471e32008-07-03 19:51:08 +0000227 @staticmethod
228 def create_one_time_host(hostname):
229 query = Host.objects.filter(hostname=hostname)
230 if query.count() == 0:
231 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000232 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000233 else:
234 host = query[0]
235 if not host.invalid:
236 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000237 'hostname' : '%s already exists in the autotest DB. '
238 'Select it rather than entering it as a one time '
239 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000240 })
241 host.clean_object()
showarde65d2af2008-07-30 23:34:21 +0000242 AclGroup.objects.get(name='Everyone').hosts.add(host)
showardb8471e32008-07-03 19:51:08 +0000243 host.status = Host.Status.READY
showard1ab512b2008-07-30 23:39:04 +0000244 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000245 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000246 host.save()
247 return host
mbligh5244cbb2008-04-24 20:39:52 +0000248
jadmanski0afbb632008-06-06 21:10:57 +0000249 def clean_object(self):
250 self.aclgroup_set.clear()
251 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000252
253
jadmanski0afbb632008-06-06 21:10:57 +0000254 def save(self):
255 # extra spaces in the hostname can be a sneaky source of errors
256 self.hostname = self.hostname.strip()
257 # is this a new object being saved for the first time?
258 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000259 if not first_time:
260 AclGroup.check_for_acl_violation_hosts([self])
showardfb2a7fa2008-07-17 17:04:12 +0000261 if self.locked and not self.locked_by:
262 self.locked_by = thread_local.get_user()
263 self.lock_time = datetime.now()
showard21baa452008-10-21 00:08:39 +0000264 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000265 elif not self.locked and self.locked_by:
266 self.locked_by = None
267 self.lock_time = None
jadmanski0afbb632008-06-06 21:10:57 +0000268 super(Host, self).save()
269 if first_time:
270 everyone = AclGroup.objects.get(name='Everyone')
271 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000272 self._check_for_updated_attributes()
273
mblighe8819cd2008-02-15 16:48:40 +0000274
showardb8471e32008-07-03 19:51:08 +0000275 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000276 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000277 for queue_entry in self.hostqueueentry_set.all():
278 queue_entry.deleted = True
showard9a1f2e12008-10-02 11:14:29 +0000279 queue_entry.abort(thread_local.get_user())
showardb8471e32008-07-03 19:51:08 +0000280 super(Host, self).delete()
281
mblighe8819cd2008-02-15 16:48:40 +0000282
showard2bab8f42008-11-12 18:15:22 +0000283 def on_attribute_changed(self, attribute, old_value):
284 assert attribute == 'status'
showard67831ae2009-01-16 03:07:38 +0000285 logger.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000286
287
showardc92da832009-04-07 18:14:34 +0000288 def enqueue_job(self, job, atomic_group=None):
289 """Enqueue a job on this host."""
jadmanski0afbb632008-06-06 21:10:57 +0000290 queue_entry = HostQueueEntry(host=self, job=job,
showardc92da832009-04-07 18:14:34 +0000291 status=HostQueueEntry.Status.QUEUED,
292 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000293 # allow recovery of dead hosts from the frontend
294 if not self.active_queue_entry() and self.is_dead():
295 self.status = Host.Status.READY
296 self.save()
297 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000298
showard08f981b2008-06-24 21:59:03 +0000299 block = IneligibleHostQueue(job=job, host=self)
300 block.save()
301
mblighe8819cd2008-02-15 16:48:40 +0000302
jadmanski0afbb632008-06-06 21:10:57 +0000303 def platform(self):
304 # TODO(showard): slighly hacky?
305 platforms = self.labels.filter(platform=True)
306 if len(platforms) == 0:
307 return None
308 return platforms[0]
309 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000310
311
jadmanski0afbb632008-06-06 21:10:57 +0000312 def is_dead(self):
313 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000314
315
jadmanski0afbb632008-06-06 21:10:57 +0000316 def active_queue_entry(self):
317 active = list(self.hostqueueentry_set.filter(active=True))
318 if not active:
319 return None
320 assert len(active) == 1, ('More than one active entry for '
321 'host ' + self.hostname)
322 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000323
324
jadmanski0afbb632008-06-06 21:10:57 +0000325 class Meta:
326 db_table = 'hosts'
mblighe8819cd2008-02-15 16:48:40 +0000327
jadmanski0afbb632008-06-06 21:10:57 +0000328 class Admin:
329 # TODO(showard) - showing platform requires a SQL query for
330 # each row (since labels are many-to-many) - should we remove
331 # it?
332 list_display = ('hostname', 'platform', 'locked', 'status')
showarddf062562008-07-03 19:56:37 +0000333 list_filter = ('labels', 'locked', 'protection')
jadmanski0afbb632008-06-06 21:10:57 +0000334 search_fields = ('hostname', 'status')
335 # undocumented Django feature - if you set manager here, the
336 # admin code will use it, otherwise it'll use a default Manager
337 manager = model_logic.ValidObjectsManager()
mblighe8819cd2008-02-15 16:48:40 +0000338
jadmanski0afbb632008-06-06 21:10:57 +0000339 def __str__(self):
340 return self.hostname
mblighe8819cd2008-02-15 16:48:40 +0000341
342
showard7c785282008-05-29 19:45:12 +0000343class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000344 """\
345 Required:
showard909c7a62008-07-15 21:52:38 +0000346 author: author name
347 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000348 name: test name
showard909c7a62008-07-15 21:52:38 +0000349 time: short, medium, long
350 test_class: This describes the class for your the test belongs in.
351 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000352 test_type: Client or Server
353 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000354 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
355 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000356 i.e. if sync_count = 2 it is a sync job that requires two
357 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000358 Optional:
showard909c7a62008-07-15 21:52:38 +0000359 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000360 dependency_labels: many-to-many relationship with labels corresponding to
361 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000362 experimental: If this is set to True production servers will ignore the test
363 run_verify: Whether or not the scheduler should run the verify stage
jadmanski0afbb632008-06-06 21:10:57 +0000364 """
showard909c7a62008-07-15 21:52:38 +0000365 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
jadmanski0afbb632008-06-06 21:10:57 +0000366 # TODO(showard) - this should be merged with Job.ControlType (but right
367 # now they use opposite values)
368 Types = enum.Enum('Client', 'Server', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000369
jadmanski0afbb632008-06-06 21:10:57 +0000370 name = dbmodels.CharField(maxlength=255, unique=True)
showard909c7a62008-07-15 21:52:38 +0000371 author = dbmodels.CharField(maxlength=255)
372 test_class = dbmodels.CharField(maxlength=255)
373 test_category = dbmodels.CharField(maxlength=255)
374 dependencies = dbmodels.CharField(maxlength=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000375 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000376 experimental = dbmodels.BooleanField(default=True)
377 run_verify = dbmodels.BooleanField(default=True)
378 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
379 default=TestTime.MEDIUM)
jadmanski0afbb632008-06-06 21:10:57 +0000380 test_type = dbmodels.SmallIntegerField(choices=Types.choices())
showard909c7a62008-07-15 21:52:38 +0000381 sync_count = dbmodels.IntegerField(default=1)
showard909c7a62008-07-15 21:52:38 +0000382 path = dbmodels.CharField(maxlength=255, unique=True)
showard989f25d2008-10-01 11:38:11 +0000383 dependency_labels = dbmodels.ManyToManyField(
384 Label, blank=True, filter_interface=dbmodels.HORIZONTAL)
mblighe8819cd2008-02-15 16:48:40 +0000385
jadmanski0afbb632008-06-06 21:10:57 +0000386 name_field = 'name'
387 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000388
389
jadmanski0afbb632008-06-06 21:10:57 +0000390 class Meta:
391 db_table = 'autotests'
mblighe8819cd2008-02-15 16:48:40 +0000392
jadmanski0afbb632008-06-06 21:10:57 +0000393 class Admin:
394 fields = (
395 (None, {'fields' :
showard909c7a62008-07-15 21:52:38 +0000396 ('name', 'author', 'test_category', 'test_class',
showard2bab8f42008-11-12 18:15:22 +0000397 'test_time', 'sync_count', 'test_type', 'sync_count',
showard909c7a62008-07-15 21:52:38 +0000398 'path', 'dependencies', 'experimental', 'run_verify',
399 'description')}),
jadmanski0afbb632008-06-06 21:10:57 +0000400 )
showard2bab8f42008-11-12 18:15:22 +0000401 list_display = ('name', 'test_type', 'description', 'sync_count')
jadmanski0afbb632008-06-06 21:10:57 +0000402 search_fields = ('name',)
mblighe8819cd2008-02-15 16:48:40 +0000403
jadmanski0afbb632008-06-06 21:10:57 +0000404 def __str__(self):
405 return self.name
mblighe8819cd2008-02-15 16:48:40 +0000406
407
showard2b9a88b2008-06-13 20:55:03 +0000408class Profiler(dbmodels.Model, model_logic.ModelExtensions):
409 """\
410 Required:
411 name: profiler name
412 test_type: Client or Server
413
414 Optional:
415 description: arbirary text description
416 """
417 name = dbmodels.CharField(maxlength=255, unique=True)
418 description = dbmodels.TextField(blank=True)
419
420 name_field = 'name'
421 objects = model_logic.ExtendedManager()
422
423
424 class Meta:
425 db_table = 'profilers'
426
427 class Admin:
428 list_display = ('name', 'description')
429 search_fields = ('name',)
430
431 def __str__(self):
432 return self.name
433
434
showard7c785282008-05-29 19:45:12 +0000435class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000436 """\
437 Required:
438 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000439
jadmanski0afbb632008-06-06 21:10:57 +0000440 Optional:
441 description: arbitrary description of group
442 """
443 name = dbmodels.CharField(maxlength=255, unique=True)
444 description = dbmodels.CharField(maxlength=255, blank=True)
showard04f2cd82008-07-25 20:53:31 +0000445 users = dbmodels.ManyToManyField(User, blank=True,
jadmanski0afbb632008-06-06 21:10:57 +0000446 filter_interface=dbmodels.HORIZONTAL)
447 hosts = dbmodels.ManyToManyField(Host,
448 filter_interface=dbmodels.HORIZONTAL)
mblighe8819cd2008-02-15 16:48:40 +0000449
jadmanski0afbb632008-06-06 21:10:57 +0000450 name_field = 'name'
451 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000452
showard08f981b2008-06-24 21:59:03 +0000453 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000454 def check_for_acl_violation_hosts(hosts):
455 user = thread_local.get_user()
456 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000457 return
showard3dd47c22008-07-10 00:41:36 +0000458 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000459 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000460 for host in hosts:
461 # Check if the user has access to this host,
462 # but only if it is not a metahost
463 if (isinstance(host, Host)
464 and int(host.id) not in accessible_host_ids):
465 raise AclAccessViolation("You do not have access to %s"
466 % str(host))
467
showard9dbdcda2008-10-14 17:34:36 +0000468
469 @staticmethod
showarddc817512008-11-12 18:16:41 +0000470 def check_abort_permissions(queue_entries):
471 """
472 look for queue entries that aren't abortable, meaning
473 * the job isn't owned by this user, and
474 * the machine isn't ACL-accessible, or
475 * the machine is in the "Everyone" ACL
476 """
showard9dbdcda2008-10-14 17:34:36 +0000477 user = thread_local.get_user()
478 if user.is_superuser():
479 return
showarddc817512008-11-12 18:16:41 +0000480 not_owned = queue_entries.exclude(job__owner=user.login)
481 # I do this using ID sets instead of just Django filters because
482 # filtering on M2M fields is broken in Django 0.96. It's better in 1.0.
483 accessible_ids = set(
484 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000485 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000486 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000487 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000488 cannot_abort = [entry for entry in not_owned.select_related()
489 if entry.id not in accessible_ids
490 or entry.id in public_ids]
491 if len(cannot_abort) == 0:
492 return
493 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000494 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000495 for entry in cannot_abort)
496 raise AclAccessViolation('You cannot abort the following job entries: '
497 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000498
499
showard3dd47c22008-07-10 00:41:36 +0000500 def check_for_acl_violation_acl_group(self):
501 user = thread_local.get_user()
502 if user.is_superuser():
503 return None
504 if not user in self.users.all():
505 raise AclAccessViolation("You do not have access to %s"
506 % self.name)
507
508 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000509 def on_host_membership_change():
510 everyone = AclGroup.objects.get(name='Everyone')
511
showard3dd47c22008-07-10 00:41:36 +0000512 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000513 # TODO(showard): this is a bit of a hack, since the fact that this query
514 # works is kind of a coincidence of Django internals. This trick
515 # doesn't work in general (on all foreign key relationships). I'll
516 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000517 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000518 everyone.hosts.add(*orphaned_hosts.distinct())
519
520 # find hosts in both Everyone and another ACL group, and remove them
521 # from Everyone
522 hosts_in_everyone = Host.valid_objects.filter_custom_join(
showardd9ac4452009-02-07 02:04:37 +0000523 '_everyone', aclgroup__name='Everyone')
524 acled_hosts = hosts_in_everyone.exclude(aclgroup__name='Everyone')
showard08f981b2008-06-24 21:59:03 +0000525 everyone.hosts.remove(*acled_hosts.distinct())
526
527
528 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000529 if (self.name == 'Everyone'):
530 raise AclAccessViolation("You cannot delete 'Everyone'!")
531 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000532 super(AclGroup, self).delete()
533 self.on_host_membership_change()
534
535
showard04f2cd82008-07-25 20:53:31 +0000536 def add_current_user_if_empty(self):
537 if not self.users.count():
538 self.users.add(thread_local.get_user())
539
540
showard08f981b2008-06-24 21:59:03 +0000541 # if you have a model attribute called "Manipulator", Django will
542 # automatically insert it into the beginning of the superclass list
543 # for the model's manipulators
544 class Manipulator(object):
545 """
546 Custom manipulator to get notification when ACLs are changed through
547 the admin interface.
548 """
549 def save(self, new_data):
showard3dd47c22008-07-10 00:41:36 +0000550 user = thread_local.get_user()
showard31c570e2008-07-23 19:29:17 +0000551 if hasattr(self, 'original_object'):
552 if (not user.is_superuser()
553 and self.original_object.name == 'Everyone'):
554 raise AclAccessViolation("You cannot modify 'Everyone'!")
555 self.original_object.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000556 obj = super(AclGroup.Manipulator, self).save(new_data)
showard04f2cd82008-07-25 20:53:31 +0000557 if not hasattr(self, 'original_object'):
558 obj.users.add(thread_local.get_user())
559 obj.add_current_user_if_empty()
showard08f981b2008-06-24 21:59:03 +0000560 obj.on_host_membership_change()
561 return obj
showardeb3be4d2008-04-21 20:59:26 +0000562
jadmanski0afbb632008-06-06 21:10:57 +0000563 class Meta:
564 db_table = 'acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000565
jadmanski0afbb632008-06-06 21:10:57 +0000566 class Admin:
567 list_display = ('name', 'description')
568 search_fields = ('name',)
mblighe8819cd2008-02-15 16:48:40 +0000569
jadmanski0afbb632008-06-06 21:10:57 +0000570 def __str__(self):
571 return self.name
mblighe8819cd2008-02-15 16:48:40 +0000572
mblighe8819cd2008-02-15 16:48:40 +0000573
showard7c785282008-05-29 19:45:12 +0000574class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +0000575 'Custom manager to provide efficient status counts querying.'
576 def get_status_counts(self, job_ids):
577 """\
578 Returns a dictionary mapping the given job IDs to their status
579 count dictionaries.
580 """
581 if not job_ids:
582 return {}
583 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
584 cursor = connection.cursor()
585 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +0000586 SELECT job_id, status, aborted, complete, COUNT(*)
jadmanski0afbb632008-06-06 21:10:57 +0000587 FROM host_queue_entries
588 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +0000589 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +0000590 """ % id_list)
591 all_job_counts = {}
592 for job_id in job_ids:
593 all_job_counts[job_id] = {}
showardd3dc1992009-04-22 21:01:40 +0000594 for job_id, status, aborted, complete, count in cursor.fetchall():
595 full_status = HostQueueEntry.compute_full_status(status, aborted,
596 complete)
597 all_job_counts[job_id][full_status] = count
jadmanski0afbb632008-06-06 21:10:57 +0000598 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +0000599
600
showard989f25d2008-10-01 11:38:11 +0000601 def populate_dependencies(self, jobs):
showard9a1f2e12008-10-02 11:14:29 +0000602 if not jobs:
603 return
showard989f25d2008-10-01 11:38:11 +0000604 job_ids = ','.join(str(job['id']) for job in jobs)
605 cursor = connection.cursor()
606 cursor.execute("""
showard56e93772008-10-06 10:06:22 +0000607 SELECT jobs.id, labels.name
showard989f25d2008-10-01 11:38:11 +0000608 FROM jobs
609 INNER JOIN jobs_dependency_labels
610 ON jobs.id = jobs_dependency_labels.job_id
611 INNER JOIN labels ON jobs_dependency_labels.label_id = labels.id
612 WHERE jobs.id IN (%s)
showard989f25d2008-10-01 11:38:11 +0000613 """ % job_ids)
showard56e93772008-10-06 10:06:22 +0000614 job_dependencies = {}
615 for job_id, dependency in cursor.fetchall():
616 job_dependencies.setdefault(job_id, []).append(dependency)
showard989f25d2008-10-01 11:38:11 +0000617 for job in jobs:
showard56e93772008-10-06 10:06:22 +0000618 dependencies = ','.join(job_dependencies.get(job['id'], []))
619 job['dependencies'] = dependencies
showard989f25d2008-10-01 11:38:11 +0000620
621
showard7c785282008-05-29 19:45:12 +0000622class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000623 """\
624 owner: username of job owner
625 name: job name (does not have to be unique)
626 priority: Low, Medium, High, Urgent (or 0-3)
627 control_file: contents of control file
628 control_type: Client or Server
629 created_on: date of job creation
630 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +0000631 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +0000632 run_verify: Whether or not to run the verify phase
showard3bb499f2008-07-03 19:42:20 +0000633 timeout: hours until job times out
showard542e8402008-09-19 20:16:18 +0000634 email_list: list of people to email on completion delimited by any of:
635 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +0000636 dependency_labels: many-to-many relationship with labels corresponding to
637 job dependencies
showard21baa452008-10-21 00:08:39 +0000638 reboot_before: Never, If dirty, or Always
639 reboot_after: Never, If all tests passed, or Always
jadmanski0afbb632008-06-06 21:10:57 +0000640 """
showardb1e51872008-10-07 11:08:18 +0000641 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
642 'AUTOTEST_WEB', 'job_timeout_default', default=240)
643
jadmanski0afbb632008-06-06 21:10:57 +0000644 Priority = enum.Enum('Low', 'Medium', 'High', 'Urgent')
645 ControlType = enum.Enum('Server', 'Client', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000646
jadmanski0afbb632008-06-06 21:10:57 +0000647 owner = dbmodels.CharField(maxlength=255)
648 name = dbmodels.CharField(maxlength=255)
649 priority = dbmodels.SmallIntegerField(choices=Priority.choices(),
650 blank=True, # to allow 0
651 default=Priority.MEDIUM)
652 control_file = dbmodels.TextField()
653 control_type = dbmodels.SmallIntegerField(choices=ControlType.choices(),
showardb1e51872008-10-07 11:08:18 +0000654 blank=True, # to allow 0
655 default=ControlType.CLIENT)
showard68c7aa02008-10-09 16:49:11 +0000656 created_on = dbmodels.DateTimeField()
showard2bab8f42008-11-12 18:15:22 +0000657 synch_count = dbmodels.IntegerField(null=True, default=1)
showardb1e51872008-10-07 11:08:18 +0000658 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
showard9976ce92008-10-15 20:28:13 +0000659 run_verify = dbmodels.BooleanField(default=True)
showard542e8402008-09-19 20:16:18 +0000660 email_list = dbmodels.CharField(maxlength=250, blank=True)
showard989f25d2008-10-01 11:38:11 +0000661 dependency_labels = dbmodels.ManyToManyField(
662 Label, blank=True, filter_interface=dbmodels.HORIZONTAL)
showard21baa452008-10-21 00:08:39 +0000663 reboot_before = dbmodels.SmallIntegerField(choices=RebootBefore.choices(),
664 blank=True,
showard0fc38302008-10-23 00:44:07 +0000665 default=DEFAULT_REBOOT_BEFORE)
showard21baa452008-10-21 00:08:39 +0000666 reboot_after = dbmodels.SmallIntegerField(choices=RebootAfter.choices(),
667 blank=True,
showard0fc38302008-10-23 00:44:07 +0000668 default=DEFAULT_REBOOT_AFTER)
mblighe8819cd2008-02-15 16:48:40 +0000669
670
jadmanski0afbb632008-06-06 21:10:57 +0000671 # custom manager
672 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +0000673
674
jadmanski0afbb632008-06-06 21:10:57 +0000675 def is_server_job(self):
676 return self.control_type == self.ControlType.SERVER
mblighe8819cd2008-02-15 16:48:40 +0000677
678
jadmanski0afbb632008-06-06 21:10:57 +0000679 @classmethod
680 def create(cls, owner, name, priority, control_file, control_type,
showard2bab8f42008-11-12 18:15:22 +0000681 hosts, synch_count, timeout, run_verify, email_list,
showard21baa452008-10-21 00:08:39 +0000682 dependencies, reboot_before, reboot_after):
jadmanski0afbb632008-06-06 21:10:57 +0000683 """\
684 Creates a job by taking some information (the listed args)
685 and filling in the rest of the necessary information.
686 """
showard3dd47c22008-07-10 00:41:36 +0000687 AclGroup.check_for_acl_violation_hosts(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000688 job = cls.add_object(
689 owner=owner, name=name, priority=priority,
690 control_file=control_file, control_type=control_type,
showard2bab8f42008-11-12 18:15:22 +0000691 synch_count=synch_count, timeout=timeout,
showard68c7aa02008-10-09 16:49:11 +0000692 run_verify=run_verify, email_list=email_list,
showard21baa452008-10-21 00:08:39 +0000693 reboot_before=reboot_before, reboot_after=reboot_after,
showard68c7aa02008-10-09 16:49:11 +0000694 created_on=datetime.now())
mblighe8819cd2008-02-15 16:48:40 +0000695
showard989f25d2008-10-01 11:38:11 +0000696 job.dependency_labels = dependencies
jadmanski0afbb632008-06-06 21:10:57 +0000697 return job
mblighe8819cd2008-02-15 16:48:40 +0000698
699
showardc92da832009-04-07 18:14:34 +0000700 def queue(self, hosts, atomic_group=None):
701 """Enqueue a job on the given hosts."""
702 if atomic_group and not hosts:
703 # No hosts or labels are required to queue an atomic group
704 # Job. However, if they are given, we respect them below.
705 atomic_group.enqueue_job(self)
jadmanski0afbb632008-06-06 21:10:57 +0000706 for host in hosts:
showardc92da832009-04-07 18:14:34 +0000707 host.enqueue_job(self, atomic_group=atomic_group)
mblighe8819cd2008-02-15 16:48:40 +0000708
709
jadmanski0afbb632008-06-06 21:10:57 +0000710 def user(self):
711 try:
712 return User.objects.get(login=self.owner)
713 except self.DoesNotExist:
714 return None
mblighe8819cd2008-02-15 16:48:40 +0000715
716
showard98863972008-10-29 21:14:56 +0000717 def abort(self, aborted_by):
718 for queue_entry in self.hostqueueentry_set.all():
719 queue_entry.abort(aborted_by)
720
721
jadmanski0afbb632008-06-06 21:10:57 +0000722 class Meta:
723 db_table = 'jobs'
mblighe8819cd2008-02-15 16:48:40 +0000724
jadmanski0afbb632008-06-06 21:10:57 +0000725 if settings.FULL_ADMIN:
726 class Admin:
727 list_display = ('id', 'owner', 'name', 'control_type')
mblighe8819cd2008-02-15 16:48:40 +0000728
jadmanski0afbb632008-06-06 21:10:57 +0000729 def __str__(self):
730 return '%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +0000731
732
showard7c785282008-05-29 19:45:12 +0000733class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000734 job = dbmodels.ForeignKey(Job)
735 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +0000736
jadmanski0afbb632008-06-06 21:10:57 +0000737 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000738
jadmanski0afbb632008-06-06 21:10:57 +0000739 class Meta:
740 db_table = 'ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +0000741
jadmanski0afbb632008-06-06 21:10:57 +0000742 if settings.FULL_ADMIN:
743 class Admin:
744 list_display = ('id', 'job', 'host')
mblighe8819cd2008-02-15 16:48:40 +0000745
746
showard7c785282008-05-29 19:45:12 +0000747class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
showarda3ab0d52008-11-03 19:03:47 +0000748 Status = enum.Enum('Queued', 'Starting', 'Verifying', 'Pending', 'Running',
showardd3dc1992009-04-22 21:01:40 +0000749 'Gathering', 'Parsing', 'Aborted', 'Completed',
showard97aed502008-11-04 02:01:24 +0000750 'Failed', 'Stopped', string_values=True)
showard2bab8f42008-11-12 18:15:22 +0000751 ACTIVE_STATUSES = (Status.STARTING, Status.VERIFYING, Status.PENDING,
showardd3dc1992009-04-22 21:01:40 +0000752 Status.RUNNING, Status.GATHERING)
showardc85c21b2008-11-24 22:17:37 +0000753 COMPLETE_STATUSES = (Status.ABORTED, Status.COMPLETED, Status.FAILED,
754 Status.STOPPED)
showarda3ab0d52008-11-03 19:03:47 +0000755
jadmanski0afbb632008-06-06 21:10:57 +0000756 job = dbmodels.ForeignKey(Job)
757 host = dbmodels.ForeignKey(Host, blank=True, null=True)
jadmanski0afbb632008-06-06 21:10:57 +0000758 status = dbmodels.CharField(maxlength=255)
759 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
760 db_column='meta_host')
761 active = dbmodels.BooleanField(default=False)
762 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +0000763 deleted = dbmodels.BooleanField(default=False)
showard2bab8f42008-11-12 18:15:22 +0000764 execution_subdir = dbmodels.CharField(maxlength=255, blank=True, default='')
showard89f84db2009-03-12 20:39:13 +0000765 # If atomic_group is set, this is a virtual HostQueueEntry that will
766 # be expanded into many actual hosts within the group at schedule time.
767 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +0000768 aborted = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +0000769
jadmanski0afbb632008-06-06 21:10:57 +0000770 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000771
mblighe8819cd2008-02-15 16:48:40 +0000772
showard2bab8f42008-11-12 18:15:22 +0000773 def __init__(self, *args, **kwargs):
774 super(HostQueueEntry, self).__init__(*args, **kwargs)
775 self._record_attributes(['status'])
776
777
778 def save(self):
779 self._set_active_and_complete()
780 super(HostQueueEntry, self).save()
781 self._check_for_updated_attributes()
782
783
showard3f15eed2008-11-14 22:40:48 +0000784 def host_or_metahost_name(self):
785 if self.host:
786 return self.host.hostname
787 else:
788 assert self.meta_host
789 return self.meta_host.name
790
791
showard2bab8f42008-11-12 18:15:22 +0000792 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +0000793 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +0000794 self.active, self.complete = True, False
795 elif self.status in self.COMPLETE_STATUSES:
796 self.active, self.complete = False, True
797 else:
798 self.active, self.complete = False, False
799
800
801 def on_attribute_changed(self, attribute, old_value):
802 assert attribute == 'status'
showard67831ae2009-01-16 03:07:38 +0000803 logger.info('%s/%d (%d) -> %s' % (self.host, self.job.id, self.id,
showard2bab8f42008-11-12 18:15:22 +0000804 self.status))
805
806
jadmanski0afbb632008-06-06 21:10:57 +0000807 def is_meta_host_entry(self):
808 'True if this is a entry has a meta_host instead of a host.'
809 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +0000810
showarda3ab0d52008-11-03 19:03:47 +0000811
showard4c119042008-09-29 19:16:18 +0000812 def log_abort(self, user):
showard98863972008-10-29 21:14:56 +0000813 if user is None:
814 # automatic system abort (i.e. job timeout)
815 return
showard4c119042008-09-29 19:16:18 +0000816 abort_log = AbortedHostQueueEntry(queue_entry=self, aborted_by=user)
817 abort_log.save()
818
showarda3ab0d52008-11-03 19:03:47 +0000819
showard4c119042008-09-29 19:16:18 +0000820 def abort(self, user):
showarda3ab0d52008-11-03 19:03:47 +0000821 # this isn't completely immune to race conditions since it's not atomic,
822 # but it should be safe given the scheduler's behavior.
showardd3dc1992009-04-22 21:01:40 +0000823 if not self.complete and not self.aborted:
showard4c119042008-09-29 19:16:18 +0000824 self.log_abort(user)
showardd3dc1992009-04-22 21:01:40 +0000825 self.aborted = True
showarda3ab0d52008-11-03 19:03:47 +0000826 self.save()
mblighe8819cd2008-02-15 16:48:40 +0000827
showardd3dc1992009-04-22 21:01:40 +0000828
829 @classmethod
830 def compute_full_status(cls, status, aborted, complete):
831 if aborted and not complete:
832 return 'Aborted (%s)' % status
833 return status
834
835
836 def full_status(self):
837 return self.compute_full_status(self.status, self.aborted,
838 self.complete)
839
840
841 def _postprocess_object_dict(self, object_dict):
842 object_dict['full_status'] = self.full_status()
843
844
jadmanski0afbb632008-06-06 21:10:57 +0000845 class Meta:
846 db_table = 'host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +0000847
jadmanski0afbb632008-06-06 21:10:57 +0000848 if settings.FULL_ADMIN:
849 class Admin:
850 list_display = ('id', 'job', 'host', 'status',
851 'meta_host')
showard4c119042008-09-29 19:16:18 +0000852
853
854class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
855 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
856 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +0000857 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +0000858
859 objects = model_logic.ExtendedManager()
860
showard68c7aa02008-10-09 16:49:11 +0000861
862 def save(self):
863 self.aborted_on = datetime.now()
864 super(AbortedHostQueueEntry, self).save()
865
showard4c119042008-09-29 19:16:18 +0000866 class Meta:
867 db_table = 'aborted_host_queue_entries'