blob: 0b03110a5c470cb92e125e4d1a3ef04de4e0f868 [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
Aviv Keshetfa199002013-05-09 13:31:46 -07005import django.core
6try:
7 from django.db import models as dbmodels, connection
8except django.core.exceptions.ImproperlyConfigured:
9 raise ImportError('Django database not yet configured. Import either '
10 'setup_django_environment or '
11 'setup_django_lite_environment from '
12 'autotest_lib.frontend before any imports that '
13 'depend on django models.')
jamesren35a70222010-02-16 19:30:46 +000014from xml.sax import saxutils
showardcafd16e2009-05-29 18:37:49 +000015import common
jamesrendd855242010-03-02 22:23:44 +000016from autotest_lib.frontend.afe import model_logic, model_attributes
Prashanth B489b91d2014-03-15 12:17:16 -070017from autotest_lib.frontend.afe import rdb_model_extensions
showardcafd16e2009-05-29 18:37:49 +000018from autotest_lib.frontend import settings, thread_local
Jakob Juelich1b22ff22014-09-18 16:02:49 -070019from autotest_lib.client.common_lib import enum, error, host_protections
20from autotest_lib.client.common_lib import global_config
showardeaa408e2009-09-11 18:45:31 +000021from autotest_lib.client.common_lib import host_queue_entry_states
Jakob Juelich1b22ff22014-09-18 16:02:49 -070022from autotest_lib.client.common_lib import control_data, priorities, decorators
mblighe8819cd2008-02-15 16:48:40 +000023
showard0fc38302008-10-23 00:44:07 +000024# job options and user preferences
jamesrendd855242010-03-02 22:23:44 +000025DEFAULT_REBOOT_BEFORE = model_attributes.RebootBefore.IF_DIRTY
Dan Shi07e09af2013-04-12 09:31:29 -070026DEFAULT_REBOOT_AFTER = model_attributes.RebootBefore.NEVER
mblighe8819cd2008-02-15 16:48:40 +000027
showard89f84db2009-03-12 20:39:13 +000028
mblighe8819cd2008-02-15 16:48:40 +000029class AclAccessViolation(Exception):
jadmanski0afbb632008-06-06 21:10:57 +000030 """\
31 Raised when an operation is attempted with proper permissions as
32 dictated by ACLs.
33 """
mblighe8819cd2008-02-15 16:48:40 +000034
35
showard205fd602009-03-21 00:17:35 +000036class AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model):
showard89f84db2009-03-12 20:39:13 +000037 """\
38 An atomic group defines a collection of hosts which must only be scheduled
39 all at once. Any host with a label having an atomic group will only be
40 scheduled for a job at the same time as other hosts sharing that label.
41
42 Required:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080043 name: A name for this atomic group, e.g. 'rack23' or 'funky_net'.
showard89f84db2009-03-12 20:39:13 +000044 max_number_of_machines: The maximum number of machines that will be
45 scheduled at once when scheduling jobs to this atomic group.
46 The job.synch_count is considered the minimum.
47
48 Optional:
49 description: Arbitrary text description of this group's purpose.
50 """
showarda5288b42009-07-28 20:06:08 +000051 name = dbmodels.CharField(max_length=255, unique=True)
showard89f84db2009-03-12 20:39:13 +000052 description = dbmodels.TextField(blank=True)
showarde9450c92009-06-30 01:58:52 +000053 # This magic value is the default to simplify the scheduler logic.
54 # It must be "large". The common use of atomic groups is to want all
55 # machines in the group to be used, limits on which subset used are
56 # often chosen via dependency labels.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080057 # TODO(dennisjeffrey): Revisit this so we don't have to assume that
58 # "infinity" is around 3.3 million.
showarde9450c92009-06-30 01:58:52 +000059 INFINITE_MACHINES = 333333333
60 max_number_of_machines = dbmodels.IntegerField(default=INFINITE_MACHINES)
showard205fd602009-03-21 00:17:35 +000061 invalid = dbmodels.BooleanField(default=False,
showarda5288b42009-07-28 20:06:08 +000062 editable=settings.FULL_ADMIN)
showard89f84db2009-03-12 20:39:13 +000063
showard89f84db2009-03-12 20:39:13 +000064 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +000065 objects = model_logic.ModelWithInvalidManager()
showard205fd602009-03-21 00:17:35 +000066 valid_objects = model_logic.ValidObjectsManager()
67
68
showard29f7cd22009-04-29 21:16:24 +000069 def enqueue_job(self, job, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080070 """Enqueue a job on an associated atomic group of hosts.
71
72 @param job: A job to enqueue.
73 @param is_template: Whether the status should be "Template".
74 """
showard29f7cd22009-04-29 21:16:24 +000075 queue_entry = HostQueueEntry.create(atomic_group=self, job=job,
76 is_template=is_template)
showardc92da832009-04-07 18:14:34 +000077 queue_entry.save()
78
79
showard205fd602009-03-21 00:17:35 +000080 def clean_object(self):
81 self.label_set.clear()
showard89f84db2009-03-12 20:39:13 +000082
83
84 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080085 """Metadata for class AtomicGroup."""
showardeab66ce2009-12-23 00:03:56 +000086 db_table = 'afe_atomic_groups'
showard89f84db2009-03-12 20:39:13 +000087
showard205fd602009-03-21 00:17:35 +000088
showarda5288b42009-07-28 20:06:08 +000089 def __unicode__(self):
90 return unicode(self.name)
showard89f84db2009-03-12 20:39:13 +000091
92
showard7c785282008-05-29 19:45:12 +000093class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000094 """\
95 Required:
showard89f84db2009-03-12 20:39:13 +000096 name: label name
mblighe8819cd2008-02-15 16:48:40 +000097
jadmanski0afbb632008-06-06 21:10:57 +000098 Optional:
showard89f84db2009-03-12 20:39:13 +000099 kernel_config: URL/path to kernel config for jobs run on this label.
100 platform: If True, this is a platform label (defaults to False).
101 only_if_needed: If True, a Host with this label can only be used if that
102 label is requested by the job/test (either as the meta_host or
103 in the job_dependencies).
104 atomic_group: The atomic group associated with this label.
jadmanski0afbb632008-06-06 21:10:57 +0000105 """
showarda5288b42009-07-28 20:06:08 +0000106 name = dbmodels.CharField(max_length=255, unique=True)
107 kernel_config = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000108 platform = dbmodels.BooleanField(default=False)
109 invalid = dbmodels.BooleanField(default=False,
110 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +0000111 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +0000112
jadmanski0afbb632008-06-06 21:10:57 +0000113 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +0000114 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000115 valid_objects = model_logic.ValidObjectsManager()
showard89f84db2009-03-12 20:39:13 +0000116 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
117
mbligh5244cbb2008-04-24 20:39:52 +0000118
jadmanski0afbb632008-06-06 21:10:57 +0000119 def clean_object(self):
120 self.host_set.clear()
showard01a51672009-05-29 18:42:37 +0000121 self.test_set.clear()
mblighe8819cd2008-02-15 16:48:40 +0000122
123
showard29f7cd22009-04-29 21:16:24 +0000124 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800125 """Enqueue a job on any host of this label.
126
127 @param job: A job to enqueue.
128 @param atomic_group: The associated atomic group.
129 @param is_template: Whether the status should be "Template".
130 """
showard29f7cd22009-04-29 21:16:24 +0000131 queue_entry = HostQueueEntry.create(meta_host=self, job=job,
132 is_template=is_template,
133 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000134 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000135
136
jadmanski0afbb632008-06-06 21:10:57 +0000137 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800138 """Metadata for class Label."""
showardeab66ce2009-12-23 00:03:56 +0000139 db_table = 'afe_labels'
mblighe8819cd2008-02-15 16:48:40 +0000140
showarda5288b42009-07-28 20:06:08 +0000141 def __unicode__(self):
142 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000143
144
Jakob Jülich92c06332014-08-25 19:06:57 +0000145class Shard(dbmodels.Model, model_logic.ModelExtensions):
146
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700147 hostname = dbmodels.CharField(max_length=255, unique=True)
148
149 name_field = 'hostname'
150
Jakob Jülich92c06332014-08-25 19:06:57 +0000151 labels = dbmodels.ManyToManyField(Label, blank=True,
152 db_table='afe_shards_labels')
153
154 class Meta:
155 """Metadata for class ParameterizedJob."""
156 db_table = 'afe_shards'
157
158
jamesren76fcf192010-04-21 20:39:50 +0000159class Drone(dbmodels.Model, model_logic.ModelExtensions):
160 """
161 A scheduler drone
162
163 hostname: the drone's hostname
164 """
165 hostname = dbmodels.CharField(max_length=255, unique=True)
166
167 name_field = 'hostname'
168 objects = model_logic.ExtendedManager()
169
170
171 def save(self, *args, **kwargs):
172 if not User.current_user().is_superuser():
173 raise Exception('Only superusers may edit drones')
174 super(Drone, self).save(*args, **kwargs)
175
176
177 def delete(self):
178 if not User.current_user().is_superuser():
179 raise Exception('Only superusers may delete drones')
180 super(Drone, self).delete()
181
182
183 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800184 """Metadata for class Drone."""
jamesren76fcf192010-04-21 20:39:50 +0000185 db_table = 'afe_drones'
186
187 def __unicode__(self):
188 return unicode(self.hostname)
189
190
191class DroneSet(dbmodels.Model, model_logic.ModelExtensions):
192 """
193 A set of scheduler drones
194
195 These will be used by the scheduler to decide what drones a job is allowed
196 to run on.
197
198 name: the drone set's name
199 drones: the drones that are part of the set
200 """
201 DRONE_SETS_ENABLED = global_config.global_config.get_config_value(
202 'SCHEDULER', 'drone_sets_enabled', type=bool, default=False)
203 DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value(
204 'SCHEDULER', 'default_drone_set_name', default=None)
205
206 name = dbmodels.CharField(max_length=255, unique=True)
207 drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones')
208
209 name_field = 'name'
210 objects = model_logic.ExtendedManager()
211
212
213 def save(self, *args, **kwargs):
214 if not User.current_user().is_superuser():
215 raise Exception('Only superusers may edit drone sets')
216 super(DroneSet, self).save(*args, **kwargs)
217
218
219 def delete(self):
220 if not User.current_user().is_superuser():
221 raise Exception('Only superusers may delete drone sets')
222 super(DroneSet, self).delete()
223
224
225 @classmethod
226 def drone_sets_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800227 """Returns whether drone sets are enabled.
228
229 @param cls: Implicit class object.
230 """
jamesren76fcf192010-04-21 20:39:50 +0000231 return cls.DRONE_SETS_ENABLED
232
233
234 @classmethod
235 def default_drone_set_name(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800236 """Returns the default drone set name.
237
238 @param cls: Implicit class object.
239 """
jamesren76fcf192010-04-21 20:39:50 +0000240 return cls.DEFAULT_DRONE_SET_NAME
241
242
243 @classmethod
244 def get_default(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800245 """Gets the default drone set name, compatible with Job.add_object.
246
247 @param cls: Implicit class object.
248 """
jamesren76fcf192010-04-21 20:39:50 +0000249 return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME)
250
251
252 @classmethod
253 def resolve_name(cls, drone_set_name):
254 """
255 Returns the name of one of these, if not None, in order of preference:
256 1) the drone set given,
257 2) the current user's default drone set, or
258 3) the global default drone set
259
260 or returns None if drone sets are disabled
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800261
262 @param cls: Implicit class object.
263 @param drone_set_name: A drone set name.
jamesren76fcf192010-04-21 20:39:50 +0000264 """
265 if not cls.drone_sets_enabled():
266 return None
267
268 user = User.current_user()
269 user_drone_set_name = user.drone_set and user.drone_set.name
270
271 return drone_set_name or user_drone_set_name or cls.get_default().name
272
273
274 def get_drone_hostnames(self):
275 """
276 Gets the hostnames of all drones in this drone set
277 """
278 return set(self.drones.all().values_list('hostname', flat=True))
279
280
281 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800282 """Metadata for class DroneSet."""
jamesren76fcf192010-04-21 20:39:50 +0000283 db_table = 'afe_drone_sets'
284
285 def __unicode__(self):
286 return unicode(self.name)
287
288
showardfb2a7fa2008-07-17 17:04:12 +0000289class User(dbmodels.Model, model_logic.ModelExtensions):
290 """\
291 Required:
292 login :user login name
293
294 Optional:
295 access_level: 0=User (default), 1=Admin, 100=Root
296 """
297 ACCESS_ROOT = 100
298 ACCESS_ADMIN = 1
299 ACCESS_USER = 0
300
showard64a95952010-01-13 21:27:16 +0000301 AUTOTEST_SYSTEM = 'autotest_system'
302
showarda5288b42009-07-28 20:06:08 +0000303 login = dbmodels.CharField(max_length=255, unique=True)
showardfb2a7fa2008-07-17 17:04:12 +0000304 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
305
showard0fc38302008-10-23 00:44:07 +0000306 # user preferences
jamesrendd855242010-03-02 22:23:44 +0000307 reboot_before = dbmodels.SmallIntegerField(
308 choices=model_attributes.RebootBefore.choices(), blank=True,
309 default=DEFAULT_REBOOT_BEFORE)
310 reboot_after = dbmodels.SmallIntegerField(
311 choices=model_attributes.RebootAfter.choices(), blank=True,
312 default=DEFAULT_REBOOT_AFTER)
jamesren76fcf192010-04-21 20:39:50 +0000313 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
showard97db5ba2008-11-12 18:18:02 +0000314 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000315
showardfb2a7fa2008-07-17 17:04:12 +0000316 name_field = 'login'
317 objects = model_logic.ExtendedManager()
318
319
showarda5288b42009-07-28 20:06:08 +0000320 def save(self, *args, **kwargs):
showardfb2a7fa2008-07-17 17:04:12 +0000321 # is this a new object being saved for the first time?
322 first_time = (self.id is None)
323 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000324 if user and not user.is_superuser() and user.login != self.login:
325 raise AclAccessViolation("You cannot modify user " + self.login)
showarda5288b42009-07-28 20:06:08 +0000326 super(User, self).save(*args, **kwargs)
showardfb2a7fa2008-07-17 17:04:12 +0000327 if first_time:
328 everyone = AclGroup.objects.get(name='Everyone')
329 everyone.users.add(self)
330
331
332 def is_superuser(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800333 """Returns whether the user has superuser access."""
showardfb2a7fa2008-07-17 17:04:12 +0000334 return self.access_level >= self.ACCESS_ROOT
335
336
showard64a95952010-01-13 21:27:16 +0000337 @classmethod
338 def current_user(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800339 """Returns the current user.
340
341 @param cls: Implicit class object.
342 """
showard64a95952010-01-13 21:27:16 +0000343 user = thread_local.get_user()
344 if user is None:
showardcfcdd802010-01-15 00:16:33 +0000345 user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM)
showard64a95952010-01-13 21:27:16 +0000346 user.access_level = cls.ACCESS_ROOT
347 user.save()
348 return user
349
350
showardfb2a7fa2008-07-17 17:04:12 +0000351 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800352 """Metadata for class User."""
showardeab66ce2009-12-23 00:03:56 +0000353 db_table = 'afe_users'
showardfb2a7fa2008-07-17 17:04:12 +0000354
showarda5288b42009-07-28 20:06:08 +0000355 def __unicode__(self):
356 return unicode(self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000357
358
Prashanth B489b91d2014-03-15 12:17:16 -0700359class Host(model_logic.ModelWithInvalid, rdb_model_extensions.AbstractHostModel,
showardf8b19042009-05-12 17:22:49 +0000360 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000361 """\
362 Required:
363 hostname
mblighe8819cd2008-02-15 16:48:40 +0000364
jadmanski0afbb632008-06-06 21:10:57 +0000365 optional:
showard21baa452008-10-21 00:08:39 +0000366 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000367
jadmanski0afbb632008-06-06 21:10:57 +0000368 Internal:
Prashanth B489b91d2014-03-15 12:17:16 -0700369 From AbstractHostModel:
370 synch_id: currently unused
371 status: string describing status of host
372 invalid: true if the host has been deleted
373 protection: indicates what can be done to this host during repair
374 lock_time: DateTime at which the host was locked
375 dirty: true if the host has been used without being rebooted
376 Local:
377 locked_by: user that locked the host, or null if the host is unlocked
jadmanski0afbb632008-06-06 21:10:57 +0000378 """
mblighe8819cd2008-02-15 16:48:40 +0000379
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700380 SERIALIZATION_LINKS_TO_FOLLOW = set(['aclgroup_set',
381 'hostattribute_set',
382 'labels',
383 'shard'])
384
Jakob Juelichf88fa932014-09-03 17:58:04 -0700385
386 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700387 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -0700388 self.shard = Shard.deserialize(data)
389
390
Prashanth B489b91d2014-03-15 12:17:16 -0700391 # Note: Only specify foreign keys here, specify all native host columns in
392 # rdb_model_extensions instead.
393 Protection = host_protections.Protection
showardeab66ce2009-12-23 00:03:56 +0000394 labels = dbmodels.ManyToManyField(Label, blank=True,
395 db_table='afe_hosts_labels')
showardfb2a7fa2008-07-17 17:04:12 +0000396 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
jadmanski0afbb632008-06-06 21:10:57 +0000397 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000398 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000399 valid_objects = model_logic.ValidObjectsManager()
beepscc9fc702013-12-02 12:45:38 -0800400 leased_objects = model_logic.LeasedHostManager()
mbligh5244cbb2008-04-24 20:39:52 +0000401
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700402 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
showard2bab8f42008-11-12 18:15:22 +0000403
404 def __init__(self, *args, **kwargs):
405 super(Host, self).__init__(*args, **kwargs)
406 self._record_attributes(['status'])
407
408
showardb8471e32008-07-03 19:51:08 +0000409 @staticmethod
410 def create_one_time_host(hostname):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800411 """Creates a one-time host.
412
413 @param hostname: The name for the host.
414 """
showardb8471e32008-07-03 19:51:08 +0000415 query = Host.objects.filter(hostname=hostname)
416 if query.count() == 0:
417 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000418 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000419 else:
420 host = query[0]
421 if not host.invalid:
422 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000423 'hostname' : '%s already exists in the autotest DB. '
424 'Select it rather than entering it as a one time '
425 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000426 })
showard1ab512b2008-07-30 23:39:04 +0000427 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000428 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000429 host.save()
showard2924b0a2009-06-18 23:16:15 +0000430 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000431 return host
mbligh5244cbb2008-04-24 20:39:52 +0000432
showard1ff7b2e2009-05-15 23:17:18 +0000433
Jakob Juelich59cfe542014-09-02 16:37:46 -0700434 @classmethod
435 def assign_to_shard(cls, shard):
436 """Assigns hosts to a shard.
437
438 This function will check which labels are associated with the given
439 shard. It will assign those hosts, that have labels that are assigned
440 to the shard and haven't been returned to shard earlier.
441
442 @param shard: The shard object to assign labels/hosts for.
443 @returns the hosts objects that should be sent to the shard.
444 """
445
446 # Disclaimer: concurrent heartbeats should theoretically not occur in
447 # the current setup. As they may be introduced in the near future,
448 # this comment will be left here.
449
450 # Sending stuff twice is acceptable, but forgetting something isn't.
451 # Detecting duplicates on the client is easy, but here it's harder. The
452 # following options were considered:
453 # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
454 # than select returned, as concurrently more hosts might have been
455 # inserted
456 # - UPDATE and then SELECT WHERE shard=shard: select always returns all
457 # hosts for the shard, this is overhead
458 # - SELECT and then UPDATE only selected without requerying afterwards:
459 # returns the old state of the records.
460 host_ids = list(Host.objects.filter(
461 shard=None,
462 labels=shard.labels.all(),
463 leased=False
464 ).values_list('pk', flat=True))
465
466 if host_ids:
467 Host.objects.filter(pk__in=host_ids).update(shard=shard)
468 return list(Host.objects.filter(pk__in=host_ids).all())
469 return []
470
showardafd97de2009-10-01 18:45:09 +0000471 def resurrect_object(self, old_object):
472 super(Host, self).resurrect_object(old_object)
473 # invalid hosts can be in use by the scheduler (as one-time hosts), so
474 # don't change the status
475 self.status = old_object.status
476
477
jadmanski0afbb632008-06-06 21:10:57 +0000478 def clean_object(self):
479 self.aclgroup_set.clear()
480 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000481
482
showarda5288b42009-07-28 20:06:08 +0000483 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000484 # extra spaces in the hostname can be a sneaky source of errors
485 self.hostname = self.hostname.strip()
486 # is this a new object being saved for the first time?
487 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000488 if not first_time:
489 AclGroup.check_for_acl_violation_hosts([self])
showardfb2a7fa2008-07-17 17:04:12 +0000490 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000491 self.locked_by = User.current_user()
showardfb2a7fa2008-07-17 17:04:12 +0000492 self.lock_time = datetime.now()
showard21baa452008-10-21 00:08:39 +0000493 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000494 elif not self.locked and self.locked_by:
495 self.locked_by = None
496 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000497 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000498 if first_time:
499 everyone = AclGroup.objects.get(name='Everyone')
500 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000501 self._check_for_updated_attributes()
502
mblighe8819cd2008-02-15 16:48:40 +0000503
showardb8471e32008-07-03 19:51:08 +0000504 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000505 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000506 for queue_entry in self.hostqueueentry_set.all():
507 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000508 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000509 super(Host, self).delete()
510
mblighe8819cd2008-02-15 16:48:40 +0000511
showard2bab8f42008-11-12 18:15:22 +0000512 def on_attribute_changed(self, attribute, old_value):
513 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000514 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000515
516
showard29f7cd22009-04-29 21:16:24 +0000517 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800518 """Enqueue a job on this host.
519
520 @param job: A job to enqueue.
521 @param atomic_group: The associated atomic group.
522 @param is_template: Whther the status should be "Template".
523 """
showard29f7cd22009-04-29 21:16:24 +0000524 queue_entry = HostQueueEntry.create(host=self, job=job,
525 is_template=is_template,
526 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000527 # allow recovery of dead hosts from the frontend
528 if not self.active_queue_entry() and self.is_dead():
529 self.status = Host.Status.READY
530 self.save()
531 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000532
showard08f981b2008-06-24 21:59:03 +0000533 block = IneligibleHostQueue(job=job, host=self)
534 block.save()
535
mblighe8819cd2008-02-15 16:48:40 +0000536
jadmanski0afbb632008-06-06 21:10:57 +0000537 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800538 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000539 # TODO(showard): slighly hacky?
540 platforms = self.labels.filter(platform=True)
541 if len(platforms) == 0:
542 return None
543 return platforms[0]
544 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000545
546
showardcafd16e2009-05-29 18:37:49 +0000547 @classmethod
548 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800549 """Verify the specified hosts have no associated platforms.
550
551 @param cls: Implicit class object.
552 @param hosts: The hosts to verify.
553 @raises model_logic.ValidationError if any hosts already have a
554 platform.
555 """
showardcafd16e2009-05-29 18:37:49 +0000556 Host.objects.populate_relationships(hosts, Label, 'label_list')
557 errors = []
558 for host in hosts:
559 platforms = [label.name for label in host.label_list
560 if label.platform]
561 if platforms:
562 # do a join, just in case this host has multiple platforms,
563 # we'll be able to see it
564 errors.append('Host %s already has a platform: %s' % (
565 host.hostname, ', '.join(platforms)))
566 if errors:
567 raise model_logic.ValidationError({'labels': '; '.join(errors)})
568
569
jadmanski0afbb632008-06-06 21:10:57 +0000570 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800571 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000572 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000573
574
jadmanski0afbb632008-06-06 21:10:57 +0000575 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800576 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000577 active = list(self.hostqueueentry_set.filter(active=True))
578 if not active:
579 return None
580 assert len(active) == 1, ('More than one active entry for '
581 'host ' + self.hostname)
582 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000583
584
showardf8b19042009-05-12 17:22:49 +0000585 def _get_attribute_model_and_args(self, attribute):
586 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000587
588
jadmanski0afbb632008-06-06 21:10:57 +0000589 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800590 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000591 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000592
showarda5288b42009-07-28 20:06:08 +0000593 def __unicode__(self):
594 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000595
596
showard0957a842009-05-11 19:25:08 +0000597class HostAttribute(dbmodels.Model):
598 """Arbitrary keyvals associated with hosts."""
599 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000600 attribute = dbmodels.CharField(max_length=90)
601 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000602
603 objects = model_logic.ExtendedManager()
604
605 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800606 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000607 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000608
609
showard7c785282008-05-29 19:45:12 +0000610class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000611 """\
612 Required:
showard909c7a62008-07-15 21:52:38 +0000613 author: author name
614 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000615 name: test name
showard909c7a62008-07-15 21:52:38 +0000616 time: short, medium, long
617 test_class: This describes the class for your the test belongs in.
618 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000619 test_type: Client or Server
620 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000621 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
622 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000623 i.e. if sync_count = 2 it is a sync job that requires two
624 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000625 Optional:
showard909c7a62008-07-15 21:52:38 +0000626 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000627 dependency_labels: many-to-many relationship with labels corresponding to
628 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000629 experimental: If this is set to True production servers will ignore the test
630 run_verify: Whether or not the scheduler should run the verify stage
Dan Shi07e09af2013-04-12 09:31:29 -0700631 run_reset: Whether or not the scheduler should run the reset stage
Aviv Keshet9af96d32013-03-05 12:56:24 -0800632 test_retry: Number of times to retry test if the test did not complete
633 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +0000634 """
showard909c7a62008-07-15 21:52:38 +0000635 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000636
showarda5288b42009-07-28 20:06:08 +0000637 name = dbmodels.CharField(max_length=255, unique=True)
638 author = dbmodels.CharField(max_length=255)
639 test_class = dbmodels.CharField(max_length=255)
640 test_category = dbmodels.CharField(max_length=255)
641 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000642 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000643 experimental = dbmodels.BooleanField(default=True)
Dan Shi07e09af2013-04-12 09:31:29 -0700644 run_verify = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +0000645 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
646 default=TestTime.MEDIUM)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700647 test_type = dbmodels.SmallIntegerField(
648 choices=control_data.CONTROL_TYPE.choices())
showard909c7a62008-07-15 21:52:38 +0000649 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000650 path = dbmodels.CharField(max_length=255, unique=True)
Aviv Keshet9af96d32013-03-05 12:56:24 -0800651 test_retry = dbmodels.IntegerField(blank=True, default=0)
Dan Shi07e09af2013-04-12 09:31:29 -0700652 run_reset = dbmodels.BooleanField(default=True)
mblighe8819cd2008-02-15 16:48:40 +0000653
showardeab66ce2009-12-23 00:03:56 +0000654 dependency_labels = (
655 dbmodels.ManyToManyField(Label, blank=True,
656 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000657 name_field = 'name'
658 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000659
660
jamesren35a70222010-02-16 19:30:46 +0000661 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800662 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +0000663 escaped_description = saxutils.escape(self.description)
664 return '<span style="white-space:pre">%s</span>' % escaped_description
665 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000666 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000667
668
jadmanski0afbb632008-06-06 21:10:57 +0000669 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800670 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +0000671 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000672
showarda5288b42009-07-28 20:06:08 +0000673 def __unicode__(self):
674 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000675
676
jamesren4a41e012010-07-16 22:33:48 +0000677class TestParameter(dbmodels.Model):
678 """
679 A declared parameter of a test
680 """
681 test = dbmodels.ForeignKey(Test)
682 name = dbmodels.CharField(max_length=255)
683
684 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800685 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000686 db_table = 'afe_test_parameters'
687 unique_together = ('test', 'name')
688
689 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -0700690 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +0000691
692
showard2b9a88b2008-06-13 20:55:03 +0000693class Profiler(dbmodels.Model, model_logic.ModelExtensions):
694 """\
695 Required:
696 name: profiler name
697 test_type: Client or Server
698
699 Optional:
700 description: arbirary text description
701 """
showarda5288b42009-07-28 20:06:08 +0000702 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000703 description = dbmodels.TextField(blank=True)
704
705 name_field = 'name'
706 objects = model_logic.ExtendedManager()
707
708
709 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800710 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +0000711 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000712
showarda5288b42009-07-28 20:06:08 +0000713 def __unicode__(self):
714 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000715
716
showard7c785282008-05-29 19:45:12 +0000717class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000718 """\
719 Required:
720 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000721
jadmanski0afbb632008-06-06 21:10:57 +0000722 Optional:
723 description: arbitrary description of group
724 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700725
726 SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
727
showarda5288b42009-07-28 20:06:08 +0000728 name = dbmodels.CharField(max_length=255, unique=True)
729 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000730 users = dbmodels.ManyToManyField(User, blank=False,
731 db_table='afe_acl_groups_users')
732 hosts = dbmodels.ManyToManyField(Host, blank=True,
733 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000734
jadmanski0afbb632008-06-06 21:10:57 +0000735 name_field = 'name'
736 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000737
showard08f981b2008-06-24 21:59:03 +0000738 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000739 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800740 """Verify the current user has access to the specified hosts.
741
742 @param hosts: The hosts to verify against.
743 @raises AclAccessViolation if the current user doesn't have access
744 to a host.
745 """
showard64a95952010-01-13 21:27:16 +0000746 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000747 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000748 return
showard3dd47c22008-07-10 00:41:36 +0000749 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000750 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000751 for host in hosts:
752 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800753 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +0000754 no_access = (isinstance(host, Host)
755 and not host.invalid
756 and int(host.id) not in accessible_host_ids)
757 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000758 raise AclAccessViolation("%s does not have access to %s" %
759 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000760
showard9dbdcda2008-10-14 17:34:36 +0000761
762 @staticmethod
showarddc817512008-11-12 18:16:41 +0000763 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800764 """Look for queue entries that aren't abortable by the current user.
765
766 An entry is not abortable if:
767 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +0000768 * the machine isn't ACL-accessible, or
769 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800770
771 @param queue_entries: The queue entries to check.
772 @raises AclAccessViolation if a queue entry is not abortable by the
773 current user.
showarddc817512008-11-12 18:16:41 +0000774 """
showard64a95952010-01-13 21:27:16 +0000775 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000776 if user.is_superuser():
777 return
showarddc817512008-11-12 18:16:41 +0000778 not_owned = queue_entries.exclude(job__owner=user.login)
779 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000780 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
781 # 1.0.
782 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000783 accessible_ids = set(
784 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000785 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000786 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000787 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000788 cannot_abort = [entry for entry in not_owned.select_related()
789 if entry.id not in accessible_ids
790 or entry.id in public_ids]
791 if len(cannot_abort) == 0:
792 return
793 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000794 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000795 for entry in cannot_abort)
796 raise AclAccessViolation('You cannot abort the following job entries: '
797 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000798
799
showard3dd47c22008-07-10 00:41:36 +0000800 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800801 """Verifies the current user has acces to this ACL group.
802
803 @raises AclAccessViolation if the current user doesn't have access to
804 this ACL group.
805 """
showard64a95952010-01-13 21:27:16 +0000806 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000807 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000808 return
809 if self.name == 'Everyone':
810 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000811 if not user in self.users.all():
812 raise AclAccessViolation("You do not have access to %s"
813 % self.name)
814
815 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000816 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800817 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +0000818 everyone = AclGroup.objects.get(name='Everyone')
819
showard3dd47c22008-07-10 00:41:36 +0000820 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000821 # TODO(showard): this is a bit of a hack, since the fact that this query
822 # works is kind of a coincidence of Django internals. This trick
823 # doesn't work in general (on all foreign key relationships). I'll
824 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000825 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000826 everyone.hosts.add(*orphaned_hosts.distinct())
827
828 # find hosts in both Everyone and another ACL group, and remove them
829 # from Everyone
showarda5288b42009-07-28 20:06:08 +0000830 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
831 acled_hosts = set()
832 for host in hosts_in_everyone:
833 # Has an ACL group other than Everyone
834 if host.aclgroup_set.count() > 1:
835 acled_hosts.add(host)
836 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +0000837
838
839 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000840 if (self.name == 'Everyone'):
841 raise AclAccessViolation("You cannot delete 'Everyone'!")
842 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000843 super(AclGroup, self).delete()
844 self.on_host_membership_change()
845
846
showard04f2cd82008-07-25 20:53:31 +0000847 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800848 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +0000849 if not self.users.count():
showard64a95952010-01-13 21:27:16 +0000850 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +0000851
852
showard8cbaf1e2009-09-08 16:27:04 +0000853 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800854 """Called after a save.
855
856 @param change: Whether there was a change.
857 """
showard8cbaf1e2009-09-08 16:27:04 +0000858 if not change:
showard64a95952010-01-13 21:27:16 +0000859 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +0000860 self.add_current_user_if_empty()
861 self.on_host_membership_change()
862
863
864 def save(self, *args, **kwargs):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700865 change = bool(self.id)
866 if change:
showard8cbaf1e2009-09-08 16:27:04 +0000867 # Check the original object for an ACL violation
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700868 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
showard8cbaf1e2009-09-08 16:27:04 +0000869 super(AclGroup, self).save(*args, **kwargs)
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700870 self.perform_after_save(change)
showard8cbaf1e2009-09-08 16:27:04 +0000871
showardeb3be4d2008-04-21 20:59:26 +0000872
jadmanski0afbb632008-06-06 21:10:57 +0000873 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800874 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +0000875 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000876
showarda5288b42009-07-28 20:06:08 +0000877 def __unicode__(self):
878 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000879
mblighe8819cd2008-02-15 16:48:40 +0000880
jamesren4a41e012010-07-16 22:33:48 +0000881class Kernel(dbmodels.Model):
882 """
883 A kernel configuration for a parameterized job
884 """
885 version = dbmodels.CharField(max_length=255)
886 cmdline = dbmodels.CharField(max_length=255, blank=True)
887
888 @classmethod
889 def create_kernels(cls, kernel_list):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800890 """Creates all kernels in the kernel list.
jamesren4a41e012010-07-16 22:33:48 +0000891
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800892 @param cls: Implicit class object.
893 @param kernel_list: A list of dictionaries that describe the kernels,
894 in the same format as the 'kernel' argument to
895 rpc_interface.generate_control_file.
896 @return A list of the created kernels.
jamesren4a41e012010-07-16 22:33:48 +0000897 """
898 if not kernel_list:
899 return None
900 return [cls._create(kernel) for kernel in kernel_list]
901
902
903 @classmethod
904 def _create(cls, kernel_dict):
905 version = kernel_dict.pop('version')
906 cmdline = kernel_dict.pop('cmdline', '')
907
908 if kernel_dict:
909 raise Exception('Extraneous kernel arguments remain: %r'
910 % kernel_dict)
911
912 kernel, _ = cls.objects.get_or_create(version=version,
913 cmdline=cmdline)
914 return kernel
915
916
917 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800918 """Metadata for class Kernel."""
jamesren4a41e012010-07-16 22:33:48 +0000919 db_table = 'afe_kernels'
920 unique_together = ('version', 'cmdline')
921
922 def __unicode__(self):
923 return u'%s %s' % (self.version, self.cmdline)
924
925
926class ParameterizedJob(dbmodels.Model):
927 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800928 Auxiliary configuration for a parameterized job.
jamesren4a41e012010-07-16 22:33:48 +0000929 """
930 test = dbmodels.ForeignKey(Test)
931 label = dbmodels.ForeignKey(Label, null=True)
932 use_container = dbmodels.BooleanField(default=False)
933 profile_only = dbmodels.BooleanField(default=False)
934 upload_kernel_config = dbmodels.BooleanField(default=False)
935
936 kernels = dbmodels.ManyToManyField(
937 Kernel, db_table='afe_parameterized_job_kernels')
938 profilers = dbmodels.ManyToManyField(
939 Profiler, through='ParameterizedJobProfiler')
940
941
942 @classmethod
943 def smart_get(cls, id_or_name, *args, **kwargs):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800944 """For compatibility with Job.add_object.
945
946 @param cls: Implicit class object.
947 @param id_or_name: The ID or name to get.
948 @param args: Non-keyword arguments.
949 @param kwargs: Keyword arguments.
950 """
jamesren4a41e012010-07-16 22:33:48 +0000951 return cls.objects.get(pk=id_or_name)
952
953
954 def job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800955 """Returns the job if it exists, or else None."""
jamesren4a41e012010-07-16 22:33:48 +0000956 jobs = self.job_set.all()
957 assert jobs.count() <= 1
958 return jobs and jobs[0] or None
959
960
961 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800962 """Metadata for class ParameterizedJob."""
jamesren4a41e012010-07-16 22:33:48 +0000963 db_table = 'afe_parameterized_jobs'
964
965 def __unicode__(self):
966 return u'%s (parameterized) - %s' % (self.test.name, self.job())
967
968
969class ParameterizedJobProfiler(dbmodels.Model):
970 """
971 A profiler to run on a parameterized job
972 """
973 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
974 profiler = dbmodels.ForeignKey(Profiler)
975
976 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800977 """Metedata for class ParameterizedJobProfiler."""
jamesren4a41e012010-07-16 22:33:48 +0000978 db_table = 'afe_parameterized_jobs_profilers'
979 unique_together = ('parameterized_job', 'profiler')
980
981
982class ParameterizedJobProfilerParameter(dbmodels.Model):
983 """
984 A parameter for a profiler in a parameterized job
985 """
986 parameterized_job_profiler = dbmodels.ForeignKey(ParameterizedJobProfiler)
987 parameter_name = dbmodels.CharField(max_length=255)
988 parameter_value = dbmodels.TextField()
989 parameter_type = dbmodels.CharField(
990 max_length=8, choices=model_attributes.ParameterTypes.choices())
991
992 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800993 """Metadata for class ParameterizedJobProfilerParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000994 db_table = 'afe_parameterized_job_profiler_parameters'
995 unique_together = ('parameterized_job_profiler', 'parameter_name')
996
997 def __unicode__(self):
998 return u'%s - %s' % (self.parameterized_job_profiler.profiler.name,
999 self.parameter_name)
1000
1001
1002class ParameterizedJobParameter(dbmodels.Model):
1003 """
1004 Parameters for a parameterized job
1005 """
1006 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1007 test_parameter = dbmodels.ForeignKey(TestParameter)
1008 parameter_value = dbmodels.TextField()
1009 parameter_type = dbmodels.CharField(
1010 max_length=8, choices=model_attributes.ParameterTypes.choices())
1011
1012 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001013 """Metadata for class ParameterizedJobParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001014 db_table = 'afe_parameterized_job_parameters'
1015 unique_together = ('parameterized_job', 'test_parameter')
1016
1017 def __unicode__(self):
1018 return u'%s - %s' % (self.parameterized_job.job().name,
1019 self.test_parameter.name)
1020
1021
showard7c785282008-05-29 19:45:12 +00001022class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +00001023 'Custom manager to provide efficient status counts querying.'
1024 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001025 """Returns a dict mapping the given job IDs to their status count dicts.
1026
1027 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +00001028 """
1029 if not job_ids:
1030 return {}
1031 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1032 cursor = connection.cursor()
1033 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +00001034 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +00001035 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +00001036 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +00001037 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +00001038 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +00001039 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +00001040 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +00001041 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +00001042 full_status = HostQueueEntry.compute_full_status(status, aborted,
1043 complete)
showardb6d16622009-05-26 19:35:29 +00001044 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +00001045 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +00001046 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +00001047
1048
showard7c785282008-05-29 19:45:12 +00001049class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001050 """\
1051 owner: username of job owner
1052 name: job name (does not have to be unique)
Alex Miller7d658cf2013-09-04 16:00:35 -07001053 priority: Integer priority value. Higher is more important.
jadmanski0afbb632008-06-06 21:10:57 +00001054 control_file: contents of control file
1055 control_type: Client or Server
1056 created_on: date of job creation
1057 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +00001058 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +00001059 run_verify: Whether or not to run the verify phase
Dan Shi07e09af2013-04-12 09:31:29 -07001060 run_reset: Whether or not to run the reset phase
Simran Basi94dc0032013-11-12 14:09:46 -08001061 timeout: DEPRECATED - hours from queuing time until job times out
1062 timeout_mins: minutes from job queuing time until the job times out
Simran Basi34217022012-11-06 13:43:15 -08001063 max_runtime_hrs: DEPRECATED - hours from job starting time until job
1064 times out
1065 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +00001066 email_list: list of people to email on completion delimited by any of:
1067 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +00001068 dependency_labels: many-to-many relationship with labels corresponding to
1069 job dependencies
showard21baa452008-10-21 00:08:39 +00001070 reboot_before: Never, If dirty, or Always
1071 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001072 parse_failed_repair: if True, a failed repair launched by this job will have
1073 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001074 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001075 parent_job: Parent job (optional)
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001076 test_retry: Number of times to retry test if the test did not complete
1077 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +00001078 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001079
1080 # TODO: Investigate, if jobkeyval_set is really needed.
1081 # dynamic_suite will write them into an attached file for the drone, but
1082 # it doesn't seem like they are actually used. If they aren't used, remove
1083 # jobkeyval_set here.
1084 SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1085 'hostqueueentry_set',
1086 'jobkeyval_set',
1087 'shard'])
1088
1089
Jakob Juelichf88fa932014-09-03 17:58:04 -07001090 def _deserialize_relation(self, link, data):
1091 if link in ['hostqueueentry_set', 'jobkeyval_set']:
1092 for obj in data:
1093 obj['job_id'] = self.id
1094
1095 super(Job, self)._deserialize_relation(link, data)
1096
1097
1098 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001099 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -07001100 self.shard = Shard.deserialize(data)
1101
1102
Jakob Juelich1b22ff22014-09-18 16:02:49 -07001103 def sanity_check_update_from_shard(self, shard, updated_serialized):
1104 if not self.shard_id == shard.id:
1105 raise error.UnallowedRecordsSentToMaster(
1106 'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1107 'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1108 shard.id))
1109
1110
Simran Basi94dc0032013-11-12 14:09:46 -08001111 # TIMEOUT is deprecated.
showardb1e51872008-10-07 11:08:18 +00001112 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
Simran Basi94dc0032013-11-12 14:09:46 -08001113 'AUTOTEST_WEB', 'job_timeout_default', default=24)
1114 DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1115 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
Simran Basi34217022012-11-06 13:43:15 -08001116 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1117 # completed.
showard12f3e322009-05-13 21:27:42 +00001118 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1119 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001120 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1121 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001122 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1123 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1124 default=False)
showardb1e51872008-10-07 11:08:18 +00001125
showarda5288b42009-07-28 20:06:08 +00001126 owner = dbmodels.CharField(max_length=255)
1127 name = dbmodels.CharField(max_length=255)
Alex Miller7d658cf2013-09-04 16:00:35 -07001128 priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
jamesren4a41e012010-07-16 22:33:48 +00001129 control_file = dbmodels.TextField(null=True, blank=True)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001130 control_type = dbmodels.SmallIntegerField(
1131 choices=control_data.CONTROL_TYPE.choices(),
1132 blank=True, # to allow 0
1133 default=control_data.CONTROL_TYPE.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001134 created_on = dbmodels.DateTimeField()
Simran Basi8d89b642014-05-02 17:33:20 -07001135 synch_count = dbmodels.IntegerField(blank=True, default=0)
showardb1e51872008-10-07 11:08:18 +00001136 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
Dan Shi07e09af2013-04-12 09:31:29 -07001137 run_verify = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001138 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001139 dependency_labels = (
1140 dbmodels.ManyToManyField(Label, blank=True,
1141 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001142 reboot_before = dbmodels.SmallIntegerField(
1143 choices=model_attributes.RebootBefore.choices(), blank=True,
1144 default=DEFAULT_REBOOT_BEFORE)
1145 reboot_after = dbmodels.SmallIntegerField(
1146 choices=model_attributes.RebootAfter.choices(), blank=True,
1147 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001148 parse_failed_repair = dbmodels.BooleanField(
1149 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001150 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1151 # completed.
showard12f3e322009-05-13 21:27:42 +00001152 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001153 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001154 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001155
jamesren4a41e012010-07-16 22:33:48 +00001156 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1157 blank=True)
1158
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001159 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001160
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001161 test_retry = dbmodels.IntegerField(blank=True, default=0)
1162
Dan Shi07e09af2013-04-12 09:31:29 -07001163 run_reset = dbmodels.BooleanField(default=True)
1164
Simran Basi94dc0032013-11-12 14:09:46 -08001165 timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1166
Jakob Jülich92c06332014-08-25 19:06:57 +00001167 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1168
jadmanski0afbb632008-06-06 21:10:57 +00001169 # custom manager
1170 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001171
1172
Alex Millerec212252014-02-28 16:48:34 -08001173 @decorators.cached_property
1174 def labels(self):
1175 """All the labels of this job"""
1176 # We need to convert dependency_labels to a list, because all() gives us
1177 # back an iterator, and storing/caching an iterator means we'd only be
1178 # able to read from it once.
1179 return list(self.dependency_labels.all())
1180
1181
jadmanski0afbb632008-06-06 21:10:57 +00001182 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001183 """Returns whether this job is of type server."""
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001184 return self.control_type == control_data.CONTROL_TYPE.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001185
1186
jadmanski0afbb632008-06-06 21:10:57 +00001187 @classmethod
jamesren4a41e012010-07-16 22:33:48 +00001188 def parameterized_jobs_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001189 """Returns whether parameterized jobs are enabled.
1190
1191 @param cls: Implicit class object.
1192 """
jamesren4a41e012010-07-16 22:33:48 +00001193 return global_config.global_config.get_config_value(
1194 'AUTOTEST_WEB', 'parameterized_jobs', type=bool)
1195
1196
1197 @classmethod
1198 def check_parameterized_job(cls, control_file, parameterized_job):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001199 """Checks that the job is valid given the global config settings.
jamesren4a41e012010-07-16 22:33:48 +00001200
1201 First, either control_file must be set, or parameterized_job must be
1202 set, but not both. Second, parameterized_job must be set if and only if
1203 the parameterized_jobs option in the global config is set to True.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001204
1205 @param cls: Implict class object.
1206 @param control_file: A control file.
1207 @param parameterized_job: A parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001208 """
1209 if not (bool(control_file) ^ bool(parameterized_job)):
1210 raise Exception('Job must have either control file or '
1211 'parameterization, but not both')
1212
1213 parameterized_jobs_enabled = cls.parameterized_jobs_enabled()
1214 if control_file and parameterized_jobs_enabled:
1215 raise Exception('Control file specified, but parameterized jobs '
1216 'are enabled')
1217 if parameterized_job and not parameterized_jobs_enabled:
1218 raise Exception('Parameterized job specified, but parameterized '
1219 'jobs are not enabled')
1220
1221
1222 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001223 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001224 """Creates a job.
1225
1226 The job is created by taking some information (the listed args) and
1227 filling in the rest of the necessary information.
1228
1229 @param cls: Implicit class object.
1230 @param owner: The owner for the job.
1231 @param options: An options object.
1232 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001233 """
showard3dd47c22008-07-10 00:41:36 +00001234 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001235
jamesren4a41e012010-07-16 22:33:48 +00001236 control_file = options.get('control_file')
1237 parameterized_job = options.get('parameterized_job')
jamesren4a41e012010-07-16 22:33:48 +00001238
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001239 # The current implementation of parameterized jobs requires that only
1240 # control files or parameterized jobs are used. Using the image
1241 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1242 # parameterized jobs and control files jobs, it does muck enough with
1243 # normal jobs by adding a parameterized id to them that this check will
1244 # fail. So for now we just skip this check.
1245 # cls.check_parameterized_job(control_file=control_file,
1246 # parameterized_job=parameterized_job)
showard4d233752010-01-20 19:06:40 +00001247 user = User.current_user()
1248 if options.get('reboot_before') is None:
1249 options['reboot_before'] = user.get_reboot_before_display()
1250 if options.get('reboot_after') is None:
1251 options['reboot_after'] = user.get_reboot_after_display()
1252
jamesren76fcf192010-04-21 20:39:50 +00001253 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1254
Simran Basi94dc0032013-11-12 14:09:46 -08001255 if options.get('timeout_mins') is None and options.get('timeout'):
1256 options['timeout_mins'] = options['timeout'] * 60
1257
jadmanski0afbb632008-06-06 21:10:57 +00001258 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001259 owner=owner,
1260 name=options['name'],
1261 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001262 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001263 control_type=options['control_type'],
1264 synch_count=options.get('synch_count'),
Simran Basi94dc0032013-11-12 14:09:46 -08001265 # timeout needs to be deleted in the future.
showarda1e74b32009-05-12 17:32:04 +00001266 timeout=options.get('timeout'),
Simran Basi94dc0032013-11-12 14:09:46 -08001267 timeout_mins=options.get('timeout_mins'),
Simran Basi34217022012-11-06 13:43:15 -08001268 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001269 run_verify=options.get('run_verify'),
1270 email_list=options.get('email_list'),
1271 reboot_before=options.get('reboot_before'),
1272 reboot_after=options.get('reboot_after'),
1273 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001274 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001275 drone_set=drone_set,
Aviv Keshet18308922013-02-19 17:49:49 -08001276 parameterized_job=parameterized_job,
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001277 parent_job=options.get('parent_job_id'),
Dan Shi07e09af2013-04-12 09:31:29 -07001278 test_retry=options.get('test_retry'),
1279 run_reset=options.get('run_reset'))
mblighe8819cd2008-02-15 16:48:40 +00001280
showarda1e74b32009-05-12 17:32:04 +00001281 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001282
jamesrend8b6e172010-04-16 23:45:00 +00001283 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001284 for key, value in options['keyvals'].iteritems():
1285 JobKeyval.objects.create(job=job, key=key, value=value)
1286
jadmanski0afbb632008-06-06 21:10:57 +00001287 return job
mblighe8819cd2008-02-15 16:48:40 +00001288
1289
Jakob Juelich59cfe542014-09-02 16:37:46 -07001290 @classmethod
1291 def assign_to_shard(cls, shard):
1292 """Assigns unassigned jobs to a shard.
1293
1294 All jobs that have the platform label that was assigned to the given
1295 shard are assigned to the shard and returned.
1296 @param shard: The shard to assign jobs to.
1297 @returns The job objects that should be sent to the shard.
1298 """
1299 # Disclaimer: Concurrent heartbeats should not occur in today's setup.
1300 # If this changes or they are triggered manually, this applies:
1301 # Jobs may be returned more than once by concurrent calls of this
1302 # function, as there is a race condition between SELECT and UPDATE.
1303 job_ids = list(Job.objects.filter(
1304 shard=None,
1305 dependency_labels=shard.labels.all()
1306 ).exclude(
1307 hostqueueentry__complete=True
1308 ).exclude(
1309 hostqueueentry__active=True
1310 ).values_list('pk', flat=True))
1311 if job_ids:
1312 Job.objects.filter(pk__in=job_ids).update(shard=shard)
1313 return list(Job.objects.filter(pk__in=job_ids).all())
1314 return []
1315
1316
jamesren4a41e012010-07-16 22:33:48 +00001317 def save(self, *args, **kwargs):
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001318 # The current implementation of parameterized jobs requires that only
1319 # control files or parameterized jobs are used. Using the image
1320 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1321 # parameterized jobs and control files jobs, it does muck enough with
1322 # normal jobs by adding a parameterized id to them that this check will
1323 # fail. So for now we just skip this check.
1324 # cls.check_parameterized_job(control_file=self.control_file,
1325 # parameterized_job=self.parameterized_job)
jamesren4a41e012010-07-16 22:33:48 +00001326 super(Job, self).save(*args, **kwargs)
1327
1328
showard29f7cd22009-04-29 21:16:24 +00001329 def queue(self, hosts, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001330 """Enqueue a job on the given hosts.
1331
1332 @param hosts: The hosts to use.
1333 @param atomic_group: The associated atomic group.
1334 @param is_template: Whether the status should be "Template".
1335 """
showarda9545c02009-12-18 22:44:26 +00001336 if not hosts:
1337 if atomic_group:
1338 # No hosts or labels are required to queue an atomic group
1339 # Job. However, if they are given, we respect them below.
1340 atomic_group.enqueue_job(self, is_template=is_template)
1341 else:
1342 # hostless job
1343 entry = HostQueueEntry.create(job=self, is_template=is_template)
1344 entry.save()
1345 return
1346
jadmanski0afbb632008-06-06 21:10:57 +00001347 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +00001348 host.enqueue_job(self, atomic_group=atomic_group,
1349 is_template=is_template)
1350
1351
1352 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001353 """Creates a recurring job.
1354
1355 @param start_date: The starting date of the job.
1356 @param loop_period: How often to re-run the job, in seconds.
1357 @param loop_count: The re-run count.
1358 @param owner: The owner of the job.
1359 """
showard29f7cd22009-04-29 21:16:24 +00001360 rec = RecurringRun(job=self, start_date=start_date,
1361 loop_period=loop_period,
1362 loop_count=loop_count,
1363 owner=User.objects.get(login=owner))
1364 rec.save()
1365 return rec.id
mblighe8819cd2008-02-15 16:48:40 +00001366
1367
jadmanski0afbb632008-06-06 21:10:57 +00001368 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001369 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001370 try:
1371 return User.objects.get(login=self.owner)
1372 except self.DoesNotExist:
1373 return None
mblighe8819cd2008-02-15 16:48:40 +00001374
1375
showard64a95952010-01-13 21:27:16 +00001376 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001377 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001378 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001379 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001380
1381
showardd1195652009-12-08 22:21:02 +00001382 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001383 """Returns a string tag for this job."""
showardd1195652009-12-08 22:21:02 +00001384 return '%s-%s' % (self.id, self.owner)
1385
1386
showardc1a98d12010-01-15 00:22:22 +00001387 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001388 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001389 return dict((keyval.key, keyval.value)
1390 for keyval in self.jobkeyval_set.all())
1391
1392
jadmanski0afbb632008-06-06 21:10:57 +00001393 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001394 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001395 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001396
showarda5288b42009-07-28 20:06:08 +00001397 def __unicode__(self):
1398 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001399
1400
showardc1a98d12010-01-15 00:22:22 +00001401class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1402 """Keyvals associated with jobs"""
1403 job = dbmodels.ForeignKey(Job)
1404 key = dbmodels.CharField(max_length=90)
1405 value = dbmodels.CharField(max_length=300)
1406
1407 objects = model_logic.ExtendedManager()
1408
1409 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001410 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001411 db_table = 'afe_job_keyvals'
1412
1413
showard7c785282008-05-29 19:45:12 +00001414class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001415 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001416 job = dbmodels.ForeignKey(Job)
1417 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001418
jadmanski0afbb632008-06-06 21:10:57 +00001419 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001420
jadmanski0afbb632008-06-06 21:10:57 +00001421 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001422 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001423 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001424
mblighe8819cd2008-02-15 16:48:40 +00001425
showard7c785282008-05-29 19:45:12 +00001426class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001427 """Represents a host queue entry."""
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001428
1429 SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
1430
Jakob Juelichf88fa932014-09-03 17:58:04 -07001431
1432 def custom_deserialize_relation(self, link, data):
1433 assert link == 'meta_host'
1434 self.meta_host = Label.deserialize(data)
1435
1436
Jakob Juelich1b22ff22014-09-18 16:02:49 -07001437 def sanity_check_update_from_shard(self, shard, updated_serialized,
1438 job_ids_sent):
1439 if self.job_id not in job_ids_sent:
1440 raise error.UnallowedRecordsSentToMaster(
1441 'Sent HostQueueEntry without corresponding '
1442 'job entry: %s' % updated_serialized)
1443
1444
showardeaa408e2009-09-11 18:45:31 +00001445 Status = host_queue_entry_states.Status
1446 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001447 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001448
jadmanski0afbb632008-06-06 21:10:57 +00001449 job = dbmodels.ForeignKey(Job)
1450 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001451 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001452 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1453 db_column='meta_host')
1454 active = dbmodels.BooleanField(default=False)
1455 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001456 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001457 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1458 default='')
showard89f84db2009-03-12 20:39:13 +00001459 # If atomic_group is set, this is a virtual HostQueueEntry that will
1460 # be expanded into many actual hosts within the group at schedule time.
1461 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001462 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001463 started_on = dbmodels.DateTimeField(null=True, blank=True)
Fang Deng51599032014-06-23 17:24:27 -07001464 finished_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001465
jadmanski0afbb632008-06-06 21:10:57 +00001466 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001467
mblighe8819cd2008-02-15 16:48:40 +00001468
showard2bab8f42008-11-12 18:15:22 +00001469 def __init__(self, *args, **kwargs):
1470 super(HostQueueEntry, self).__init__(*args, **kwargs)
1471 self._record_attributes(['status'])
1472
1473
showard29f7cd22009-04-29 21:16:24 +00001474 @classmethod
1475 def create(cls, job, host=None, meta_host=None, atomic_group=None,
1476 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001477 """Creates a new host queue entry.
1478
1479 @param cls: Implicit class object.
1480 @param job: The associated job.
1481 @param host: The associated host.
1482 @param meta_host: The associated meta host.
1483 @param atomic_group: The associated atomic group.
1484 @param is_template: Whether the status should be "Template".
1485 """
showard29f7cd22009-04-29 21:16:24 +00001486 if is_template:
1487 status = cls.Status.TEMPLATE
1488 else:
1489 status = cls.Status.QUEUED
1490
1491 return cls(job=job, host=host, meta_host=meta_host,
1492 atomic_group=atomic_group, status=status)
1493
1494
showarda5288b42009-07-28 20:06:08 +00001495 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001496 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001497 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001498 self._check_for_updated_attributes()
1499
1500
showardc0ac3a72009-07-08 21:14:45 +00001501 def execution_path(self):
1502 """
1503 Path to this entry's results (relative to the base results directory).
1504 """
showardd1195652009-12-08 22:21:02 +00001505 return os.path.join(self.job.tag(), self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001506
1507
showard3f15eed2008-11-14 22:40:48 +00001508 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001509 """Returns the first non-None name found in priority order.
1510
1511 The priority order checked is: (1) host name; (2) meta host name; and
1512 (3) atomic group name.
1513 """
showard3f15eed2008-11-14 22:40:48 +00001514 if self.host:
1515 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001516 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +00001517 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +00001518 else:
1519 assert self.atomic_group, "no host, meta_host or atomic group!"
1520 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +00001521
1522
showard2bab8f42008-11-12 18:15:22 +00001523 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001524 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001525 self.active, self.complete = True, False
1526 elif self.status in self.COMPLETE_STATUSES:
1527 self.active, self.complete = False, True
1528 else:
1529 self.active, self.complete = False, False
1530
1531
1532 def on_attribute_changed(self, attribute, old_value):
1533 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001534 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1535 self.status)
showard2bab8f42008-11-12 18:15:22 +00001536
1537
jadmanski0afbb632008-06-06 21:10:57 +00001538 def is_meta_host_entry(self):
1539 'True if this is a entry has a meta_host instead of a host.'
1540 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001541
showarda3ab0d52008-11-03 19:03:47 +00001542
Simran Basic1b26762013-06-26 14:23:21 -07001543 # This code is shared between rpc_interface and models.HostQueueEntry.
1544 # Sadly due to circular imports between the 2 (crbug.com/230100) making it
1545 # a class method was the best way to refactor it. Attempting to put it in
1546 # rpc_utils or a new utils module failed as that would require us to import
1547 # models.py but to call it from here we would have to import the utils.py
1548 # thus creating a cycle.
1549 @classmethod
1550 def abort_host_queue_entries(cls, host_queue_entries):
1551 """Aborts a collection of host_queue_entries.
1552
1553 Abort these host queue entry and all host queue entries of jobs created
1554 by them.
1555
1556 @param host_queue_entries: List of host queue entries we want to abort.
1557 """
1558 # This isn't completely immune to race conditions since it's not atomic,
1559 # but it should be safe given the scheduler's behavior.
1560
1561 # TODO(milleral): crbug.com/230100
1562 # The |abort_host_queue_entries| rpc does nearly exactly this,
1563 # however, trying to re-use the code generates some horrible
1564 # circular import error. I'd be nice to refactor things around
1565 # sometime so the code could be reused.
1566
1567 # Fixpoint algorithm to find the whole tree of HQEs to abort to
1568 # minimize the total number of database queries:
1569 children = set()
1570 new_children = set(host_queue_entries)
1571 while new_children:
1572 children.update(new_children)
1573 new_child_ids = [hqe.job_id for hqe in new_children]
1574 new_children = HostQueueEntry.objects.filter(
1575 job__parent_job__in=new_child_ids,
1576 complete=False, aborted=False).all()
1577 # To handle circular parental relationships
1578 new_children = set(new_children) - children
1579
1580 # Associate a user with the host queue entries that we're about
1581 # to abort so that we can look up who to blame for the aborts.
1582 now = datetime.now()
1583 user = User.current_user()
1584 aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
1585 aborted_by=user, aborted_on=now) for hqe in children]
1586 AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
1587 # Bulk update all of the HQEs to set the abort bit.
1588 child_ids = [hqe.id for hqe in children]
1589 HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
1590
1591
Scott Zawalski23041432013-04-17 07:39:09 -07001592 def abort(self):
Alex Millerdea67042013-04-22 17:23:34 -07001593 """ Aborts this host queue entry.
Simran Basic1b26762013-06-26 14:23:21 -07001594
Alex Millerdea67042013-04-22 17:23:34 -07001595 Abort this host queue entry and all host queue entries of jobs created by
1596 this one.
1597
1598 """
showardd3dc1992009-04-22 21:01:40 +00001599 if not self.complete and not self.aborted:
Simran Basi97582a22013-06-27 12:03:21 -07001600 HostQueueEntry.abort_host_queue_entries([self])
mblighe8819cd2008-02-15 16:48:40 +00001601
showardd3dc1992009-04-22 21:01:40 +00001602
1603 @classmethod
1604 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001605 """Returns a modified status msg if the host queue entry was aborted.
1606
1607 @param cls: Implicit class object.
1608 @param status: The original status message.
1609 @param aborted: Whether the host queue entry was aborted.
1610 @param complete: Whether the host queue entry was completed.
1611 """
showardd3dc1992009-04-22 21:01:40 +00001612 if aborted and not complete:
1613 return 'Aborted (%s)' % status
1614 return status
1615
1616
1617 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001618 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00001619 return self.compute_full_status(self.status, self.aborted,
1620 self.complete)
1621
1622
1623 def _postprocess_object_dict(self, object_dict):
1624 object_dict['full_status'] = self.full_status()
1625
1626
jadmanski0afbb632008-06-06 21:10:57 +00001627 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001628 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001629 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00001630
showard12f3e322009-05-13 21:27:42 +00001631
showard4c119042008-09-29 19:16:18 +00001632
showarda5288b42009-07-28 20:06:08 +00001633 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00001634 hostname = None
1635 if self.host:
1636 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00001637 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00001638
1639
showard4c119042008-09-29 19:16:18 +00001640class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001641 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00001642 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
1643 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00001644 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00001645
1646 objects = model_logic.ExtendedManager()
1647
showard68c7aa02008-10-09 16:49:11 +00001648
showarda5288b42009-07-28 20:06:08 +00001649 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00001650 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00001651 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00001652
showard4c119042008-09-29 19:16:18 +00001653 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001654 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001655 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00001656
1657
1658class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
1659 """\
1660 job: job to use as a template
1661 owner: owner of the instantiated template
1662 start_date: Run the job at scheduled date
1663 loop_period: Re-run (loop) the job periodically
1664 (in every loop_period seconds)
1665 loop_count: Re-run (loop) count
1666 """
1667
1668 job = dbmodels.ForeignKey(Job)
1669 owner = dbmodels.ForeignKey(User)
1670 start_date = dbmodels.DateTimeField()
1671 loop_period = dbmodels.IntegerField(blank=True)
1672 loop_count = dbmodels.IntegerField(blank=True)
1673
1674 objects = model_logic.ExtendedManager()
1675
1676 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001677 """Metadata for class RecurringRun."""
showardeab66ce2009-12-23 00:03:56 +00001678 db_table = 'afe_recurring_run'
showard29f7cd22009-04-29 21:16:24 +00001679
showarda5288b42009-07-28 20:06:08 +00001680 def __unicode__(self):
1681 return u'RecurringRun(job %s, start %s, period %s, count %s)' % (
showard29f7cd22009-04-29 21:16:24 +00001682 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +00001683
1684
1685class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
1686 """\
1687 Tasks to run on hosts at the next time they are in the Ready state. Use this
1688 for high-priority tasks, such as forced repair or forced reinstall.
1689
1690 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00001691 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00001692 time_requested: date and time the request for this task was made
1693 is_active: task is currently running
1694 is_complete: task has finished running
beeps8bb1f7d2013-08-05 01:30:09 -07001695 is_aborted: task was aborted
showard2fe3f1d2009-07-06 20:19:11 +00001696 time_started: date and time the task started
Dan Shid0725542014-06-23 15:34:27 -07001697 time_finished: date and time the task finished
showard2fe3f1d2009-07-06 20:19:11 +00001698 queue_entry: Host queue entry waiting on this task (or None, if task was not
1699 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00001700 """
Alex Millerdfff2fd2013-05-28 13:05:06 -07001701 Task = enum.Enum('Verify', 'Cleanup', 'Repair', 'Reset', 'Provision',
Dan Shi07e09af2013-04-12 09:31:29 -07001702 string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00001703
1704 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00001705 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00001706 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00001707 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00001708 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
1709 null=False)
1710 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
1711 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
beeps8bb1f7d2013-08-05 01:30:09 -07001712 is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00001713 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00001714 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00001715 success = dbmodels.BooleanField(default=False, blank=False, null=False)
Dan Shid0725542014-06-23 15:34:27 -07001716 time_finished = dbmodels.DateTimeField(null=True, blank=True)
showard6d7b2ff2009-06-10 00:16:47 +00001717
1718 objects = model_logic.ExtendedManager()
1719
1720
showard9bb960b2009-11-19 01:02:11 +00001721 def save(self, **kwargs):
1722 if self.queue_entry:
1723 self.requested_by = User.objects.get(
1724 login=self.queue_entry.job.owner)
1725 super(SpecialTask, self).save(**kwargs)
1726
1727
showarded2afea2009-07-07 20:54:07 +00001728 def execution_path(self):
showardc0ac3a72009-07-08 21:14:45 +00001729 """@see HostQueueEntry.execution_path()"""
showarded2afea2009-07-07 20:54:07 +00001730 return 'hosts/%s/%s-%s' % (self.host.hostname, self.id,
1731 self.task.lower())
1732
1733
showardc0ac3a72009-07-08 21:14:45 +00001734 # property to emulate HostQueueEntry.status
1735 @property
1736 def status(self):
1737 """
1738 Return a host queue entry status appropriate for this task. Although
1739 SpecialTasks are not HostQueueEntries, it is helpful to the user to
1740 present similar statuses.
1741 """
1742 if self.is_complete:
showarde60e44e2009-11-13 20:45:38 +00001743 if self.success:
1744 return HostQueueEntry.Status.COMPLETED
1745 return HostQueueEntry.Status.FAILED
showardc0ac3a72009-07-08 21:14:45 +00001746 if self.is_active:
1747 return HostQueueEntry.Status.RUNNING
1748 return HostQueueEntry.Status.QUEUED
1749
1750
1751 # property to emulate HostQueueEntry.started_on
1752 @property
1753 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001754 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00001755 return self.time_started
1756
1757
showard6d7b2ff2009-06-10 00:16:47 +00001758 @classmethod
showardc5103442010-01-15 00:20:26 +00001759 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001760 """Schedules a special task on a host if not already scheduled.
1761
1762 @param cls: Implicit class object.
1763 @param host: The host to use.
1764 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00001765 """
showardc5103442010-01-15 00:20:26 +00001766 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
1767 is_active=False,
1768 is_complete=False)
1769 if existing_tasks:
1770 return existing_tasks[0]
1771
1772 special_task = SpecialTask(host=host, task=task,
1773 requested_by=User.current_user())
1774 special_task.save()
1775 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00001776
1777
beeps8bb1f7d2013-08-05 01:30:09 -07001778 def abort(self):
1779 """ Abort this special task."""
1780 self.is_aborted = True
1781 self.save()
1782
1783
showarded2afea2009-07-07 20:54:07 +00001784 def activate(self):
showard474d1362009-08-20 23:32:01 +00001785 """
1786 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00001787 """
showard97446882009-07-20 22:37:28 +00001788 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00001789 self.is_active = True
1790 self.time_started = datetime.now()
1791 self.save()
1792
1793
showarde60e44e2009-11-13 20:45:38 +00001794 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001795 """Sets a task as completed.
1796
1797 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00001798 """
showard97446882009-07-20 22:37:28 +00001799 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00001800 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00001801 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00001802 self.success = success
Dan Shid85d6112014-07-14 10:32:55 -07001803 if self.time_started:
1804 self.time_finished = datetime.now()
showard2fe3f1d2009-07-06 20:19:11 +00001805 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00001806
1807
1808 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001809 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00001810 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00001811
showard474d1362009-08-20 23:32:01 +00001812
showarda5288b42009-07-28 20:06:08 +00001813 def __unicode__(self):
1814 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00001815 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00001816 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00001817 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00001818 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00001819 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00001820
1821 return result