blob: daede906620d7839119a43e822ee2168d66abc07 [file] [log] [blame]
showardfb2a7fa2008-07-17 17:04:12 +00001from datetime import datetime
showard7c785282008-05-29 19:45:12 +00002from django.db import models as dbmodels, connection
showarddf062562008-07-03 19:56:37 +00003from frontend.afe import model_logic
showard3dd47c22008-07-10 00:41:36 +00004from frontend import settings, thread_local
showardb1e51872008-10-07 11:08:18 +00005from autotest_lib.client.common_lib import enum, host_protections, global_config
mblighe8819cd2008-02-15 16:48:40 +00006
showard0fc38302008-10-23 00:44:07 +00007# job options and user preferences
8RebootBefore = enum.Enum('Never', 'If dirty', 'Always')
9DEFAULT_REBOOT_BEFORE = RebootBefore.IF_DIRTY
10RebootAfter = enum.Enum('Never', 'If all tests passed', 'Always')
11DEFAULT_REBOOT_AFTER = RebootBefore.ALWAYS
mblighe8819cd2008-02-15 16:48:40 +000012
mblighe8819cd2008-02-15 16:48:40 +000013class AclAccessViolation(Exception):
jadmanski0afbb632008-06-06 21:10:57 +000014 """\
15 Raised when an operation is attempted with proper permissions as
16 dictated by ACLs.
17 """
mblighe8819cd2008-02-15 16:48:40 +000018
19
showard7c785282008-05-29 19:45:12 +000020class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000021 """\
22 Required:
23 name: label name
mblighe8819cd2008-02-15 16:48:40 +000024
jadmanski0afbb632008-06-06 21:10:57 +000025 Optional:
26 kernel_config: url/path to kernel config to use for jobs run on this
27 label
28 platform: if True, this is a platform label (defaults to False)
showard989f25d2008-10-01 11:38:11 +000029 only_if_needed: if True, a machine with this label can only be used if that
30 that label is requested by the job/test.
jadmanski0afbb632008-06-06 21:10:57 +000031 """
32 name = dbmodels.CharField(maxlength=255, unique=True)
33 kernel_config = dbmodels.CharField(maxlength=255, blank=True)
34 platform = dbmodels.BooleanField(default=False)
35 invalid = dbmodels.BooleanField(default=False,
36 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +000037 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +000038
jadmanski0afbb632008-06-06 21:10:57 +000039 name_field = 'name'
40 objects = model_logic.ExtendedManager()
41 valid_objects = model_logic.ValidObjectsManager()
mbligh5244cbb2008-04-24 20:39:52 +000042
jadmanski0afbb632008-06-06 21:10:57 +000043 def clean_object(self):
44 self.host_set.clear()
mblighe8819cd2008-02-15 16:48:40 +000045
46
jadmanski0afbb632008-06-06 21:10:57 +000047 def enqueue_job(self, job):
48 'Enqueue a job on any host of this label.'
49 queue_entry = HostQueueEntry(meta_host=self, job=job,
50 status=Job.Status.QUEUED,
51 priority=job.priority)
52 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +000053
54
jadmanski0afbb632008-06-06 21:10:57 +000055 class Meta:
56 db_table = 'labels'
mblighe8819cd2008-02-15 16:48:40 +000057
jadmanski0afbb632008-06-06 21:10:57 +000058 class Admin:
59 list_display = ('name', 'kernel_config')
60 # see Host.Admin
61 manager = model_logic.ValidObjectsManager()
mblighe8819cd2008-02-15 16:48:40 +000062
jadmanski0afbb632008-06-06 21:10:57 +000063 def __str__(self):
64 return self.name
mblighe8819cd2008-02-15 16:48:40 +000065
66
showardfb2a7fa2008-07-17 17:04:12 +000067class User(dbmodels.Model, model_logic.ModelExtensions):
68 """\
69 Required:
70 login :user login name
71
72 Optional:
73 access_level: 0=User (default), 1=Admin, 100=Root
74 """
75 ACCESS_ROOT = 100
76 ACCESS_ADMIN = 1
77 ACCESS_USER = 0
78
79 login = dbmodels.CharField(maxlength=255, unique=True)
80 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
81
showard0fc38302008-10-23 00:44:07 +000082 # user preferences
83 reboot_before = dbmodels.SmallIntegerField(choices=RebootBefore.choices(),
84 blank=True,
85 default=DEFAULT_REBOOT_BEFORE)
86 reboot_after = dbmodels.SmallIntegerField(choices=RebootAfter.choices(),
87 blank=True,
88 default=DEFAULT_REBOOT_AFTER)
89
showardfb2a7fa2008-07-17 17:04:12 +000090 name_field = 'login'
91 objects = model_logic.ExtendedManager()
92
93
94 def save(self):
95 # is this a new object being saved for the first time?
96 first_time = (self.id is None)
97 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +000098 if user and not user.is_superuser() and user.login != self.login:
99 raise AclAccessViolation("You cannot modify user " + self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000100 super(User, self).save()
101 if first_time:
102 everyone = AclGroup.objects.get(name='Everyone')
103 everyone.users.add(self)
104
105
106 def is_superuser(self):
107 return self.access_level >= self.ACCESS_ROOT
108
109
110 class Meta:
111 db_table = 'users'
112
113 class Admin:
114 list_display = ('login', 'access_level')
115 search_fields = ('login',)
116
117 def __str__(self):
118 return self.login
119
120
showard7c785282008-05-29 19:45:12 +0000121class Host(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +0000122 """\
123 Required:
124 hostname
mblighe8819cd2008-02-15 16:48:40 +0000125
jadmanski0afbb632008-06-06 21:10:57 +0000126 optional:
showard21baa452008-10-21 00:08:39 +0000127 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000128
jadmanski0afbb632008-06-06 21:10:57 +0000129 Internal:
130 synch_id: currently unused
131 status: string describing status of host
showard21baa452008-10-21 00:08:39 +0000132 invalid: true if the host has been deleted
133 protection: indicates what can be done to this host during repair
134 locked_by: user that locked the host, or null if the host is unlocked
135 lock_time: DateTime at which the host was locked
136 dirty: true if the host has been used without being rebooted
jadmanski0afbb632008-06-06 21:10:57 +0000137 """
138 Status = enum.Enum('Verifying', 'Running', 'Ready', 'Repairing',
showard45ae8192008-11-05 19:32:53 +0000139 'Repair Failed', 'Dead', 'Cleaning', 'Pending',
jadmanski0afbb632008-06-06 21:10:57 +0000140 string_values=True)
mblighe8819cd2008-02-15 16:48:40 +0000141
jadmanski0afbb632008-06-06 21:10:57 +0000142 hostname = dbmodels.CharField(maxlength=255, unique=True)
143 labels = dbmodels.ManyToManyField(Label, blank=True,
144 filter_interface=dbmodels.HORIZONTAL)
145 locked = dbmodels.BooleanField(default=False)
146 synch_id = dbmodels.IntegerField(blank=True, null=True,
147 editable=settings.FULL_ADMIN)
148 status = dbmodels.CharField(maxlength=255, default=Status.READY,
149 choices=Status.choices(),
150 editable=settings.FULL_ADMIN)
151 invalid = dbmodels.BooleanField(default=False,
152 editable=settings.FULL_ADMIN)
showardd5afc2f2008-08-12 17:19:44 +0000153 protection = dbmodels.SmallIntegerField(null=False, blank=True,
showarddf062562008-07-03 19:56:37 +0000154 choices=host_protections.choices,
155 default=host_protections.default)
showardfb2a7fa2008-07-17 17:04:12 +0000156 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
157 lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False)
showard21baa452008-10-21 00:08:39 +0000158 dirty = dbmodels.BooleanField(default=True, editable=settings.FULL_ADMIN)
mblighe8819cd2008-02-15 16:48:40 +0000159
jadmanski0afbb632008-06-06 21:10:57 +0000160 name_field = 'hostname'
161 objects = model_logic.ExtendedManager()
162 valid_objects = model_logic.ValidObjectsManager()
mbligh5244cbb2008-04-24 20:39:52 +0000163
showardb8471e32008-07-03 19:51:08 +0000164 @staticmethod
165 def create_one_time_host(hostname):
166 query = Host.objects.filter(hostname=hostname)
167 if query.count() == 0:
168 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000169 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000170 else:
171 host = query[0]
172 if not host.invalid:
173 raise model_logic.ValidationError({
174 'hostname' : '%s already exists!' % hostname
175 })
176 host.clean_object()
showarde65d2af2008-07-30 23:34:21 +0000177 AclGroup.objects.get(name='Everyone').hosts.add(host)
showardb8471e32008-07-03 19:51:08 +0000178 host.status = Host.Status.READY
showard1ab512b2008-07-30 23:39:04 +0000179 host.protection = host_protections.Protection.DO_NOT_REPAIR
showardb8471e32008-07-03 19:51:08 +0000180 host.save()
181 return host
mbligh5244cbb2008-04-24 20:39:52 +0000182
jadmanski0afbb632008-06-06 21:10:57 +0000183 def clean_object(self):
184 self.aclgroup_set.clear()
185 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000186
187
jadmanski0afbb632008-06-06 21:10:57 +0000188 def save(self):
189 # extra spaces in the hostname can be a sneaky source of errors
190 self.hostname = self.hostname.strip()
191 # is this a new object being saved for the first time?
192 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000193 if not first_time:
194 AclGroup.check_for_acl_violation_hosts([self])
showardfb2a7fa2008-07-17 17:04:12 +0000195 if self.locked and not self.locked_by:
196 self.locked_by = thread_local.get_user()
197 self.lock_time = datetime.now()
showard21baa452008-10-21 00:08:39 +0000198 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000199 elif not self.locked and self.locked_by:
200 self.locked_by = None
201 self.lock_time = None
jadmanski0afbb632008-06-06 21:10:57 +0000202 super(Host, self).save()
203 if first_time:
204 everyone = AclGroup.objects.get(name='Everyone')
205 everyone.hosts.add(self)
mblighe8819cd2008-02-15 16:48:40 +0000206
showardb8471e32008-07-03 19:51:08 +0000207 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000208 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000209 for queue_entry in self.hostqueueentry_set.all():
210 queue_entry.deleted = True
showard9a1f2e12008-10-02 11:14:29 +0000211 queue_entry.abort(thread_local.get_user())
showardb8471e32008-07-03 19:51:08 +0000212 super(Host, self).delete()
213
mblighe8819cd2008-02-15 16:48:40 +0000214
jadmanski0afbb632008-06-06 21:10:57 +0000215 def enqueue_job(self, job):
216 ' Enqueue a job on this host.'
217 queue_entry = HostQueueEntry(host=self, job=job,
218 status=Job.Status.QUEUED,
219 priority=job.priority)
220 # allow recovery of dead hosts from the frontend
221 if not self.active_queue_entry() and self.is_dead():
222 self.status = Host.Status.READY
223 self.save()
224 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000225
showard08f981b2008-06-24 21:59:03 +0000226 block = IneligibleHostQueue(job=job, host=self)
227 block.save()
228
mblighe8819cd2008-02-15 16:48:40 +0000229
jadmanski0afbb632008-06-06 21:10:57 +0000230 def platform(self):
231 # TODO(showard): slighly hacky?
232 platforms = self.labels.filter(platform=True)
233 if len(platforms) == 0:
234 return None
235 return platforms[0]
236 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000237
238
jadmanski0afbb632008-06-06 21:10:57 +0000239 def is_dead(self):
240 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000241
242
jadmanski0afbb632008-06-06 21:10:57 +0000243 def active_queue_entry(self):
244 active = list(self.hostqueueentry_set.filter(active=True))
245 if not active:
246 return None
247 assert len(active) == 1, ('More than one active entry for '
248 'host ' + self.hostname)
249 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000250
251
jadmanski0afbb632008-06-06 21:10:57 +0000252 class Meta:
253 db_table = 'hosts'
mblighe8819cd2008-02-15 16:48:40 +0000254
jadmanski0afbb632008-06-06 21:10:57 +0000255 class Admin:
256 # TODO(showard) - showing platform requires a SQL query for
257 # each row (since labels are many-to-many) - should we remove
258 # it?
259 list_display = ('hostname', 'platform', 'locked', 'status')
showarddf062562008-07-03 19:56:37 +0000260 list_filter = ('labels', 'locked', 'protection')
jadmanski0afbb632008-06-06 21:10:57 +0000261 search_fields = ('hostname', 'status')
262 # undocumented Django feature - if you set manager here, the
263 # admin code will use it, otherwise it'll use a default Manager
264 manager = model_logic.ValidObjectsManager()
mblighe8819cd2008-02-15 16:48:40 +0000265
jadmanski0afbb632008-06-06 21:10:57 +0000266 def __str__(self):
267 return self.hostname
mblighe8819cd2008-02-15 16:48:40 +0000268
269
showard7c785282008-05-29 19:45:12 +0000270class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000271 """\
272 Required:
showard909c7a62008-07-15 21:52:38 +0000273 author: author name
274 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000275 name: test name
showard909c7a62008-07-15 21:52:38 +0000276 time: short, medium, long
277 test_class: This describes the class for your the test belongs in.
278 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000279 test_type: Client or Server
280 path: path to pass to run_test()
281 synch_type: whether the test should run synchronously or asynchronously
showard909c7a62008-07-15 21:52:38 +0000282 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
283 async job. If it's >1 it's sync job for that number of machines
284 i.e. if sync_count = 2 it is a sync job that requires two
285 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000286 Optional:
showard909c7a62008-07-15 21:52:38 +0000287 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000288 dependency_labels: many-to-many relationship with labels corresponding to
289 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000290 experimental: If this is set to True production servers will ignore the test
291 run_verify: Whether or not the scheduler should run the verify stage
jadmanski0afbb632008-06-06 21:10:57 +0000292 """
showard909c7a62008-07-15 21:52:38 +0000293 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
jadmanski0afbb632008-06-06 21:10:57 +0000294 SynchType = enum.Enum('Asynchronous', 'Synchronous', start_value=1)
295 # TODO(showard) - this should be merged with Job.ControlType (but right
296 # now they use opposite values)
297 Types = enum.Enum('Client', 'Server', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000298
jadmanski0afbb632008-06-06 21:10:57 +0000299 name = dbmodels.CharField(maxlength=255, unique=True)
showard909c7a62008-07-15 21:52:38 +0000300 author = dbmodels.CharField(maxlength=255)
301 test_class = dbmodels.CharField(maxlength=255)
302 test_category = dbmodels.CharField(maxlength=255)
303 dependencies = dbmodels.CharField(maxlength=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000304 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000305 experimental = dbmodels.BooleanField(default=True)
306 run_verify = dbmodels.BooleanField(default=True)
307 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
308 default=TestTime.MEDIUM)
jadmanski0afbb632008-06-06 21:10:57 +0000309 test_type = dbmodels.SmallIntegerField(choices=Types.choices())
showard909c7a62008-07-15 21:52:38 +0000310 sync_count = dbmodels.IntegerField(default=1)
jadmanski0afbb632008-06-06 21:10:57 +0000311 synch_type = dbmodels.SmallIntegerField(choices=SynchType.choices(),
312 default=SynchType.ASYNCHRONOUS)
showard909c7a62008-07-15 21:52:38 +0000313 path = dbmodels.CharField(maxlength=255, unique=True)
showard989f25d2008-10-01 11:38:11 +0000314 dependency_labels = dbmodels.ManyToManyField(
315 Label, blank=True, filter_interface=dbmodels.HORIZONTAL)
mblighe8819cd2008-02-15 16:48:40 +0000316
jadmanski0afbb632008-06-06 21:10:57 +0000317 name_field = 'name'
318 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000319
320
jadmanski0afbb632008-06-06 21:10:57 +0000321 class Meta:
322 db_table = 'autotests'
mblighe8819cd2008-02-15 16:48:40 +0000323
jadmanski0afbb632008-06-06 21:10:57 +0000324 class Admin:
325 fields = (
326 (None, {'fields' :
showard909c7a62008-07-15 21:52:38 +0000327 ('name', 'author', 'test_category', 'test_class',
328 'test_time', 'synch_type', 'test_type', 'sync_count',
329 'path', 'dependencies', 'experimental', 'run_verify',
330 'description')}),
jadmanski0afbb632008-06-06 21:10:57 +0000331 )
showard909c7a62008-07-15 21:52:38 +0000332 list_display = ('name', 'test_type', 'description', 'synch_type')
jadmanski0afbb632008-06-06 21:10:57 +0000333 search_fields = ('name',)
mblighe8819cd2008-02-15 16:48:40 +0000334
jadmanski0afbb632008-06-06 21:10:57 +0000335 def __str__(self):
336 return self.name
mblighe8819cd2008-02-15 16:48:40 +0000337
338
showard2b9a88b2008-06-13 20:55:03 +0000339class Profiler(dbmodels.Model, model_logic.ModelExtensions):
340 """\
341 Required:
342 name: profiler name
343 test_type: Client or Server
344
345 Optional:
346 description: arbirary text description
347 """
348 name = dbmodels.CharField(maxlength=255, unique=True)
349 description = dbmodels.TextField(blank=True)
350
351 name_field = 'name'
352 objects = model_logic.ExtendedManager()
353
354
355 class Meta:
356 db_table = 'profilers'
357
358 class Admin:
359 list_display = ('name', 'description')
360 search_fields = ('name',)
361
362 def __str__(self):
363 return self.name
364
365
showard7c785282008-05-29 19:45:12 +0000366class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000367 """\
368 Required:
369 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000370
jadmanski0afbb632008-06-06 21:10:57 +0000371 Optional:
372 description: arbitrary description of group
373 """
374 name = dbmodels.CharField(maxlength=255, unique=True)
375 description = dbmodels.CharField(maxlength=255, blank=True)
showard04f2cd82008-07-25 20:53:31 +0000376 users = dbmodels.ManyToManyField(User, blank=True,
jadmanski0afbb632008-06-06 21:10:57 +0000377 filter_interface=dbmodels.HORIZONTAL)
378 hosts = dbmodels.ManyToManyField(Host,
379 filter_interface=dbmodels.HORIZONTAL)
mblighe8819cd2008-02-15 16:48:40 +0000380
jadmanski0afbb632008-06-06 21:10:57 +0000381 name_field = 'name'
382 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000383
showard08f981b2008-06-24 21:59:03 +0000384 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000385 def check_for_acl_violation_hosts(hosts):
386 user = thread_local.get_user()
387 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000388 return
showard3dd47c22008-07-10 00:41:36 +0000389 accessible_host_ids = set(
390 host.id for host in Host.objects.filter(acl_group__users=user))
391 for host in hosts:
392 # Check if the user has access to this host,
393 # but only if it is not a metahost
394 if (isinstance(host, Host)
395 and int(host.id) not in accessible_host_ids):
396 raise AclAccessViolation("You do not have access to %s"
397 % str(host))
398
showard9dbdcda2008-10-14 17:34:36 +0000399
400 @staticmethod
401 def check_for_acl_violation_queue_entries(queue_entries):
402 user = thread_local.get_user()
403 if user.is_superuser():
404 return
405 for queue_entry in queue_entries:
406 job = queue_entry.job
407 if user.login != job.owner:
408 raise AclAccessViolation('You cannot abort job %d owned by %s' %
409 (job.id, job.owner))
410
411
showard3dd47c22008-07-10 00:41:36 +0000412 def check_for_acl_violation_acl_group(self):
413 user = thread_local.get_user()
414 if user.is_superuser():
415 return None
416 if not user in self.users.all():
417 raise AclAccessViolation("You do not have access to %s"
418 % self.name)
419
420 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000421 def on_host_membership_change():
422 everyone = AclGroup.objects.get(name='Everyone')
423
showard3dd47c22008-07-10 00:41:36 +0000424 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000425 # TODO(showard): this is a bit of a hack, since the fact that this query
426 # works is kind of a coincidence of Django internals. This trick
427 # doesn't work in general (on all foreign key relationships). I'll
428 # replace it with a better technique when the need arises.
429 orphaned_hosts = Host.valid_objects.filter(acl_group__id__isnull=True)
430 everyone.hosts.add(*orphaned_hosts.distinct())
431
432 # find hosts in both Everyone and another ACL group, and remove them
433 # from Everyone
434 hosts_in_everyone = Host.valid_objects.filter_custom_join(
435 '_everyone', acl_group__name='Everyone')
436 acled_hosts = hosts_in_everyone.exclude(acl_group__name='Everyone')
437 everyone.hosts.remove(*acled_hosts.distinct())
438
439
440 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000441 if (self.name == 'Everyone'):
442 raise AclAccessViolation("You cannot delete 'Everyone'!")
443 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000444 super(AclGroup, self).delete()
445 self.on_host_membership_change()
446
447
showard04f2cd82008-07-25 20:53:31 +0000448 def add_current_user_if_empty(self):
449 if not self.users.count():
450 self.users.add(thread_local.get_user())
451
452
showard08f981b2008-06-24 21:59:03 +0000453 # if you have a model attribute called "Manipulator", Django will
454 # automatically insert it into the beginning of the superclass list
455 # for the model's manipulators
456 class Manipulator(object):
457 """
458 Custom manipulator to get notification when ACLs are changed through
459 the admin interface.
460 """
461 def save(self, new_data):
showard3dd47c22008-07-10 00:41:36 +0000462 user = thread_local.get_user()
showard31c570e2008-07-23 19:29:17 +0000463 if hasattr(self, 'original_object'):
464 if (not user.is_superuser()
465 and self.original_object.name == 'Everyone'):
466 raise AclAccessViolation("You cannot modify 'Everyone'!")
467 self.original_object.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000468 obj = super(AclGroup.Manipulator, self).save(new_data)
showard04f2cd82008-07-25 20:53:31 +0000469 if not hasattr(self, 'original_object'):
470 obj.users.add(thread_local.get_user())
471 obj.add_current_user_if_empty()
showard08f981b2008-06-24 21:59:03 +0000472 obj.on_host_membership_change()
473 return obj
showardeb3be4d2008-04-21 20:59:26 +0000474
jadmanski0afbb632008-06-06 21:10:57 +0000475 class Meta:
476 db_table = 'acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000477
jadmanski0afbb632008-06-06 21:10:57 +0000478 class Admin:
479 list_display = ('name', 'description')
480 search_fields = ('name',)
mblighe8819cd2008-02-15 16:48:40 +0000481
jadmanski0afbb632008-06-06 21:10:57 +0000482 def __str__(self):
483 return self.name
mblighe8819cd2008-02-15 16:48:40 +0000484
485# hack to make the column name in the many-to-many DB tables match the one
486# generated by ruby
487AclGroup._meta.object_name = 'acl_group'
488
489
showard7c785282008-05-29 19:45:12 +0000490class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +0000491 'Custom manager to provide efficient status counts querying.'
492 def get_status_counts(self, job_ids):
493 """\
494 Returns a dictionary mapping the given job IDs to their status
495 count dictionaries.
496 """
497 if not job_ids:
498 return {}
499 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
500 cursor = connection.cursor()
501 cursor.execute("""
502 SELECT job_id, status, COUNT(*)
503 FROM host_queue_entries
504 WHERE job_id IN %s
505 GROUP BY job_id, status
506 """ % id_list)
507 all_job_counts = {}
508 for job_id in job_ids:
509 all_job_counts[job_id] = {}
510 for job_id, status, count in cursor.fetchall():
511 all_job_counts[job_id][status] = count
512 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +0000513
514
showard989f25d2008-10-01 11:38:11 +0000515 def populate_dependencies(self, jobs):
showard9a1f2e12008-10-02 11:14:29 +0000516 if not jobs:
517 return
showard989f25d2008-10-01 11:38:11 +0000518 job_ids = ','.join(str(job['id']) for job in jobs)
519 cursor = connection.cursor()
520 cursor.execute("""
showard56e93772008-10-06 10:06:22 +0000521 SELECT jobs.id, labels.name
showard989f25d2008-10-01 11:38:11 +0000522 FROM jobs
523 INNER JOIN jobs_dependency_labels
524 ON jobs.id = jobs_dependency_labels.job_id
525 INNER JOIN labels ON jobs_dependency_labels.label_id = labels.id
526 WHERE jobs.id IN (%s)
showard989f25d2008-10-01 11:38:11 +0000527 """ % job_ids)
showard56e93772008-10-06 10:06:22 +0000528 job_dependencies = {}
529 for job_id, dependency in cursor.fetchall():
530 job_dependencies.setdefault(job_id, []).append(dependency)
showard989f25d2008-10-01 11:38:11 +0000531 for job in jobs:
showard56e93772008-10-06 10:06:22 +0000532 dependencies = ','.join(job_dependencies.get(job['id'], []))
533 job['dependencies'] = dependencies
showard989f25d2008-10-01 11:38:11 +0000534
535
showard7c785282008-05-29 19:45:12 +0000536class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000537 """\
538 owner: username of job owner
539 name: job name (does not have to be unique)
540 priority: Low, Medium, High, Urgent (or 0-3)
541 control_file: contents of control file
542 control_type: Client or Server
543 created_on: date of job creation
544 submitted_on: date of job submission
545 synch_type: Asynchronous or Synchronous (i.e. job must run on all hosts
546 simultaneously; used for server-side control files)
showard21baa452008-10-21 00:08:39 +0000547 synch_count: currently unused
showard909c7a62008-07-15 21:52:38 +0000548 run_verify: Whether or not to run the verify phase
jadmanski0afbb632008-06-06 21:10:57 +0000549 synchronizing: for scheduler use
showard3bb499f2008-07-03 19:42:20 +0000550 timeout: hours until job times out
showard542e8402008-09-19 20:16:18 +0000551 email_list: list of people to email on completion delimited by any of:
552 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +0000553 dependency_labels: many-to-many relationship with labels corresponding to
554 job dependencies
showard21baa452008-10-21 00:08:39 +0000555 reboot_before: Never, If dirty, or Always
556 reboot_after: Never, If all tests passed, or Always
jadmanski0afbb632008-06-06 21:10:57 +0000557 """
showardb1e51872008-10-07 11:08:18 +0000558 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
559 'AUTOTEST_WEB', 'job_timeout_default', default=240)
560
jadmanski0afbb632008-06-06 21:10:57 +0000561 Priority = enum.Enum('Low', 'Medium', 'High', 'Urgent')
562 ControlType = enum.Enum('Server', 'Client', start_value=1)
563 Status = enum.Enum('Created', 'Queued', 'Pending', 'Running',
564 'Completed', 'Abort', 'Aborting', 'Aborted',
showardd823b362008-07-24 16:35:46 +0000565 'Failed', 'Starting', string_values=True)
mblighe8819cd2008-02-15 16:48:40 +0000566
jadmanski0afbb632008-06-06 21:10:57 +0000567 owner = dbmodels.CharField(maxlength=255)
568 name = dbmodels.CharField(maxlength=255)
569 priority = dbmodels.SmallIntegerField(choices=Priority.choices(),
570 blank=True, # to allow 0
571 default=Priority.MEDIUM)
572 control_file = dbmodels.TextField()
573 control_type = dbmodels.SmallIntegerField(choices=ControlType.choices(),
showardb1e51872008-10-07 11:08:18 +0000574 blank=True, # to allow 0
575 default=ControlType.CLIENT)
showard68c7aa02008-10-09 16:49:11 +0000576 created_on = dbmodels.DateTimeField()
jadmanski0afbb632008-06-06 21:10:57 +0000577 synch_type = dbmodels.SmallIntegerField(
578 blank=True, null=True, choices=Test.SynchType.choices())
579 synch_count = dbmodels.IntegerField(blank=True, null=True)
580 synchronizing = dbmodels.BooleanField(default=False)
showardb1e51872008-10-07 11:08:18 +0000581 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
showard9976ce92008-10-15 20:28:13 +0000582 run_verify = dbmodels.BooleanField(default=True)
showard542e8402008-09-19 20:16:18 +0000583 email_list = dbmodels.CharField(maxlength=250, blank=True)
showard989f25d2008-10-01 11:38:11 +0000584 dependency_labels = dbmodels.ManyToManyField(
585 Label, blank=True, filter_interface=dbmodels.HORIZONTAL)
showard21baa452008-10-21 00:08:39 +0000586 reboot_before = dbmodels.SmallIntegerField(choices=RebootBefore.choices(),
587 blank=True,
showard0fc38302008-10-23 00:44:07 +0000588 default=DEFAULT_REBOOT_BEFORE)
showard21baa452008-10-21 00:08:39 +0000589 reboot_after = dbmodels.SmallIntegerField(choices=RebootAfter.choices(),
590 blank=True,
showard0fc38302008-10-23 00:44:07 +0000591 default=DEFAULT_REBOOT_AFTER)
mblighe8819cd2008-02-15 16:48:40 +0000592
593
jadmanski0afbb632008-06-06 21:10:57 +0000594 # custom manager
595 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +0000596
597
jadmanski0afbb632008-06-06 21:10:57 +0000598 def is_server_job(self):
599 return self.control_type == self.ControlType.SERVER
mblighe8819cd2008-02-15 16:48:40 +0000600
601
jadmanski0afbb632008-06-06 21:10:57 +0000602 @classmethod
603 def create(cls, owner, name, priority, control_file, control_type,
showard989f25d2008-10-01 11:38:11 +0000604 hosts, synch_type, timeout, run_verify, email_list,
showard21baa452008-10-21 00:08:39 +0000605 dependencies, reboot_before, reboot_after):
jadmanski0afbb632008-06-06 21:10:57 +0000606 """\
607 Creates a job by taking some information (the listed args)
608 and filling in the rest of the necessary information.
609 """
showard3dd47c22008-07-10 00:41:36 +0000610 AclGroup.check_for_acl_violation_hosts(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000611 job = cls.add_object(
612 owner=owner, name=name, priority=priority,
613 control_file=control_file, control_type=control_type,
showard909c7a62008-07-15 21:52:38 +0000614 synch_type=synch_type, timeout=timeout,
showard68c7aa02008-10-09 16:49:11 +0000615 run_verify=run_verify, email_list=email_list,
showard21baa452008-10-21 00:08:39 +0000616 reboot_before=reboot_before, reboot_after=reboot_after,
showard68c7aa02008-10-09 16:49:11 +0000617 created_on=datetime.now())
mblighe8819cd2008-02-15 16:48:40 +0000618
showard21baa452008-10-21 00:08:39 +0000619 if job.synch_type == Test.SynchType.ASYNCHRONOUS and len(hosts) == 0:
620 raise model_logic.ValidationError({'hosts':
621 'asynchronous jobs require at '
622 'least one host to run on'})
jadmanski0afbb632008-06-06 21:10:57 +0000623 job.save()
showard989f25d2008-10-01 11:38:11 +0000624 job.dependency_labels = dependencies
jadmanski0afbb632008-06-06 21:10:57 +0000625 return job
mblighe8819cd2008-02-15 16:48:40 +0000626
627
jadmanski0afbb632008-06-06 21:10:57 +0000628 def queue(self, hosts):
629 'Enqueue a job on the given hosts.'
630 for host in hosts:
631 host.enqueue_job(self)
mblighe8819cd2008-02-15 16:48:40 +0000632
633
jadmanski0afbb632008-06-06 21:10:57 +0000634 def user(self):
635 try:
636 return User.objects.get(login=self.owner)
637 except self.DoesNotExist:
638 return None
mblighe8819cd2008-02-15 16:48:40 +0000639
640
showard98863972008-10-29 21:14:56 +0000641 def abort(self, aborted_by):
642 for queue_entry in self.hostqueueentry_set.all():
643 queue_entry.abort(aborted_by)
644
645
jadmanski0afbb632008-06-06 21:10:57 +0000646 class Meta:
647 db_table = 'jobs'
mblighe8819cd2008-02-15 16:48:40 +0000648
jadmanski0afbb632008-06-06 21:10:57 +0000649 if settings.FULL_ADMIN:
650 class Admin:
651 list_display = ('id', 'owner', 'name', 'control_type')
mblighe8819cd2008-02-15 16:48:40 +0000652
jadmanski0afbb632008-06-06 21:10:57 +0000653 def __str__(self):
654 return '%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +0000655
656
showard7c785282008-05-29 19:45:12 +0000657class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000658 job = dbmodels.ForeignKey(Job)
659 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +0000660
jadmanski0afbb632008-06-06 21:10:57 +0000661 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000662
jadmanski0afbb632008-06-06 21:10:57 +0000663 class Meta:
664 db_table = 'ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +0000665
jadmanski0afbb632008-06-06 21:10:57 +0000666 if settings.FULL_ADMIN:
667 class Admin:
668 list_display = ('id', 'job', 'host')
mblighe8819cd2008-02-15 16:48:40 +0000669
670
showard7c785282008-05-29 19:45:12 +0000671class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
showarda3ab0d52008-11-03 19:03:47 +0000672 Status = enum.Enum('Queued', 'Starting', 'Verifying', 'Pending', 'Running',
showard97aed502008-11-04 02:01:24 +0000673 'Parsing', 'Abort', 'Aborting', 'Aborted', 'Completed',
674 'Failed', 'Stopped', string_values=True)
showarda3ab0d52008-11-03 19:03:47 +0000675 ABORT_STATUSES = (Status.ABORT, Status.ABORTING, Status.ABORTED)
676
jadmanski0afbb632008-06-06 21:10:57 +0000677 job = dbmodels.ForeignKey(Job)
678 host = dbmodels.ForeignKey(Host, blank=True, null=True)
679 priority = dbmodels.SmallIntegerField()
680 status = dbmodels.CharField(maxlength=255)
681 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
682 db_column='meta_host')
683 active = dbmodels.BooleanField(default=False)
684 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +0000685 deleted = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +0000686
jadmanski0afbb632008-06-06 21:10:57 +0000687 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000688
mblighe8819cd2008-02-15 16:48:40 +0000689
jadmanski0afbb632008-06-06 21:10:57 +0000690 def is_meta_host_entry(self):
691 'True if this is a entry has a meta_host instead of a host.'
692 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +0000693
showarda3ab0d52008-11-03 19:03:47 +0000694
showard4c119042008-09-29 19:16:18 +0000695 def log_abort(self, user):
showard98863972008-10-29 21:14:56 +0000696 if user is None:
697 # automatic system abort (i.e. job timeout)
698 return
showard4c119042008-09-29 19:16:18 +0000699 abort_log = AbortedHostQueueEntry(queue_entry=self, aborted_by=user)
700 abort_log.save()
701
showarda3ab0d52008-11-03 19:03:47 +0000702
showard4c119042008-09-29 19:16:18 +0000703 def abort(self, user):
showarda3ab0d52008-11-03 19:03:47 +0000704 # this isn't completely immune to race conditions since it's not atomic,
705 # but it should be safe given the scheduler's behavior.
706 if not self.complete and self.status not in self.ABORT_STATUSES:
showardb8471e32008-07-03 19:51:08 +0000707 self.status = Job.Status.ABORT
showard4c119042008-09-29 19:16:18 +0000708 self.log_abort(user)
showarda3ab0d52008-11-03 19:03:47 +0000709 self.save()
mblighe8819cd2008-02-15 16:48:40 +0000710
jadmanski0afbb632008-06-06 21:10:57 +0000711 class Meta:
712 db_table = 'host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +0000713
jadmanski0afbb632008-06-06 21:10:57 +0000714 if settings.FULL_ADMIN:
715 class Admin:
716 list_display = ('id', 'job', 'host', 'status',
717 'meta_host')
showard4c119042008-09-29 19:16:18 +0000718
719
720class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
721 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
722 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +0000723 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +0000724
725 objects = model_logic.ExtendedManager()
726
showard68c7aa02008-10-09 16:49:11 +0000727
728 def save(self):
729 self.aborted_on = datetime.now()
730 super(AbortedHostQueueEntry, self).save()
731
showard4c119042008-09-29 19:16:18 +0000732 class Meta:
733 db_table = 'aborted_host_queue_entries'