blob: f4a9e3192ac2004c6a9d4f3851ca44d179b0d39a [file] [log] [blame]
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001# pylint: disable-msg=C0111
2
showardd1195652009-12-08 22:21:02 +00003import logging, os
showardfb2a7fa2008-07-17 17:04:12 +00004from datetime import datetime
showard7c785282008-05-29 19:45:12 +00005from django.db import models as dbmodels, connection
jamesren35a70222010-02-16 19:30:46 +00006from xml.sax import saxutils
showardcafd16e2009-05-29 18:37:49 +00007import common
jamesrendd855242010-03-02 22:23:44 +00008from autotest_lib.frontend.afe import model_logic, model_attributes
showardcafd16e2009-05-29 18:37:49 +00009from autotest_lib.frontend import settings, thread_local
showardb1e51872008-10-07 11:08:18 +000010from autotest_lib.client.common_lib import enum, host_protections, global_config
showardeaa408e2009-09-11 18:45:31 +000011from autotest_lib.client.common_lib import host_queue_entry_states
mblighe8819cd2008-02-15 16:48:40 +000012
showard0fc38302008-10-23 00:44:07 +000013# job options and user preferences
jamesrendd855242010-03-02 22:23:44 +000014DEFAULT_REBOOT_BEFORE = model_attributes.RebootBefore.IF_DIRTY
15DEFAULT_REBOOT_AFTER = model_attributes.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:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080032 name: A name for this atomic group, e.g. 'rack23' or 'funky_net'.
showard89f84db2009-03-12 20:39:13 +000033 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 """
showarda5288b42009-07-28 20:06:08 +000040 name = dbmodels.CharField(max_length=255, unique=True)
showard89f84db2009-03-12 20:39:13 +000041 description = dbmodels.TextField(blank=True)
showarde9450c92009-06-30 01:58:52 +000042 # This magic value is the default to simplify the scheduler logic.
43 # It must be "large". The common use of atomic groups is to want all
44 # machines in the group to be used, limits on which subset used are
45 # often chosen via dependency labels.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080046 # TODO(dennisjeffrey): Revisit this so we don't have to assume that
47 # "infinity" is around 3.3 million.
showarde9450c92009-06-30 01:58:52 +000048 INFINITE_MACHINES = 333333333
49 max_number_of_machines = dbmodels.IntegerField(default=INFINITE_MACHINES)
showard205fd602009-03-21 00:17:35 +000050 invalid = dbmodels.BooleanField(default=False,
showarda5288b42009-07-28 20:06:08 +000051 editable=settings.FULL_ADMIN)
showard89f84db2009-03-12 20:39:13 +000052
showard89f84db2009-03-12 20:39:13 +000053 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +000054 objects = model_logic.ModelWithInvalidManager()
showard205fd602009-03-21 00:17:35 +000055 valid_objects = model_logic.ValidObjectsManager()
56
57
showard29f7cd22009-04-29 21:16:24 +000058 def enqueue_job(self, job, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080059 """Enqueue a job on an associated atomic group of hosts.
60
61 @param job: A job to enqueue.
62 @param is_template: Whether the status should be "Template".
63 """
showard29f7cd22009-04-29 21:16:24 +000064 queue_entry = HostQueueEntry.create(atomic_group=self, job=job,
65 is_template=is_template)
showardc92da832009-04-07 18:14:34 +000066 queue_entry.save()
67
68
showard205fd602009-03-21 00:17:35 +000069 def clean_object(self):
70 self.label_set.clear()
showard89f84db2009-03-12 20:39:13 +000071
72
73 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080074 """Metadata for class AtomicGroup."""
showardeab66ce2009-12-23 00:03:56 +000075 db_table = 'afe_atomic_groups'
showard89f84db2009-03-12 20:39:13 +000076
showard205fd602009-03-21 00:17:35 +000077
showarda5288b42009-07-28 20:06:08 +000078 def __unicode__(self):
79 return unicode(self.name)
showard89f84db2009-03-12 20:39:13 +000080
81
showard7c785282008-05-29 19:45:12 +000082class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000083 """\
84 Required:
showard89f84db2009-03-12 20:39:13 +000085 name: label name
mblighe8819cd2008-02-15 16:48:40 +000086
jadmanski0afbb632008-06-06 21:10:57 +000087 Optional:
showard89f84db2009-03-12 20:39:13 +000088 kernel_config: URL/path to kernel config for jobs run on this label.
89 platform: If True, this is a platform label (defaults to False).
90 only_if_needed: If True, a Host with this label can only be used if that
91 label is requested by the job/test (either as the meta_host or
92 in the job_dependencies).
93 atomic_group: The atomic group associated with this label.
jadmanski0afbb632008-06-06 21:10:57 +000094 """
showarda5288b42009-07-28 20:06:08 +000095 name = dbmodels.CharField(max_length=255, unique=True)
96 kernel_config = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +000097 platform = dbmodels.BooleanField(default=False)
98 invalid = dbmodels.BooleanField(default=False,
99 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +0000100 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +0000101
jadmanski0afbb632008-06-06 21:10:57 +0000102 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +0000103 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000104 valid_objects = model_logic.ValidObjectsManager()
showard89f84db2009-03-12 20:39:13 +0000105 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
106
mbligh5244cbb2008-04-24 20:39:52 +0000107
jadmanski0afbb632008-06-06 21:10:57 +0000108 def clean_object(self):
109 self.host_set.clear()
showard01a51672009-05-29 18:42:37 +0000110 self.test_set.clear()
mblighe8819cd2008-02-15 16:48:40 +0000111
112
showard29f7cd22009-04-29 21:16:24 +0000113 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800114 """Enqueue a job on any host of this label.
115
116 @param job: A job to enqueue.
117 @param atomic_group: The associated atomic group.
118 @param is_template: Whether the status should be "Template".
119 """
showard29f7cd22009-04-29 21:16:24 +0000120 queue_entry = HostQueueEntry.create(meta_host=self, job=job,
121 is_template=is_template,
122 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000123 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000124
125
jadmanski0afbb632008-06-06 21:10:57 +0000126 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800127 """Metadata for class Label."""
showardeab66ce2009-12-23 00:03:56 +0000128 db_table = 'afe_labels'
mblighe8819cd2008-02-15 16:48:40 +0000129
showarda5288b42009-07-28 20:06:08 +0000130 def __unicode__(self):
131 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000132
133
jamesren76fcf192010-04-21 20:39:50 +0000134class Drone(dbmodels.Model, model_logic.ModelExtensions):
135 """
136 A scheduler drone
137
138 hostname: the drone's hostname
139 """
140 hostname = dbmodels.CharField(max_length=255, unique=True)
141
142 name_field = 'hostname'
143 objects = model_logic.ExtendedManager()
144
145
146 def save(self, *args, **kwargs):
147 if not User.current_user().is_superuser():
148 raise Exception('Only superusers may edit drones')
149 super(Drone, self).save(*args, **kwargs)
150
151
152 def delete(self):
153 if not User.current_user().is_superuser():
154 raise Exception('Only superusers may delete drones')
155 super(Drone, self).delete()
156
157
158 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800159 """Metadata for class Drone."""
jamesren76fcf192010-04-21 20:39:50 +0000160 db_table = 'afe_drones'
161
162 def __unicode__(self):
163 return unicode(self.hostname)
164
165
166class DroneSet(dbmodels.Model, model_logic.ModelExtensions):
167 """
168 A set of scheduler drones
169
170 These will be used by the scheduler to decide what drones a job is allowed
171 to run on.
172
173 name: the drone set's name
174 drones: the drones that are part of the set
175 """
176 DRONE_SETS_ENABLED = global_config.global_config.get_config_value(
177 'SCHEDULER', 'drone_sets_enabled', type=bool, default=False)
178 DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value(
179 'SCHEDULER', 'default_drone_set_name', default=None)
180
181 name = dbmodels.CharField(max_length=255, unique=True)
182 drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones')
183
184 name_field = 'name'
185 objects = model_logic.ExtendedManager()
186
187
188 def save(self, *args, **kwargs):
189 if not User.current_user().is_superuser():
190 raise Exception('Only superusers may edit drone sets')
191 super(DroneSet, self).save(*args, **kwargs)
192
193
194 def delete(self):
195 if not User.current_user().is_superuser():
196 raise Exception('Only superusers may delete drone sets')
197 super(DroneSet, self).delete()
198
199
200 @classmethod
201 def drone_sets_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800202 """Returns whether drone sets are enabled.
203
204 @param cls: Implicit class object.
205 """
jamesren76fcf192010-04-21 20:39:50 +0000206 return cls.DRONE_SETS_ENABLED
207
208
209 @classmethod
210 def default_drone_set_name(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800211 """Returns the default drone set name.
212
213 @param cls: Implicit class object.
214 """
jamesren76fcf192010-04-21 20:39:50 +0000215 return cls.DEFAULT_DRONE_SET_NAME
216
217
218 @classmethod
219 def get_default(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800220 """Gets the default drone set name, compatible with Job.add_object.
221
222 @param cls: Implicit class object.
223 """
jamesren76fcf192010-04-21 20:39:50 +0000224 return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME)
225
226
227 @classmethod
228 def resolve_name(cls, drone_set_name):
229 """
230 Returns the name of one of these, if not None, in order of preference:
231 1) the drone set given,
232 2) the current user's default drone set, or
233 3) the global default drone set
234
235 or returns None if drone sets are disabled
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800236
237 @param cls: Implicit class object.
238 @param drone_set_name: A drone set name.
jamesren76fcf192010-04-21 20:39:50 +0000239 """
240 if not cls.drone_sets_enabled():
241 return None
242
243 user = User.current_user()
244 user_drone_set_name = user.drone_set and user.drone_set.name
245
246 return drone_set_name or user_drone_set_name or cls.get_default().name
247
248
249 def get_drone_hostnames(self):
250 """
251 Gets the hostnames of all drones in this drone set
252 """
253 return set(self.drones.all().values_list('hostname', flat=True))
254
255
256 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800257 """Metadata for class DroneSet."""
jamesren76fcf192010-04-21 20:39:50 +0000258 db_table = 'afe_drone_sets'
259
260 def __unicode__(self):
261 return unicode(self.name)
262
263
showardfb2a7fa2008-07-17 17:04:12 +0000264class User(dbmodels.Model, model_logic.ModelExtensions):
265 """\
266 Required:
267 login :user login name
268
269 Optional:
270 access_level: 0=User (default), 1=Admin, 100=Root
271 """
272 ACCESS_ROOT = 100
273 ACCESS_ADMIN = 1
274 ACCESS_USER = 0
275
showard64a95952010-01-13 21:27:16 +0000276 AUTOTEST_SYSTEM = 'autotest_system'
277
showarda5288b42009-07-28 20:06:08 +0000278 login = dbmodels.CharField(max_length=255, unique=True)
showardfb2a7fa2008-07-17 17:04:12 +0000279 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
280
showard0fc38302008-10-23 00:44:07 +0000281 # user preferences
jamesrendd855242010-03-02 22:23:44 +0000282 reboot_before = dbmodels.SmallIntegerField(
283 choices=model_attributes.RebootBefore.choices(), blank=True,
284 default=DEFAULT_REBOOT_BEFORE)
285 reboot_after = dbmodels.SmallIntegerField(
286 choices=model_attributes.RebootAfter.choices(), blank=True,
287 default=DEFAULT_REBOOT_AFTER)
jamesren76fcf192010-04-21 20:39:50 +0000288 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
showard97db5ba2008-11-12 18:18:02 +0000289 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000290
showardfb2a7fa2008-07-17 17:04:12 +0000291 name_field = 'login'
292 objects = model_logic.ExtendedManager()
293
294
showarda5288b42009-07-28 20:06:08 +0000295 def save(self, *args, **kwargs):
showardfb2a7fa2008-07-17 17:04:12 +0000296 # is this a new object being saved for the first time?
297 first_time = (self.id is None)
298 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000299 if user and not user.is_superuser() and user.login != self.login:
300 raise AclAccessViolation("You cannot modify user " + self.login)
showarda5288b42009-07-28 20:06:08 +0000301 super(User, self).save(*args, **kwargs)
showardfb2a7fa2008-07-17 17:04:12 +0000302 if first_time:
303 everyone = AclGroup.objects.get(name='Everyone')
304 everyone.users.add(self)
305
306
307 def is_superuser(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800308 """Returns whether the user has superuser access."""
showardfb2a7fa2008-07-17 17:04:12 +0000309 return self.access_level >= self.ACCESS_ROOT
310
311
showard64a95952010-01-13 21:27:16 +0000312 @classmethod
313 def current_user(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800314 """Returns the current user.
315
316 @param cls: Implicit class object.
317 """
showard64a95952010-01-13 21:27:16 +0000318 user = thread_local.get_user()
319 if user is None:
showardcfcdd802010-01-15 00:16:33 +0000320 user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM)
showard64a95952010-01-13 21:27:16 +0000321 user.access_level = cls.ACCESS_ROOT
322 user.save()
323 return user
324
325
showardfb2a7fa2008-07-17 17:04:12 +0000326 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800327 """Metadata for class User."""
showardeab66ce2009-12-23 00:03:56 +0000328 db_table = 'afe_users'
showardfb2a7fa2008-07-17 17:04:12 +0000329
showarda5288b42009-07-28 20:06:08 +0000330 def __unicode__(self):
331 return unicode(self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000332
333
showardf8b19042009-05-12 17:22:49 +0000334class Host(model_logic.ModelWithInvalid, dbmodels.Model,
335 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000336 """\
337 Required:
338 hostname
mblighe8819cd2008-02-15 16:48:40 +0000339
jadmanski0afbb632008-06-06 21:10:57 +0000340 optional:
showard21baa452008-10-21 00:08:39 +0000341 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000342
jadmanski0afbb632008-06-06 21:10:57 +0000343 Internal:
344 synch_id: currently unused
345 status: string describing status of host
showard21baa452008-10-21 00:08:39 +0000346 invalid: true if the host has been deleted
347 protection: indicates what can be done to this host during repair
348 locked_by: user that locked the host, or null if the host is unlocked
349 lock_time: DateTime at which the host was locked
350 dirty: true if the host has been used without being rebooted
jadmanski0afbb632008-06-06 21:10:57 +0000351 """
352 Status = enum.Enum('Verifying', 'Running', 'Ready', 'Repairing',
jamesren121eee62010-04-13 19:10:12 +0000353 'Repair Failed', 'Cleaning', 'Pending',
showard6d7b2ff2009-06-10 00:16:47 +0000354 string_values=True)
showard4d233752010-01-20 19:06:40 +0000355 Protection = host_protections.Protection
mblighe8819cd2008-02-15 16:48:40 +0000356
showarda5288b42009-07-28 20:06:08 +0000357 hostname = dbmodels.CharField(max_length=255, unique=True)
showardeab66ce2009-12-23 00:03:56 +0000358 labels = dbmodels.ManyToManyField(Label, blank=True,
359 db_table='afe_hosts_labels')
jadmanski0afbb632008-06-06 21:10:57 +0000360 locked = dbmodels.BooleanField(default=False)
361 synch_id = dbmodels.IntegerField(blank=True, null=True,
362 editable=settings.FULL_ADMIN)
showarda5288b42009-07-28 20:06:08 +0000363 status = dbmodels.CharField(max_length=255, default=Status.READY,
jadmanski0afbb632008-06-06 21:10:57 +0000364 choices=Status.choices(),
365 editable=settings.FULL_ADMIN)
366 invalid = dbmodels.BooleanField(default=False,
367 editable=settings.FULL_ADMIN)
showardd5afc2f2008-08-12 17:19:44 +0000368 protection = dbmodels.SmallIntegerField(null=False, blank=True,
showarddf062562008-07-03 19:56:37 +0000369 choices=host_protections.choices,
370 default=host_protections.default)
showardfb2a7fa2008-07-17 17:04:12 +0000371 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
372 lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False)
showard21baa452008-10-21 00:08:39 +0000373 dirty = dbmodels.BooleanField(default=True, editable=settings.FULL_ADMIN)
mblighe8819cd2008-02-15 16:48:40 +0000374
jadmanski0afbb632008-06-06 21:10:57 +0000375 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000376 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000377 valid_objects = model_logic.ValidObjectsManager()
mbligh5244cbb2008-04-24 20:39:52 +0000378
showard2bab8f42008-11-12 18:15:22 +0000379
380 def __init__(self, *args, **kwargs):
381 super(Host, self).__init__(*args, **kwargs)
382 self._record_attributes(['status'])
383
384
showardb8471e32008-07-03 19:51:08 +0000385 @staticmethod
386 def create_one_time_host(hostname):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800387 """Creates a one-time host.
388
389 @param hostname: The name for the host.
390 """
showardb8471e32008-07-03 19:51:08 +0000391 query = Host.objects.filter(hostname=hostname)
392 if query.count() == 0:
393 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000394 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000395 else:
396 host = query[0]
397 if not host.invalid:
398 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000399 'hostname' : '%s already exists in the autotest DB. '
400 'Select it rather than entering it as a one time '
401 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000402 })
showard1ab512b2008-07-30 23:39:04 +0000403 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000404 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000405 host.save()
showard2924b0a2009-06-18 23:16:15 +0000406 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000407 return host
mbligh5244cbb2008-04-24 20:39:52 +0000408
showard1ff7b2e2009-05-15 23:17:18 +0000409
showardafd97de2009-10-01 18:45:09 +0000410 def resurrect_object(self, old_object):
411 super(Host, self).resurrect_object(old_object)
412 # invalid hosts can be in use by the scheduler (as one-time hosts), so
413 # don't change the status
414 self.status = old_object.status
415
416
jadmanski0afbb632008-06-06 21:10:57 +0000417 def clean_object(self):
418 self.aclgroup_set.clear()
419 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000420
421
showarda5288b42009-07-28 20:06:08 +0000422 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000423 # extra spaces in the hostname can be a sneaky source of errors
424 self.hostname = self.hostname.strip()
425 # is this a new object being saved for the first time?
426 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000427 if not first_time:
428 AclGroup.check_for_acl_violation_hosts([self])
showardfb2a7fa2008-07-17 17:04:12 +0000429 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000430 self.locked_by = User.current_user()
showardfb2a7fa2008-07-17 17:04:12 +0000431 self.lock_time = datetime.now()
showard21baa452008-10-21 00:08:39 +0000432 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000433 elif not self.locked and self.locked_by:
434 self.locked_by = None
435 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000436 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000437 if first_time:
438 everyone = AclGroup.objects.get(name='Everyone')
439 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000440 self._check_for_updated_attributes()
441
mblighe8819cd2008-02-15 16:48:40 +0000442
showardb8471e32008-07-03 19:51:08 +0000443 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000444 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000445 for queue_entry in self.hostqueueentry_set.all():
446 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000447 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000448 super(Host, self).delete()
449
mblighe8819cd2008-02-15 16:48:40 +0000450
showard2bab8f42008-11-12 18:15:22 +0000451 def on_attribute_changed(self, attribute, old_value):
452 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000453 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000454
455
showard29f7cd22009-04-29 21:16:24 +0000456 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800457 """Enqueue a job on this host.
458
459 @param job: A job to enqueue.
460 @param atomic_group: The associated atomic group.
461 @param is_template: Whther the status should be "Template".
462 """
showard29f7cd22009-04-29 21:16:24 +0000463 queue_entry = HostQueueEntry.create(host=self, job=job,
464 is_template=is_template,
465 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000466 # allow recovery of dead hosts from the frontend
467 if not self.active_queue_entry() and self.is_dead():
468 self.status = Host.Status.READY
469 self.save()
470 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000471
showard08f981b2008-06-24 21:59:03 +0000472 block = IneligibleHostQueue(job=job, host=self)
473 block.save()
474
mblighe8819cd2008-02-15 16:48:40 +0000475
jadmanski0afbb632008-06-06 21:10:57 +0000476 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800477 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000478 # TODO(showard): slighly hacky?
479 platforms = self.labels.filter(platform=True)
480 if len(platforms) == 0:
481 return None
482 return platforms[0]
483 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000484
485
showardcafd16e2009-05-29 18:37:49 +0000486 @classmethod
487 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800488 """Verify the specified hosts have no associated platforms.
489
490 @param cls: Implicit class object.
491 @param hosts: The hosts to verify.
492 @raises model_logic.ValidationError if any hosts already have a
493 platform.
494 """
showardcafd16e2009-05-29 18:37:49 +0000495 Host.objects.populate_relationships(hosts, Label, 'label_list')
496 errors = []
497 for host in hosts:
498 platforms = [label.name for label in host.label_list
499 if label.platform]
500 if platforms:
501 # do a join, just in case this host has multiple platforms,
502 # we'll be able to see it
503 errors.append('Host %s already has a platform: %s' % (
504 host.hostname, ', '.join(platforms)))
505 if errors:
506 raise model_logic.ValidationError({'labels': '; '.join(errors)})
507
508
jadmanski0afbb632008-06-06 21:10:57 +0000509 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800510 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000511 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000512
513
jadmanski0afbb632008-06-06 21:10:57 +0000514 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800515 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000516 active = list(self.hostqueueentry_set.filter(active=True))
517 if not active:
518 return None
519 assert len(active) == 1, ('More than one active entry for '
520 'host ' + self.hostname)
521 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000522
523
showardf8b19042009-05-12 17:22:49 +0000524 def _get_attribute_model_and_args(self, attribute):
525 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000526
527
jadmanski0afbb632008-06-06 21:10:57 +0000528 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800529 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000530 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000531
showarda5288b42009-07-28 20:06:08 +0000532 def __unicode__(self):
533 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000534
535
showard0957a842009-05-11 19:25:08 +0000536class HostAttribute(dbmodels.Model):
537 """Arbitrary keyvals associated with hosts."""
538 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000539 attribute = dbmodels.CharField(max_length=90)
540 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000541
542 objects = model_logic.ExtendedManager()
543
544 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800545 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000546 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000547
548
showard7c785282008-05-29 19:45:12 +0000549class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000550 """\
551 Required:
showard909c7a62008-07-15 21:52:38 +0000552 author: author name
553 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000554 name: test name
showard909c7a62008-07-15 21:52:38 +0000555 time: short, medium, long
556 test_class: This describes the class for your the test belongs in.
557 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000558 test_type: Client or Server
559 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000560 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
561 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000562 i.e. if sync_count = 2 it is a sync job that requires two
563 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000564 Optional:
showard909c7a62008-07-15 21:52:38 +0000565 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000566 dependency_labels: many-to-many relationship with labels corresponding to
567 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000568 experimental: If this is set to True production servers will ignore the test
569 run_verify: Whether or not the scheduler should run the verify stage
jadmanski0afbb632008-06-06 21:10:57 +0000570 """
showard909c7a62008-07-15 21:52:38 +0000571 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
jamesrencd7a81a2010-04-21 20:39:08 +0000572 TestTypes = model_attributes.TestTypes
jadmanski0afbb632008-06-06 21:10:57 +0000573 # TODO(showard) - this should be merged with Job.ControlType (but right
574 # now they use opposite values)
mblighe8819cd2008-02-15 16:48:40 +0000575
showarda5288b42009-07-28 20:06:08 +0000576 name = dbmodels.CharField(max_length=255, unique=True)
577 author = dbmodels.CharField(max_length=255)
578 test_class = dbmodels.CharField(max_length=255)
579 test_category = dbmodels.CharField(max_length=255)
580 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000581 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000582 experimental = dbmodels.BooleanField(default=True)
583 run_verify = dbmodels.BooleanField(default=True)
584 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
585 default=TestTime.MEDIUM)
jamesrencd7a81a2010-04-21 20:39:08 +0000586 test_type = dbmodels.SmallIntegerField(choices=TestTypes.choices())
showard909c7a62008-07-15 21:52:38 +0000587 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000588 path = dbmodels.CharField(max_length=255, unique=True)
mblighe8819cd2008-02-15 16:48:40 +0000589
showardeab66ce2009-12-23 00:03:56 +0000590 dependency_labels = (
591 dbmodels.ManyToManyField(Label, blank=True,
592 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000593 name_field = 'name'
594 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000595
596
jamesren35a70222010-02-16 19:30:46 +0000597 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800598 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +0000599 escaped_description = saxutils.escape(self.description)
600 return '<span style="white-space:pre">%s</span>' % escaped_description
601 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000602 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000603
604
jadmanski0afbb632008-06-06 21:10:57 +0000605 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800606 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +0000607 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000608
showarda5288b42009-07-28 20:06:08 +0000609 def __unicode__(self):
610 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000611
612
jamesren4a41e012010-07-16 22:33:48 +0000613class TestParameter(dbmodels.Model):
614 """
615 A declared parameter of a test
616 """
617 test = dbmodels.ForeignKey(Test)
618 name = dbmodels.CharField(max_length=255)
619
620 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800621 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000622 db_table = 'afe_test_parameters'
623 unique_together = ('test', 'name')
624
625 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -0700626 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +0000627
628
showard2b9a88b2008-06-13 20:55:03 +0000629class Profiler(dbmodels.Model, model_logic.ModelExtensions):
630 """\
631 Required:
632 name: profiler name
633 test_type: Client or Server
634
635 Optional:
636 description: arbirary text description
637 """
showarda5288b42009-07-28 20:06:08 +0000638 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000639 description = dbmodels.TextField(blank=True)
640
641 name_field = 'name'
642 objects = model_logic.ExtendedManager()
643
644
645 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800646 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +0000647 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000648
showarda5288b42009-07-28 20:06:08 +0000649 def __unicode__(self):
650 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000651
652
showard7c785282008-05-29 19:45:12 +0000653class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000654 """\
655 Required:
656 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000657
jadmanski0afbb632008-06-06 21:10:57 +0000658 Optional:
659 description: arbitrary description of group
660 """
showarda5288b42009-07-28 20:06:08 +0000661 name = dbmodels.CharField(max_length=255, unique=True)
662 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000663 users = dbmodels.ManyToManyField(User, blank=False,
664 db_table='afe_acl_groups_users')
665 hosts = dbmodels.ManyToManyField(Host, blank=True,
666 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000667
jadmanski0afbb632008-06-06 21:10:57 +0000668 name_field = 'name'
669 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000670
showard08f981b2008-06-24 21:59:03 +0000671 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000672 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800673 """Verify the current user has access to the specified hosts.
674
675 @param hosts: The hosts to verify against.
676 @raises AclAccessViolation if the current user doesn't have access
677 to a host.
678 """
showard64a95952010-01-13 21:27:16 +0000679 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000680 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000681 return
showard3dd47c22008-07-10 00:41:36 +0000682 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000683 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000684 for host in hosts:
685 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800686 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +0000687 no_access = (isinstance(host, Host)
688 and not host.invalid
689 and int(host.id) not in accessible_host_ids)
690 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000691 raise AclAccessViolation("%s does not have access to %s" %
692 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000693
showard9dbdcda2008-10-14 17:34:36 +0000694
695 @staticmethod
showarddc817512008-11-12 18:16:41 +0000696 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800697 """Look for queue entries that aren't abortable by the current user.
698
699 An entry is not abortable if:
700 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +0000701 * the machine isn't ACL-accessible, or
702 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800703
704 @param queue_entries: The queue entries to check.
705 @raises AclAccessViolation if a queue entry is not abortable by the
706 current user.
showarddc817512008-11-12 18:16:41 +0000707 """
showard64a95952010-01-13 21:27:16 +0000708 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000709 if user.is_superuser():
710 return
showarddc817512008-11-12 18:16:41 +0000711 not_owned = queue_entries.exclude(job__owner=user.login)
712 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000713 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
714 # 1.0.
715 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000716 accessible_ids = set(
717 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000718 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000719 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000720 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000721 cannot_abort = [entry for entry in not_owned.select_related()
722 if entry.id not in accessible_ids
723 or entry.id in public_ids]
724 if len(cannot_abort) == 0:
725 return
726 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000727 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000728 for entry in cannot_abort)
729 raise AclAccessViolation('You cannot abort the following job entries: '
730 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000731
732
showard3dd47c22008-07-10 00:41:36 +0000733 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800734 """Verifies the current user has acces to this ACL group.
735
736 @raises AclAccessViolation if the current user doesn't have access to
737 this ACL group.
738 """
showard64a95952010-01-13 21:27:16 +0000739 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000740 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000741 return
742 if self.name == 'Everyone':
743 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000744 if not user in self.users.all():
745 raise AclAccessViolation("You do not have access to %s"
746 % self.name)
747
748 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000749 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800750 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +0000751 everyone = AclGroup.objects.get(name='Everyone')
752
showard3dd47c22008-07-10 00:41:36 +0000753 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000754 # TODO(showard): this is a bit of a hack, since the fact that this query
755 # works is kind of a coincidence of Django internals. This trick
756 # doesn't work in general (on all foreign key relationships). I'll
757 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000758 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000759 everyone.hosts.add(*orphaned_hosts.distinct())
760
761 # find hosts in both Everyone and another ACL group, and remove them
762 # from Everyone
showarda5288b42009-07-28 20:06:08 +0000763 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
764 acled_hosts = set()
765 for host in hosts_in_everyone:
766 # Has an ACL group other than Everyone
767 if host.aclgroup_set.count() > 1:
768 acled_hosts.add(host)
769 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +0000770
771
772 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000773 if (self.name == 'Everyone'):
774 raise AclAccessViolation("You cannot delete 'Everyone'!")
775 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000776 super(AclGroup, self).delete()
777 self.on_host_membership_change()
778
779
showard04f2cd82008-07-25 20:53:31 +0000780 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800781 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +0000782 if not self.users.count():
showard64a95952010-01-13 21:27:16 +0000783 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +0000784
785
showard8cbaf1e2009-09-08 16:27:04 +0000786 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800787 """Called after a save.
788
789 @param change: Whether there was a change.
790 """
showard8cbaf1e2009-09-08 16:27:04 +0000791 if not change:
showard64a95952010-01-13 21:27:16 +0000792 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +0000793 self.add_current_user_if_empty()
794 self.on_host_membership_change()
795
796
797 def save(self, *args, **kwargs):
798 change = bool(self.id)
799 if change:
800 # Check the original object for an ACL violation
801 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
802 super(AclGroup, self).save(*args, **kwargs)
803 self.perform_after_save(change)
804
showardeb3be4d2008-04-21 20:59:26 +0000805
jadmanski0afbb632008-06-06 21:10:57 +0000806 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800807 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +0000808 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000809
showarda5288b42009-07-28 20:06:08 +0000810 def __unicode__(self):
811 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000812
mblighe8819cd2008-02-15 16:48:40 +0000813
jamesren4a41e012010-07-16 22:33:48 +0000814class Kernel(dbmodels.Model):
815 """
816 A kernel configuration for a parameterized job
817 """
818 version = dbmodels.CharField(max_length=255)
819 cmdline = dbmodels.CharField(max_length=255, blank=True)
820
821 @classmethod
822 def create_kernels(cls, kernel_list):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800823 """Creates all kernels in the kernel list.
jamesren4a41e012010-07-16 22:33:48 +0000824
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800825 @param cls: Implicit class object.
826 @param kernel_list: A list of dictionaries that describe the kernels,
827 in the same format as the 'kernel' argument to
828 rpc_interface.generate_control_file.
829 @return A list of the created kernels.
jamesren4a41e012010-07-16 22:33:48 +0000830 """
831 if not kernel_list:
832 return None
833 return [cls._create(kernel) for kernel in kernel_list]
834
835
836 @classmethod
837 def _create(cls, kernel_dict):
838 version = kernel_dict.pop('version')
839 cmdline = kernel_dict.pop('cmdline', '')
840
841 if kernel_dict:
842 raise Exception('Extraneous kernel arguments remain: %r'
843 % kernel_dict)
844
845 kernel, _ = cls.objects.get_or_create(version=version,
846 cmdline=cmdline)
847 return kernel
848
849
850 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800851 """Metadata for class Kernel."""
jamesren4a41e012010-07-16 22:33:48 +0000852 db_table = 'afe_kernels'
853 unique_together = ('version', 'cmdline')
854
855 def __unicode__(self):
856 return u'%s %s' % (self.version, self.cmdline)
857
858
859class ParameterizedJob(dbmodels.Model):
860 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800861 Auxiliary configuration for a parameterized job.
jamesren4a41e012010-07-16 22:33:48 +0000862 """
863 test = dbmodels.ForeignKey(Test)
864 label = dbmodels.ForeignKey(Label, null=True)
865 use_container = dbmodels.BooleanField(default=False)
866 profile_only = dbmodels.BooleanField(default=False)
867 upload_kernel_config = dbmodels.BooleanField(default=False)
868
869 kernels = dbmodels.ManyToManyField(
870 Kernel, db_table='afe_parameterized_job_kernels')
871 profilers = dbmodels.ManyToManyField(
872 Profiler, through='ParameterizedJobProfiler')
873
874
875 @classmethod
876 def smart_get(cls, id_or_name, *args, **kwargs):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800877 """For compatibility with Job.add_object.
878
879 @param cls: Implicit class object.
880 @param id_or_name: The ID or name to get.
881 @param args: Non-keyword arguments.
882 @param kwargs: Keyword arguments.
883 """
jamesren4a41e012010-07-16 22:33:48 +0000884 return cls.objects.get(pk=id_or_name)
885
886
887 def job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800888 """Returns the job if it exists, or else None."""
jamesren4a41e012010-07-16 22:33:48 +0000889 jobs = self.job_set.all()
890 assert jobs.count() <= 1
891 return jobs and jobs[0] or None
892
893
894 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800895 """Metadata for class ParameterizedJob."""
jamesren4a41e012010-07-16 22:33:48 +0000896 db_table = 'afe_parameterized_jobs'
897
898 def __unicode__(self):
899 return u'%s (parameterized) - %s' % (self.test.name, self.job())
900
901
902class ParameterizedJobProfiler(dbmodels.Model):
903 """
904 A profiler to run on a parameterized job
905 """
906 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
907 profiler = dbmodels.ForeignKey(Profiler)
908
909 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800910 """Metedata for class ParameterizedJobProfiler."""
jamesren4a41e012010-07-16 22:33:48 +0000911 db_table = 'afe_parameterized_jobs_profilers'
912 unique_together = ('parameterized_job', 'profiler')
913
914
915class ParameterizedJobProfilerParameter(dbmodels.Model):
916 """
917 A parameter for a profiler in a parameterized job
918 """
919 parameterized_job_profiler = dbmodels.ForeignKey(ParameterizedJobProfiler)
920 parameter_name = dbmodels.CharField(max_length=255)
921 parameter_value = dbmodels.TextField()
922 parameter_type = dbmodels.CharField(
923 max_length=8, choices=model_attributes.ParameterTypes.choices())
924
925 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800926 """Metadata for class ParameterizedJobProfilerParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000927 db_table = 'afe_parameterized_job_profiler_parameters'
928 unique_together = ('parameterized_job_profiler', 'parameter_name')
929
930 def __unicode__(self):
931 return u'%s - %s' % (self.parameterized_job_profiler.profiler.name,
932 self.parameter_name)
933
934
935class ParameterizedJobParameter(dbmodels.Model):
936 """
937 Parameters for a parameterized job
938 """
939 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
940 test_parameter = dbmodels.ForeignKey(TestParameter)
941 parameter_value = dbmodels.TextField()
942 parameter_type = dbmodels.CharField(
943 max_length=8, choices=model_attributes.ParameterTypes.choices())
944
945 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800946 """Metadata for class ParameterizedJobParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000947 db_table = 'afe_parameterized_job_parameters'
948 unique_together = ('parameterized_job', 'test_parameter')
949
950 def __unicode__(self):
951 return u'%s - %s' % (self.parameterized_job.job().name,
952 self.test_parameter.name)
953
954
showard7c785282008-05-29 19:45:12 +0000955class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +0000956 'Custom manager to provide efficient status counts querying.'
957 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800958 """Returns a dict mapping the given job IDs to their status count dicts.
959
960 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +0000961 """
962 if not job_ids:
963 return {}
964 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
965 cursor = connection.cursor()
966 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +0000967 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +0000968 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +0000969 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +0000970 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +0000971 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +0000972 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +0000973 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +0000974 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +0000975 full_status = HostQueueEntry.compute_full_status(status, aborted,
976 complete)
showardb6d16622009-05-26 19:35:29 +0000977 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +0000978 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +0000979 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +0000980
981
showard7c785282008-05-29 19:45:12 +0000982class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000983 """\
984 owner: username of job owner
985 name: job name (does not have to be unique)
986 priority: Low, Medium, High, Urgent (or 0-3)
987 control_file: contents of control file
988 control_type: Client or Server
989 created_on: date of job creation
990 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +0000991 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +0000992 run_verify: Whether or not to run the verify phase
showard12f3e322009-05-13 21:27:42 +0000993 timeout: hours from queuing time until job times out
Simran Basi34217022012-11-06 13:43:15 -0800994 max_runtime_hrs: DEPRECATED - hours from job starting time until job
995 times out
996 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +0000997 email_list: list of people to email on completion delimited by any of:
998 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +0000999 dependency_labels: many-to-many relationship with labels corresponding to
1000 job dependencies
showard21baa452008-10-21 00:08:39 +00001001 reboot_before: Never, If dirty, or Always
1002 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001003 parse_failed_repair: if True, a failed repair launched by this job will have
1004 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001005 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001006 parent_job: Parent job (optional)
jadmanski0afbb632008-06-06 21:10:57 +00001007 """
showardb1e51872008-10-07 11:08:18 +00001008 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
1009 'AUTOTEST_WEB', 'job_timeout_default', default=240)
Simran Basi34217022012-11-06 13:43:15 -08001010 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1011 # completed.
showard12f3e322009-05-13 21:27:42 +00001012 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1013 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001014 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1015 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001016 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1017 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1018 default=False)
showardb1e51872008-10-07 11:08:18 +00001019
jadmanski0afbb632008-06-06 21:10:57 +00001020 Priority = enum.Enum('Low', 'Medium', 'High', 'Urgent')
1021 ControlType = enum.Enum('Server', 'Client', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +00001022
showarda5288b42009-07-28 20:06:08 +00001023 owner = dbmodels.CharField(max_length=255)
1024 name = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001025 priority = dbmodels.SmallIntegerField(choices=Priority.choices(),
1026 blank=True, # to allow 0
1027 default=Priority.MEDIUM)
jamesren4a41e012010-07-16 22:33:48 +00001028 control_file = dbmodels.TextField(null=True, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +00001029 control_type = dbmodels.SmallIntegerField(choices=ControlType.choices(),
showardb1e51872008-10-07 11:08:18 +00001030 blank=True, # to allow 0
1031 default=ControlType.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001032 created_on = dbmodels.DateTimeField()
showard2bab8f42008-11-12 18:15:22 +00001033 synch_count = dbmodels.IntegerField(null=True, default=1)
showardb1e51872008-10-07 11:08:18 +00001034 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
showard9976ce92008-10-15 20:28:13 +00001035 run_verify = dbmodels.BooleanField(default=True)
showarda5288b42009-07-28 20:06:08 +00001036 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001037 dependency_labels = (
1038 dbmodels.ManyToManyField(Label, blank=True,
1039 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001040 reboot_before = dbmodels.SmallIntegerField(
1041 choices=model_attributes.RebootBefore.choices(), blank=True,
1042 default=DEFAULT_REBOOT_BEFORE)
1043 reboot_after = dbmodels.SmallIntegerField(
1044 choices=model_attributes.RebootAfter.choices(), blank=True,
1045 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001046 parse_failed_repair = dbmodels.BooleanField(
1047 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001048 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1049 # completed.
showard12f3e322009-05-13 21:27:42 +00001050 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001051 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001052 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001053
jamesren4a41e012010-07-16 22:33:48 +00001054 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1055 blank=True)
1056
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001057 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001058
jadmanski0afbb632008-06-06 21:10:57 +00001059 # custom manager
1060 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001061
1062
jadmanski0afbb632008-06-06 21:10:57 +00001063 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001064 """Returns whether this job is of type server."""
jadmanski0afbb632008-06-06 21:10:57 +00001065 return self.control_type == self.ControlType.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001066
1067
jadmanski0afbb632008-06-06 21:10:57 +00001068 @classmethod
jamesren4a41e012010-07-16 22:33:48 +00001069 def parameterized_jobs_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001070 """Returns whether parameterized jobs are enabled.
1071
1072 @param cls: Implicit class object.
1073 """
jamesren4a41e012010-07-16 22:33:48 +00001074 return global_config.global_config.get_config_value(
1075 'AUTOTEST_WEB', 'parameterized_jobs', type=bool)
1076
1077
1078 @classmethod
1079 def check_parameterized_job(cls, control_file, parameterized_job):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001080 """Checks that the job is valid given the global config settings.
jamesren4a41e012010-07-16 22:33:48 +00001081
1082 First, either control_file must be set, or parameterized_job must be
1083 set, but not both. Second, parameterized_job must be set if and only if
1084 the parameterized_jobs option in the global config is set to True.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001085
1086 @param cls: Implict class object.
1087 @param control_file: A control file.
1088 @param parameterized_job: A parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001089 """
1090 if not (bool(control_file) ^ bool(parameterized_job)):
1091 raise Exception('Job must have either control file or '
1092 'parameterization, but not both')
1093
1094 parameterized_jobs_enabled = cls.parameterized_jobs_enabled()
1095 if control_file and parameterized_jobs_enabled:
1096 raise Exception('Control file specified, but parameterized jobs '
1097 'are enabled')
1098 if parameterized_job and not parameterized_jobs_enabled:
1099 raise Exception('Parameterized job specified, but parameterized '
1100 'jobs are not enabled')
1101
1102
1103 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001104 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001105 """Creates a job.
1106
1107 The job is created by taking some information (the listed args) and
1108 filling in the rest of the necessary information.
1109
1110 @param cls: Implicit class object.
1111 @param owner: The owner for the job.
1112 @param options: An options object.
1113 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001114 """
showard3dd47c22008-07-10 00:41:36 +00001115 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001116
jamesren4a41e012010-07-16 22:33:48 +00001117 control_file = options.get('control_file')
1118 parameterized_job = options.get('parameterized_job')
jamesren4a41e012010-07-16 22:33:48 +00001119
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001120 # The current implementation of parameterized jobs requires that only
1121 # control files or parameterized jobs are used. Using the image
1122 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1123 # parameterized jobs and control files jobs, it does muck enough with
1124 # normal jobs by adding a parameterized id to them that this check will
1125 # fail. So for now we just skip this check.
1126 # cls.check_parameterized_job(control_file=control_file,
1127 # parameterized_job=parameterized_job)
showard4d233752010-01-20 19:06:40 +00001128 user = User.current_user()
1129 if options.get('reboot_before') is None:
1130 options['reboot_before'] = user.get_reboot_before_display()
1131 if options.get('reboot_after') is None:
1132 options['reboot_after'] = user.get_reboot_after_display()
1133
jamesren76fcf192010-04-21 20:39:50 +00001134 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1135
jadmanski0afbb632008-06-06 21:10:57 +00001136 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001137 owner=owner,
1138 name=options['name'],
1139 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001140 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001141 control_type=options['control_type'],
1142 synch_count=options.get('synch_count'),
1143 timeout=options.get('timeout'),
Simran Basi34217022012-11-06 13:43:15 -08001144 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001145 run_verify=options.get('run_verify'),
1146 email_list=options.get('email_list'),
1147 reboot_before=options.get('reboot_before'),
1148 reboot_after=options.get('reboot_after'),
1149 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001150 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001151 drone_set=drone_set,
Aviv Keshet18308922013-02-19 17:49:49 -08001152 parameterized_job=parameterized_job,
1153 parent_job=options.get('parent_job_id'))
mblighe8819cd2008-02-15 16:48:40 +00001154
showarda1e74b32009-05-12 17:32:04 +00001155 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001156
jamesrend8b6e172010-04-16 23:45:00 +00001157 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001158 for key, value in options['keyvals'].iteritems():
1159 JobKeyval.objects.create(job=job, key=key, value=value)
1160
jadmanski0afbb632008-06-06 21:10:57 +00001161 return job
mblighe8819cd2008-02-15 16:48:40 +00001162
1163
jamesren4a41e012010-07-16 22:33:48 +00001164 def save(self, *args, **kwargs):
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001165 # The current implementation of parameterized jobs requires that only
1166 # control files or parameterized jobs are used. Using the image
1167 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1168 # parameterized jobs and control files jobs, it does muck enough with
1169 # normal jobs by adding a parameterized id to them that this check will
1170 # fail. So for now we just skip this check.
1171 # cls.check_parameterized_job(control_file=self.control_file,
1172 # parameterized_job=self.parameterized_job)
jamesren4a41e012010-07-16 22:33:48 +00001173 super(Job, self).save(*args, **kwargs)
1174
1175
showard29f7cd22009-04-29 21:16:24 +00001176 def queue(self, hosts, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001177 """Enqueue a job on the given hosts.
1178
1179 @param hosts: The hosts to use.
1180 @param atomic_group: The associated atomic group.
1181 @param is_template: Whether the status should be "Template".
1182 """
showarda9545c02009-12-18 22:44:26 +00001183 if not hosts:
1184 if atomic_group:
1185 # No hosts or labels are required to queue an atomic group
1186 # Job. However, if they are given, we respect them below.
1187 atomic_group.enqueue_job(self, is_template=is_template)
1188 else:
1189 # hostless job
1190 entry = HostQueueEntry.create(job=self, is_template=is_template)
1191 entry.save()
1192 return
1193
jadmanski0afbb632008-06-06 21:10:57 +00001194 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +00001195 host.enqueue_job(self, atomic_group=atomic_group,
1196 is_template=is_template)
1197
1198
1199 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001200 """Creates a recurring job.
1201
1202 @param start_date: The starting date of the job.
1203 @param loop_period: How often to re-run the job, in seconds.
1204 @param loop_count: The re-run count.
1205 @param owner: The owner of the job.
1206 """
showard29f7cd22009-04-29 21:16:24 +00001207 rec = RecurringRun(job=self, start_date=start_date,
1208 loop_period=loop_period,
1209 loop_count=loop_count,
1210 owner=User.objects.get(login=owner))
1211 rec.save()
1212 return rec.id
mblighe8819cd2008-02-15 16:48:40 +00001213
1214
jadmanski0afbb632008-06-06 21:10:57 +00001215 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001216 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001217 try:
1218 return User.objects.get(login=self.owner)
1219 except self.DoesNotExist:
1220 return None
mblighe8819cd2008-02-15 16:48:40 +00001221
1222
showard64a95952010-01-13 21:27:16 +00001223 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001224 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001225 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001226 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001227
1228
showardd1195652009-12-08 22:21:02 +00001229 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001230 """Returns a string tag for this job."""
showardd1195652009-12-08 22:21:02 +00001231 return '%s-%s' % (self.id, self.owner)
1232
1233
showardc1a98d12010-01-15 00:22:22 +00001234 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001235 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001236 return dict((keyval.key, keyval.value)
1237 for keyval in self.jobkeyval_set.all())
1238
1239
jadmanski0afbb632008-06-06 21:10:57 +00001240 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001241 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001242 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001243
showarda5288b42009-07-28 20:06:08 +00001244 def __unicode__(self):
1245 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001246
1247
showardc1a98d12010-01-15 00:22:22 +00001248class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1249 """Keyvals associated with jobs"""
1250 job = dbmodels.ForeignKey(Job)
1251 key = dbmodels.CharField(max_length=90)
1252 value = dbmodels.CharField(max_length=300)
1253
1254 objects = model_logic.ExtendedManager()
1255
1256 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001257 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001258 db_table = 'afe_job_keyvals'
1259
1260
showard7c785282008-05-29 19:45:12 +00001261class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001262 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001263 job = dbmodels.ForeignKey(Job)
1264 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001265
jadmanski0afbb632008-06-06 21:10:57 +00001266 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001267
jadmanski0afbb632008-06-06 21:10:57 +00001268 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001269 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001270 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001271
mblighe8819cd2008-02-15 16:48:40 +00001272
showard7c785282008-05-29 19:45:12 +00001273class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001274 """Represents a host queue entry."""
showardeaa408e2009-09-11 18:45:31 +00001275 Status = host_queue_entry_states.Status
1276 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001277 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001278
jadmanski0afbb632008-06-06 21:10:57 +00001279 job = dbmodels.ForeignKey(Job)
1280 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001281 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001282 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1283 db_column='meta_host')
1284 active = dbmodels.BooleanField(default=False)
1285 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001286 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001287 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1288 default='')
showard89f84db2009-03-12 20:39:13 +00001289 # If atomic_group is set, this is a virtual HostQueueEntry that will
1290 # be expanded into many actual hosts within the group at schedule time.
1291 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001292 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001293 started_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001294
jadmanski0afbb632008-06-06 21:10:57 +00001295 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001296
mblighe8819cd2008-02-15 16:48:40 +00001297
showard2bab8f42008-11-12 18:15:22 +00001298 def __init__(self, *args, **kwargs):
1299 super(HostQueueEntry, self).__init__(*args, **kwargs)
1300 self._record_attributes(['status'])
1301
1302
showard29f7cd22009-04-29 21:16:24 +00001303 @classmethod
1304 def create(cls, job, host=None, meta_host=None, atomic_group=None,
1305 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001306 """Creates a new host queue entry.
1307
1308 @param cls: Implicit class object.
1309 @param job: The associated job.
1310 @param host: The associated host.
1311 @param meta_host: The associated meta host.
1312 @param atomic_group: The associated atomic group.
1313 @param is_template: Whether the status should be "Template".
1314 """
showard29f7cd22009-04-29 21:16:24 +00001315 if is_template:
1316 status = cls.Status.TEMPLATE
1317 else:
1318 status = cls.Status.QUEUED
1319
1320 return cls(job=job, host=host, meta_host=meta_host,
1321 atomic_group=atomic_group, status=status)
1322
1323
showarda5288b42009-07-28 20:06:08 +00001324 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001325 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001326 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001327 self._check_for_updated_attributes()
1328
1329
showardc0ac3a72009-07-08 21:14:45 +00001330 def execution_path(self):
1331 """
1332 Path to this entry's results (relative to the base results directory).
1333 """
showardd1195652009-12-08 22:21:02 +00001334 return os.path.join(self.job.tag(), self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001335
1336
showard3f15eed2008-11-14 22:40:48 +00001337 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001338 """Returns the first non-None name found in priority order.
1339
1340 The priority order checked is: (1) host name; (2) meta host name; and
1341 (3) atomic group name.
1342 """
showard3f15eed2008-11-14 22:40:48 +00001343 if self.host:
1344 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001345 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +00001346 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +00001347 else:
1348 assert self.atomic_group, "no host, meta_host or atomic group!"
1349 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +00001350
1351
showard2bab8f42008-11-12 18:15:22 +00001352 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001353 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001354 self.active, self.complete = True, False
1355 elif self.status in self.COMPLETE_STATUSES:
1356 self.active, self.complete = False, True
1357 else:
1358 self.active, self.complete = False, False
1359
1360
1361 def on_attribute_changed(self, attribute, old_value):
1362 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001363 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1364 self.status)
showard2bab8f42008-11-12 18:15:22 +00001365
1366
jadmanski0afbb632008-06-06 21:10:57 +00001367 def is_meta_host_entry(self):
1368 'True if this is a entry has a meta_host instead of a host.'
1369 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001370
showarda3ab0d52008-11-03 19:03:47 +00001371
showard4c119042008-09-29 19:16:18 +00001372 def log_abort(self, user):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001373 """Logs an abort.
1374
1375 @param user: The user performing the abort.
1376 """
showard4c119042008-09-29 19:16:18 +00001377 abort_log = AbortedHostQueueEntry(queue_entry=self, aborted_by=user)
1378 abort_log.save()
1379
showarda3ab0d52008-11-03 19:03:47 +00001380
showard64a95952010-01-13 21:27:16 +00001381 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001382 """Aborts this host queue entry."""
1383 # This isn't completely immune to race conditions since it's not atomic,
showarda3ab0d52008-11-03 19:03:47 +00001384 # but it should be safe given the scheduler's behavior.
showardd3dc1992009-04-22 21:01:40 +00001385 if not self.complete and not self.aborted:
showard64a95952010-01-13 21:27:16 +00001386 self.log_abort(User.current_user())
showardd3dc1992009-04-22 21:01:40 +00001387 self.aborted = True
showarda3ab0d52008-11-03 19:03:47 +00001388 self.save()
mblighe8819cd2008-02-15 16:48:40 +00001389
showardd3dc1992009-04-22 21:01:40 +00001390
1391 @classmethod
1392 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001393 """Returns a modified status msg if the host queue entry was aborted.
1394
1395 @param cls: Implicit class object.
1396 @param status: The original status message.
1397 @param aborted: Whether the host queue entry was aborted.
1398 @param complete: Whether the host queue entry was completed.
1399 """
showardd3dc1992009-04-22 21:01:40 +00001400 if aborted and not complete:
1401 return 'Aborted (%s)' % status
1402 return status
1403
1404
1405 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001406 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00001407 return self.compute_full_status(self.status, self.aborted,
1408 self.complete)
1409
1410
1411 def _postprocess_object_dict(self, object_dict):
1412 object_dict['full_status'] = self.full_status()
1413
1414
jadmanski0afbb632008-06-06 21:10:57 +00001415 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001416 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001417 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00001418
showard12f3e322009-05-13 21:27:42 +00001419
showard4c119042008-09-29 19:16:18 +00001420
showarda5288b42009-07-28 20:06:08 +00001421 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00001422 hostname = None
1423 if self.host:
1424 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00001425 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00001426
1427
showard4c119042008-09-29 19:16:18 +00001428class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001429 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00001430 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
1431 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00001432 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00001433
1434 objects = model_logic.ExtendedManager()
1435
showard68c7aa02008-10-09 16:49:11 +00001436
showarda5288b42009-07-28 20:06:08 +00001437 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00001438 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00001439 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00001440
showard4c119042008-09-29 19:16:18 +00001441 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001442 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001443 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00001444
1445
1446class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
1447 """\
1448 job: job to use as a template
1449 owner: owner of the instantiated template
1450 start_date: Run the job at scheduled date
1451 loop_period: Re-run (loop) the job periodically
1452 (in every loop_period seconds)
1453 loop_count: Re-run (loop) count
1454 """
1455
1456 job = dbmodels.ForeignKey(Job)
1457 owner = dbmodels.ForeignKey(User)
1458 start_date = dbmodels.DateTimeField()
1459 loop_period = dbmodels.IntegerField(blank=True)
1460 loop_count = dbmodels.IntegerField(blank=True)
1461
1462 objects = model_logic.ExtendedManager()
1463
1464 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001465 """Metadata for class RecurringRun."""
showardeab66ce2009-12-23 00:03:56 +00001466 db_table = 'afe_recurring_run'
showard29f7cd22009-04-29 21:16:24 +00001467
showarda5288b42009-07-28 20:06:08 +00001468 def __unicode__(self):
1469 return u'RecurringRun(job %s, start %s, period %s, count %s)' % (
showard29f7cd22009-04-29 21:16:24 +00001470 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +00001471
1472
1473class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
1474 """\
1475 Tasks to run on hosts at the next time they are in the Ready state. Use this
1476 for high-priority tasks, such as forced repair or forced reinstall.
1477
1478 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00001479 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00001480 time_requested: date and time the request for this task was made
1481 is_active: task is currently running
1482 is_complete: task has finished running
showard2fe3f1d2009-07-06 20:19:11 +00001483 time_started: date and time the task started
showard2fe3f1d2009-07-06 20:19:11 +00001484 queue_entry: Host queue entry waiting on this task (or None, if task was not
1485 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00001486 """
showard2fe3f1d2009-07-06 20:19:11 +00001487 Task = enum.Enum('Verify', 'Cleanup', 'Repair', string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00001488
1489 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00001490 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00001491 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00001492 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00001493 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
1494 null=False)
1495 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
1496 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00001497 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00001498 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00001499 success = dbmodels.BooleanField(default=False, blank=False, null=False)
showard6d7b2ff2009-06-10 00:16:47 +00001500
1501 objects = model_logic.ExtendedManager()
1502
1503
showard9bb960b2009-11-19 01:02:11 +00001504 def save(self, **kwargs):
1505 if self.queue_entry:
1506 self.requested_by = User.objects.get(
1507 login=self.queue_entry.job.owner)
1508 super(SpecialTask, self).save(**kwargs)
1509
1510
showarded2afea2009-07-07 20:54:07 +00001511 def execution_path(self):
showardc0ac3a72009-07-08 21:14:45 +00001512 """@see HostQueueEntry.execution_path()"""
showarded2afea2009-07-07 20:54:07 +00001513 return 'hosts/%s/%s-%s' % (self.host.hostname, self.id,
1514 self.task.lower())
1515
1516
showardc0ac3a72009-07-08 21:14:45 +00001517 # property to emulate HostQueueEntry.status
1518 @property
1519 def status(self):
1520 """
1521 Return a host queue entry status appropriate for this task. Although
1522 SpecialTasks are not HostQueueEntries, it is helpful to the user to
1523 present similar statuses.
1524 """
1525 if self.is_complete:
showarde60e44e2009-11-13 20:45:38 +00001526 if self.success:
1527 return HostQueueEntry.Status.COMPLETED
1528 return HostQueueEntry.Status.FAILED
showardc0ac3a72009-07-08 21:14:45 +00001529 if self.is_active:
1530 return HostQueueEntry.Status.RUNNING
1531 return HostQueueEntry.Status.QUEUED
1532
1533
1534 # property to emulate HostQueueEntry.started_on
1535 @property
1536 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001537 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00001538 return self.time_started
1539
1540
showard6d7b2ff2009-06-10 00:16:47 +00001541 @classmethod
showardc5103442010-01-15 00:20:26 +00001542 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001543 """Schedules a special task on a host if not already scheduled.
1544
1545 @param cls: Implicit class object.
1546 @param host: The host to use.
1547 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00001548 """
showardc5103442010-01-15 00:20:26 +00001549 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
1550 is_active=False,
1551 is_complete=False)
1552 if existing_tasks:
1553 return existing_tasks[0]
1554
1555 special_task = SpecialTask(host=host, task=task,
1556 requested_by=User.current_user())
1557 special_task.save()
1558 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00001559
1560
showarded2afea2009-07-07 20:54:07 +00001561 def activate(self):
showard474d1362009-08-20 23:32:01 +00001562 """
1563 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00001564 """
showard97446882009-07-20 22:37:28 +00001565 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00001566 self.is_active = True
1567 self.time_started = datetime.now()
1568 self.save()
1569
1570
showarde60e44e2009-11-13 20:45:38 +00001571 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001572 """Sets a task as completed.
1573
1574 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00001575 """
showard97446882009-07-20 22:37:28 +00001576 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00001577 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00001578 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00001579 self.success = success
showard2fe3f1d2009-07-06 20:19:11 +00001580 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00001581
1582
1583 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001584 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00001585 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00001586
showard474d1362009-08-20 23:32:01 +00001587
showarda5288b42009-07-28 20:06:08 +00001588 def __unicode__(self):
1589 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00001590 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00001591 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00001592 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00001593 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00001594 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00001595
1596 return result