blob: a813a4e4ff6cc0f2bb77bec781b426a2cbbe12b0 [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 Juelicha94efe62014-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 Juelicha94efe62014-09-18 16:02:49 -070022from autotest_lib.client.common_lib import control_data, priorities, decorators
Dan Shia0acfbc2014-10-14 15:56:23 -070023from autotest_lib.client.common_lib.cros.graphite import es_utils
mblighe8819cd2008-02-15 16:48:40 +000024
showard0fc38302008-10-23 00:44:07 +000025# job options and user preferences
jamesrendd855242010-03-02 22:23:44 +000026DEFAULT_REBOOT_BEFORE = model_attributes.RebootBefore.IF_DIRTY
Dan Shi07e09af2013-04-12 09:31:29 -070027DEFAULT_REBOOT_AFTER = model_attributes.RebootBefore.NEVER
mblighe8819cd2008-02-15 16:48:40 +000028
showard89f84db2009-03-12 20:39:13 +000029
mblighe8819cd2008-02-15 16:48:40 +000030class AclAccessViolation(Exception):
jadmanski0afbb632008-06-06 21:10:57 +000031 """\
32 Raised when an operation is attempted with proper permissions as
33 dictated by ACLs.
34 """
mblighe8819cd2008-02-15 16:48:40 +000035
36
showard205fd602009-03-21 00:17:35 +000037class AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model):
showard89f84db2009-03-12 20:39:13 +000038 """\
39 An atomic group defines a collection of hosts which must only be scheduled
40 all at once. Any host with a label having an atomic group will only be
41 scheduled for a job at the same time as other hosts sharing that label.
42
43 Required:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080044 name: A name for this atomic group, e.g. 'rack23' or 'funky_net'.
showard89f84db2009-03-12 20:39:13 +000045 max_number_of_machines: The maximum number of machines that will be
46 scheduled at once when scheduling jobs to this atomic group.
47 The job.synch_count is considered the minimum.
48
49 Optional:
50 description: Arbitrary text description of this group's purpose.
51 """
showarda5288b42009-07-28 20:06:08 +000052 name = dbmodels.CharField(max_length=255, unique=True)
showard89f84db2009-03-12 20:39:13 +000053 description = dbmodels.TextField(blank=True)
showarde9450c92009-06-30 01:58:52 +000054 # This magic value is the default to simplify the scheduler logic.
55 # It must be "large". The common use of atomic groups is to want all
56 # machines in the group to be used, limits on which subset used are
57 # often chosen via dependency labels.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080058 # TODO(dennisjeffrey): Revisit this so we don't have to assume that
59 # "infinity" is around 3.3 million.
showarde9450c92009-06-30 01:58:52 +000060 INFINITE_MACHINES = 333333333
61 max_number_of_machines = dbmodels.IntegerField(default=INFINITE_MACHINES)
showard205fd602009-03-21 00:17:35 +000062 invalid = dbmodels.BooleanField(default=False,
showarda5288b42009-07-28 20:06:08 +000063 editable=settings.FULL_ADMIN)
showard89f84db2009-03-12 20:39:13 +000064
showard89f84db2009-03-12 20:39:13 +000065 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +000066 objects = model_logic.ModelWithInvalidManager()
showard205fd602009-03-21 00:17:35 +000067 valid_objects = model_logic.ValidObjectsManager()
68
69
showard29f7cd22009-04-29 21:16:24 +000070 def enqueue_job(self, job, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080071 """Enqueue a job on an associated atomic group of hosts.
72
73 @param job: A job to enqueue.
74 @param is_template: Whether the status should be "Template".
75 """
showard29f7cd22009-04-29 21:16:24 +000076 queue_entry = HostQueueEntry.create(atomic_group=self, job=job,
77 is_template=is_template)
showardc92da832009-04-07 18:14:34 +000078 queue_entry.save()
79
80
showard205fd602009-03-21 00:17:35 +000081 def clean_object(self):
82 self.label_set.clear()
showard89f84db2009-03-12 20:39:13 +000083
84
85 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080086 """Metadata for class AtomicGroup."""
showardeab66ce2009-12-23 00:03:56 +000087 db_table = 'afe_atomic_groups'
showard89f84db2009-03-12 20:39:13 +000088
showard205fd602009-03-21 00:17:35 +000089
showarda5288b42009-07-28 20:06:08 +000090 def __unicode__(self):
91 return unicode(self.name)
showard89f84db2009-03-12 20:39:13 +000092
93
showard7c785282008-05-29 19:45:12 +000094class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000095 """\
96 Required:
showard89f84db2009-03-12 20:39:13 +000097 name: label name
mblighe8819cd2008-02-15 16:48:40 +000098
jadmanski0afbb632008-06-06 21:10:57 +000099 Optional:
showard89f84db2009-03-12 20:39:13 +0000100 kernel_config: URL/path to kernel config for jobs run on this label.
101 platform: If True, this is a platform label (defaults to False).
102 only_if_needed: If True, a Host with this label can only be used if that
103 label is requested by the job/test (either as the meta_host or
104 in the job_dependencies).
105 atomic_group: The atomic group associated with this label.
jadmanski0afbb632008-06-06 21:10:57 +0000106 """
showarda5288b42009-07-28 20:06:08 +0000107 name = dbmodels.CharField(max_length=255, unique=True)
108 kernel_config = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000109 platform = dbmodels.BooleanField(default=False)
110 invalid = dbmodels.BooleanField(default=False,
111 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +0000112 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +0000113
jadmanski0afbb632008-06-06 21:10:57 +0000114 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +0000115 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000116 valid_objects = model_logic.ValidObjectsManager()
showard89f84db2009-03-12 20:39:13 +0000117 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
118
mbligh5244cbb2008-04-24 20:39:52 +0000119
jadmanski0afbb632008-06-06 21:10:57 +0000120 def clean_object(self):
121 self.host_set.clear()
showard01a51672009-05-29 18:42:37 +0000122 self.test_set.clear()
mblighe8819cd2008-02-15 16:48:40 +0000123
124
showard29f7cd22009-04-29 21:16:24 +0000125 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800126 """Enqueue a job on any host of this label.
127
128 @param job: A job to enqueue.
129 @param atomic_group: The associated atomic group.
130 @param is_template: Whether the status should be "Template".
131 """
showard29f7cd22009-04-29 21:16:24 +0000132 queue_entry = HostQueueEntry.create(meta_host=self, job=job,
133 is_template=is_template,
134 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000135 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000136
137
jadmanski0afbb632008-06-06 21:10:57 +0000138 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800139 """Metadata for class Label."""
showardeab66ce2009-12-23 00:03:56 +0000140 db_table = 'afe_labels'
mblighe8819cd2008-02-15 16:48:40 +0000141
showarda5288b42009-07-28 20:06:08 +0000142 def __unicode__(self):
143 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000144
145
Jakob Jülich92c06332014-08-25 19:06:57 +0000146class Shard(dbmodels.Model, model_logic.ModelExtensions):
147
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700148 hostname = dbmodels.CharField(max_length=255, unique=True)
149
150 name_field = 'hostname'
151
Jakob Jülich92c06332014-08-25 19:06:57 +0000152 labels = dbmodels.ManyToManyField(Label, blank=True,
153 db_table='afe_shards_labels')
154
155 class Meta:
156 """Metadata for class ParameterizedJob."""
157 db_table = 'afe_shards'
158
159
jamesren76fcf192010-04-21 20:39:50 +0000160class Drone(dbmodels.Model, model_logic.ModelExtensions):
161 """
162 A scheduler drone
163
164 hostname: the drone's hostname
165 """
166 hostname = dbmodels.CharField(max_length=255, unique=True)
167
168 name_field = 'hostname'
169 objects = model_logic.ExtendedManager()
170
171
172 def save(self, *args, **kwargs):
173 if not User.current_user().is_superuser():
174 raise Exception('Only superusers may edit drones')
175 super(Drone, self).save(*args, **kwargs)
176
177
178 def delete(self):
179 if not User.current_user().is_superuser():
180 raise Exception('Only superusers may delete drones')
181 super(Drone, self).delete()
182
183
184 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800185 """Metadata for class Drone."""
jamesren76fcf192010-04-21 20:39:50 +0000186 db_table = 'afe_drones'
187
188 def __unicode__(self):
189 return unicode(self.hostname)
190
191
192class DroneSet(dbmodels.Model, model_logic.ModelExtensions):
193 """
194 A set of scheduler drones
195
196 These will be used by the scheduler to decide what drones a job is allowed
197 to run on.
198
199 name: the drone set's name
200 drones: the drones that are part of the set
201 """
202 DRONE_SETS_ENABLED = global_config.global_config.get_config_value(
203 'SCHEDULER', 'drone_sets_enabled', type=bool, default=False)
204 DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value(
205 'SCHEDULER', 'default_drone_set_name', default=None)
206
207 name = dbmodels.CharField(max_length=255, unique=True)
208 drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones')
209
210 name_field = 'name'
211 objects = model_logic.ExtendedManager()
212
213
214 def save(self, *args, **kwargs):
215 if not User.current_user().is_superuser():
216 raise Exception('Only superusers may edit drone sets')
217 super(DroneSet, self).save(*args, **kwargs)
218
219
220 def delete(self):
221 if not User.current_user().is_superuser():
222 raise Exception('Only superusers may delete drone sets')
223 super(DroneSet, self).delete()
224
225
226 @classmethod
227 def drone_sets_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800228 """Returns whether drone sets are enabled.
229
230 @param cls: Implicit class object.
231 """
jamesren76fcf192010-04-21 20:39:50 +0000232 return cls.DRONE_SETS_ENABLED
233
234
235 @classmethod
236 def default_drone_set_name(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800237 """Returns the default drone set name.
238
239 @param cls: Implicit class object.
240 """
jamesren76fcf192010-04-21 20:39:50 +0000241 return cls.DEFAULT_DRONE_SET_NAME
242
243
244 @classmethod
245 def get_default(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800246 """Gets the default drone set name, compatible with Job.add_object.
247
248 @param cls: Implicit class object.
249 """
jamesren76fcf192010-04-21 20:39:50 +0000250 return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME)
251
252
253 @classmethod
254 def resolve_name(cls, drone_set_name):
255 """
256 Returns the name of one of these, if not None, in order of preference:
257 1) the drone set given,
258 2) the current user's default drone set, or
259 3) the global default drone set
260
261 or returns None if drone sets are disabled
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800262
263 @param cls: Implicit class object.
264 @param drone_set_name: A drone set name.
jamesren76fcf192010-04-21 20:39:50 +0000265 """
266 if not cls.drone_sets_enabled():
267 return None
268
269 user = User.current_user()
270 user_drone_set_name = user.drone_set and user.drone_set.name
271
272 return drone_set_name or user_drone_set_name or cls.get_default().name
273
274
275 def get_drone_hostnames(self):
276 """
277 Gets the hostnames of all drones in this drone set
278 """
279 return set(self.drones.all().values_list('hostname', flat=True))
280
281
282 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800283 """Metadata for class DroneSet."""
jamesren76fcf192010-04-21 20:39:50 +0000284 db_table = 'afe_drone_sets'
285
286 def __unicode__(self):
287 return unicode(self.name)
288
289
showardfb2a7fa2008-07-17 17:04:12 +0000290class User(dbmodels.Model, model_logic.ModelExtensions):
291 """\
292 Required:
293 login :user login name
294
295 Optional:
296 access_level: 0=User (default), 1=Admin, 100=Root
297 """
298 ACCESS_ROOT = 100
299 ACCESS_ADMIN = 1
300 ACCESS_USER = 0
301
showard64a95952010-01-13 21:27:16 +0000302 AUTOTEST_SYSTEM = 'autotest_system'
303
showarda5288b42009-07-28 20:06:08 +0000304 login = dbmodels.CharField(max_length=255, unique=True)
showardfb2a7fa2008-07-17 17:04:12 +0000305 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
306
showard0fc38302008-10-23 00:44:07 +0000307 # user preferences
jamesrendd855242010-03-02 22:23:44 +0000308 reboot_before = dbmodels.SmallIntegerField(
309 choices=model_attributes.RebootBefore.choices(), blank=True,
310 default=DEFAULT_REBOOT_BEFORE)
311 reboot_after = dbmodels.SmallIntegerField(
312 choices=model_attributes.RebootAfter.choices(), blank=True,
313 default=DEFAULT_REBOOT_AFTER)
jamesren76fcf192010-04-21 20:39:50 +0000314 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
showard97db5ba2008-11-12 18:18:02 +0000315 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000316
showardfb2a7fa2008-07-17 17:04:12 +0000317 name_field = 'login'
318 objects = model_logic.ExtendedManager()
319
320
showarda5288b42009-07-28 20:06:08 +0000321 def save(self, *args, **kwargs):
showardfb2a7fa2008-07-17 17:04:12 +0000322 # is this a new object being saved for the first time?
323 first_time = (self.id is None)
324 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000325 if user and not user.is_superuser() and user.login != self.login:
326 raise AclAccessViolation("You cannot modify user " + self.login)
showarda5288b42009-07-28 20:06:08 +0000327 super(User, self).save(*args, **kwargs)
showardfb2a7fa2008-07-17 17:04:12 +0000328 if first_time:
329 everyone = AclGroup.objects.get(name='Everyone')
330 everyone.users.add(self)
331
332
333 def is_superuser(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800334 """Returns whether the user has superuser access."""
showardfb2a7fa2008-07-17 17:04:12 +0000335 return self.access_level >= self.ACCESS_ROOT
336
337
showard64a95952010-01-13 21:27:16 +0000338 @classmethod
339 def current_user(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800340 """Returns the current user.
341
342 @param cls: Implicit class object.
343 """
showard64a95952010-01-13 21:27:16 +0000344 user = thread_local.get_user()
345 if user is None:
showardcfcdd802010-01-15 00:16:33 +0000346 user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM)
showard64a95952010-01-13 21:27:16 +0000347 user.access_level = cls.ACCESS_ROOT
348 user.save()
349 return user
350
351
showardfb2a7fa2008-07-17 17:04:12 +0000352 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800353 """Metadata for class User."""
showardeab66ce2009-12-23 00:03:56 +0000354 db_table = 'afe_users'
showardfb2a7fa2008-07-17 17:04:12 +0000355
showarda5288b42009-07-28 20:06:08 +0000356 def __unicode__(self):
357 return unicode(self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000358
359
Prashanth B489b91d2014-03-15 12:17:16 -0700360class Host(model_logic.ModelWithInvalid, rdb_model_extensions.AbstractHostModel,
showardf8b19042009-05-12 17:22:49 +0000361 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000362 """\
363 Required:
364 hostname
mblighe8819cd2008-02-15 16:48:40 +0000365
jadmanski0afbb632008-06-06 21:10:57 +0000366 optional:
showard21baa452008-10-21 00:08:39 +0000367 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000368
jadmanski0afbb632008-06-06 21:10:57 +0000369 Internal:
Prashanth B489b91d2014-03-15 12:17:16 -0700370 From AbstractHostModel:
371 synch_id: currently unused
372 status: string describing status of host
373 invalid: true if the host has been deleted
374 protection: indicates what can be done to this host during repair
375 lock_time: DateTime at which the host was locked
376 dirty: true if the host has been used without being rebooted
377 Local:
378 locked_by: user that locked the host, or null if the host is unlocked
jadmanski0afbb632008-06-06 21:10:57 +0000379 """
mblighe8819cd2008-02-15 16:48:40 +0000380
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700381 SERIALIZATION_LINKS_TO_FOLLOW = set(['aclgroup_set',
382 'hostattribute_set',
383 'labels',
384 'shard'])
385
Jakob Juelichf88fa932014-09-03 17:58:04 -0700386
387 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700388 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -0700389 self.shard = Shard.deserialize(data)
390
391
Prashanth B489b91d2014-03-15 12:17:16 -0700392 # Note: Only specify foreign keys here, specify all native host columns in
393 # rdb_model_extensions instead.
394 Protection = host_protections.Protection
showardeab66ce2009-12-23 00:03:56 +0000395 labels = dbmodels.ManyToManyField(Label, blank=True,
396 db_table='afe_hosts_labels')
showardfb2a7fa2008-07-17 17:04:12 +0000397 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
jadmanski0afbb632008-06-06 21:10:57 +0000398 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000399 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000400 valid_objects = model_logic.ValidObjectsManager()
beepscc9fc702013-12-02 12:45:38 -0800401 leased_objects = model_logic.LeasedHostManager()
mbligh5244cbb2008-04-24 20:39:52 +0000402
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700403 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
showard2bab8f42008-11-12 18:15:22 +0000404
405 def __init__(self, *args, **kwargs):
406 super(Host, self).__init__(*args, **kwargs)
407 self._record_attributes(['status'])
408
409
showardb8471e32008-07-03 19:51:08 +0000410 @staticmethod
411 def create_one_time_host(hostname):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800412 """Creates a one-time host.
413
414 @param hostname: The name for the host.
415 """
showardb8471e32008-07-03 19:51:08 +0000416 query = Host.objects.filter(hostname=hostname)
417 if query.count() == 0:
418 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000419 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000420 else:
421 host = query[0]
422 if not host.invalid:
423 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000424 'hostname' : '%s already exists in the autotest DB. '
425 'Select it rather than entering it as a one time '
426 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000427 })
showard1ab512b2008-07-30 23:39:04 +0000428 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000429 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000430 host.save()
showard2924b0a2009-06-18 23:16:15 +0000431 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000432 return host
mbligh5244cbb2008-04-24 20:39:52 +0000433
showard1ff7b2e2009-05-15 23:17:18 +0000434
Jakob Juelich59cfe542014-09-02 16:37:46 -0700435 @classmethod
436 def assign_to_shard(cls, shard):
437 """Assigns hosts to a shard.
438
439 This function will check which labels are associated with the given
440 shard. It will assign those hosts, that have labels that are assigned
441 to the shard and haven't been returned to shard earlier.
442
443 @param shard: The shard object to assign labels/hosts for.
444 @returns the hosts objects that should be sent to the shard.
445 """
446
447 # Disclaimer: concurrent heartbeats should theoretically not occur in
448 # the current setup. As they may be introduced in the near future,
449 # this comment will be left here.
450
451 # Sending stuff twice is acceptable, but forgetting something isn't.
452 # Detecting duplicates on the client is easy, but here it's harder. The
453 # following options were considered:
454 # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
455 # than select returned, as concurrently more hosts might have been
456 # inserted
457 # - UPDATE and then SELECT WHERE shard=shard: select always returns all
458 # hosts for the shard, this is overhead
459 # - SELECT and then UPDATE only selected without requerying afterwards:
460 # returns the old state of the records.
461 host_ids = list(Host.objects.filter(
462 shard=None,
463 labels=shard.labels.all(),
464 leased=False
465 ).values_list('pk', flat=True))
466
467 if host_ids:
468 Host.objects.filter(pk__in=host_ids).update(shard=shard)
469 return list(Host.objects.filter(pk__in=host_ids).all())
470 return []
471
showardafd97de2009-10-01 18:45:09 +0000472 def resurrect_object(self, old_object):
473 super(Host, self).resurrect_object(old_object)
474 # invalid hosts can be in use by the scheduler (as one-time hosts), so
475 # don't change the status
476 self.status = old_object.status
477
478
jadmanski0afbb632008-06-06 21:10:57 +0000479 def clean_object(self):
480 self.aclgroup_set.clear()
481 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000482
483
Dan Shia0acfbc2014-10-14 15:56:23 -0700484 def record_state(self, type_str, state, value, other_metadata=None):
485 """Record metadata in elasticsearch.
486
487 @param type_str: sets the _type field in elasticsearch db.
488 @param state: string representing what state we are recording,
489 e.g. 'locked'
490 @param value: value of the state, e.g. True
491 @param other_metadata: Other metadata to store in metaDB.
492 """
493 metadata = {
494 state: value,
495 'hostname': self.hostname,
496 }
497 if other_metadata:
498 metadata = dict(metadata.items() + other_metadata.items())
499 es_utils.ESMetadata().post(type_str=type_str, metadata=metadata)
500
501
showarda5288b42009-07-28 20:06:08 +0000502 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000503 # extra spaces in the hostname can be a sneaky source of errors
504 self.hostname = self.hostname.strip()
505 # is this a new object being saved for the first time?
506 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000507 if not first_time:
508 AclGroup.check_for_acl_violation_hosts([self])
Dan Shia0acfbc2014-10-14 15:56:23 -0700509 # If locked is changed, send its status and user made the change to
510 # metaDB. Locks are important in host history because if a device is
511 # locked then we don't really care what state it is in.
showardfb2a7fa2008-07-17 17:04:12 +0000512 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000513 self.locked_by = User.current_user()
showardfb2a7fa2008-07-17 17:04:12 +0000514 self.lock_time = datetime.now()
Dan Shia0acfbc2014-10-14 15:56:23 -0700515 self.record_state('lock_history', 'locked', self.locked,
516 {'changed_by': self.locked_by.login})
showard21baa452008-10-21 00:08:39 +0000517 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000518 elif not self.locked and self.locked_by:
Dan Shia0acfbc2014-10-14 15:56:23 -0700519 self.record_state('lock_history', 'locked', self.locked,
520 {'changed_by': self.locked_by.login})
showardfb2a7fa2008-07-17 17:04:12 +0000521 self.locked_by = None
522 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000523 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000524 if first_time:
525 everyone = AclGroup.objects.get(name='Everyone')
526 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000527 self._check_for_updated_attributes()
528
mblighe8819cd2008-02-15 16:48:40 +0000529
showardb8471e32008-07-03 19:51:08 +0000530 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000531 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000532 for queue_entry in self.hostqueueentry_set.all():
533 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000534 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000535 super(Host, self).delete()
536
mblighe8819cd2008-02-15 16:48:40 +0000537
showard2bab8f42008-11-12 18:15:22 +0000538 def on_attribute_changed(self, attribute, old_value):
539 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000540 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000541
542
showard29f7cd22009-04-29 21:16:24 +0000543 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800544 """Enqueue a job on this host.
545
546 @param job: A job to enqueue.
547 @param atomic_group: The associated atomic group.
548 @param is_template: Whther the status should be "Template".
549 """
showard29f7cd22009-04-29 21:16:24 +0000550 queue_entry = HostQueueEntry.create(host=self, job=job,
551 is_template=is_template,
552 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000553 # allow recovery of dead hosts from the frontend
554 if not self.active_queue_entry() and self.is_dead():
555 self.status = Host.Status.READY
556 self.save()
557 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000558
showard08f981b2008-06-24 21:59:03 +0000559 block = IneligibleHostQueue(job=job, host=self)
560 block.save()
561
mblighe8819cd2008-02-15 16:48:40 +0000562
jadmanski0afbb632008-06-06 21:10:57 +0000563 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800564 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000565 # TODO(showard): slighly hacky?
566 platforms = self.labels.filter(platform=True)
567 if len(platforms) == 0:
568 return None
569 return platforms[0]
570 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000571
572
showardcafd16e2009-05-29 18:37:49 +0000573 @classmethod
574 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800575 """Verify the specified hosts have no associated platforms.
576
577 @param cls: Implicit class object.
578 @param hosts: The hosts to verify.
579 @raises model_logic.ValidationError if any hosts already have a
580 platform.
581 """
showardcafd16e2009-05-29 18:37:49 +0000582 Host.objects.populate_relationships(hosts, Label, 'label_list')
583 errors = []
584 for host in hosts:
585 platforms = [label.name for label in host.label_list
586 if label.platform]
587 if platforms:
588 # do a join, just in case this host has multiple platforms,
589 # we'll be able to see it
590 errors.append('Host %s already has a platform: %s' % (
591 host.hostname, ', '.join(platforms)))
592 if errors:
593 raise model_logic.ValidationError({'labels': '; '.join(errors)})
594
595
jadmanski0afbb632008-06-06 21:10:57 +0000596 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800597 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000598 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000599
600
jadmanski0afbb632008-06-06 21:10:57 +0000601 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800602 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000603 active = list(self.hostqueueentry_set.filter(active=True))
604 if not active:
605 return None
606 assert len(active) == 1, ('More than one active entry for '
607 'host ' + self.hostname)
608 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000609
610
showardf8b19042009-05-12 17:22:49 +0000611 def _get_attribute_model_and_args(self, attribute):
612 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000613
614
jadmanski0afbb632008-06-06 21:10:57 +0000615 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800616 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000617 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000618
showarda5288b42009-07-28 20:06:08 +0000619 def __unicode__(self):
620 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000621
622
MK Ryuacf35922014-10-03 14:56:49 -0700623class HostAttribute(dbmodels.Model, model_logic.ModelExtensions):
showard0957a842009-05-11 19:25:08 +0000624 """Arbitrary keyvals associated with hosts."""
625 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000626 attribute = dbmodels.CharField(max_length=90)
627 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000628
629 objects = model_logic.ExtendedManager()
630
631 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800632 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000633 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000634
635
showard7c785282008-05-29 19:45:12 +0000636class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000637 """\
638 Required:
showard909c7a62008-07-15 21:52:38 +0000639 author: author name
640 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000641 name: test name
showard909c7a62008-07-15 21:52:38 +0000642 time: short, medium, long
643 test_class: This describes the class for your the test belongs in.
644 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000645 test_type: Client or Server
646 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000647 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
648 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000649 i.e. if sync_count = 2 it is a sync job that requires two
650 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000651 Optional:
showard909c7a62008-07-15 21:52:38 +0000652 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000653 dependency_labels: many-to-many relationship with labels corresponding to
654 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000655 experimental: If this is set to True production servers will ignore the test
656 run_verify: Whether or not the scheduler should run the verify stage
Dan Shi07e09af2013-04-12 09:31:29 -0700657 run_reset: Whether or not the scheduler should run the reset stage
Aviv Keshet9af96d32013-03-05 12:56:24 -0800658 test_retry: Number of times to retry test if the test did not complete
659 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +0000660 """
showard909c7a62008-07-15 21:52:38 +0000661 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000662
showarda5288b42009-07-28 20:06:08 +0000663 name = dbmodels.CharField(max_length=255, unique=True)
664 author = dbmodels.CharField(max_length=255)
665 test_class = dbmodels.CharField(max_length=255)
666 test_category = dbmodels.CharField(max_length=255)
667 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000668 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000669 experimental = dbmodels.BooleanField(default=True)
Dan Shi07e09af2013-04-12 09:31:29 -0700670 run_verify = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +0000671 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
672 default=TestTime.MEDIUM)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700673 test_type = dbmodels.SmallIntegerField(
674 choices=control_data.CONTROL_TYPE.choices())
showard909c7a62008-07-15 21:52:38 +0000675 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000676 path = dbmodels.CharField(max_length=255, unique=True)
Aviv Keshet9af96d32013-03-05 12:56:24 -0800677 test_retry = dbmodels.IntegerField(blank=True, default=0)
Dan Shi07e09af2013-04-12 09:31:29 -0700678 run_reset = dbmodels.BooleanField(default=True)
mblighe8819cd2008-02-15 16:48:40 +0000679
showardeab66ce2009-12-23 00:03:56 +0000680 dependency_labels = (
681 dbmodels.ManyToManyField(Label, blank=True,
682 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000683 name_field = 'name'
684 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000685
686
jamesren35a70222010-02-16 19:30:46 +0000687 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800688 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +0000689 escaped_description = saxutils.escape(self.description)
690 return '<span style="white-space:pre">%s</span>' % escaped_description
691 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000692 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000693
694
jadmanski0afbb632008-06-06 21:10:57 +0000695 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800696 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +0000697 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000698
showarda5288b42009-07-28 20:06:08 +0000699 def __unicode__(self):
700 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000701
702
jamesren4a41e012010-07-16 22:33:48 +0000703class TestParameter(dbmodels.Model):
704 """
705 A declared parameter of a test
706 """
707 test = dbmodels.ForeignKey(Test)
708 name = dbmodels.CharField(max_length=255)
709
710 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800711 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000712 db_table = 'afe_test_parameters'
713 unique_together = ('test', 'name')
714
715 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -0700716 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +0000717
718
showard2b9a88b2008-06-13 20:55:03 +0000719class Profiler(dbmodels.Model, model_logic.ModelExtensions):
720 """\
721 Required:
722 name: profiler name
723 test_type: Client or Server
724
725 Optional:
726 description: arbirary text description
727 """
showarda5288b42009-07-28 20:06:08 +0000728 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000729 description = dbmodels.TextField(blank=True)
730
731 name_field = 'name'
732 objects = model_logic.ExtendedManager()
733
734
735 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800736 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +0000737 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000738
showarda5288b42009-07-28 20:06:08 +0000739 def __unicode__(self):
740 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000741
742
showard7c785282008-05-29 19:45:12 +0000743class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000744 """\
745 Required:
746 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000747
jadmanski0afbb632008-06-06 21:10:57 +0000748 Optional:
749 description: arbitrary description of group
750 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700751
752 SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
753
showarda5288b42009-07-28 20:06:08 +0000754 name = dbmodels.CharField(max_length=255, unique=True)
755 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000756 users = dbmodels.ManyToManyField(User, blank=False,
757 db_table='afe_acl_groups_users')
758 hosts = dbmodels.ManyToManyField(Host, blank=True,
759 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000760
jadmanski0afbb632008-06-06 21:10:57 +0000761 name_field = 'name'
762 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000763
showard08f981b2008-06-24 21:59:03 +0000764 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000765 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800766 """Verify the current user has access to the specified hosts.
767
768 @param hosts: The hosts to verify against.
769 @raises AclAccessViolation if the current user doesn't have access
770 to a host.
771 """
showard64a95952010-01-13 21:27:16 +0000772 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000773 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000774 return
showard3dd47c22008-07-10 00:41:36 +0000775 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000776 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000777 for host in hosts:
778 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800779 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +0000780 no_access = (isinstance(host, Host)
781 and not host.invalid
782 and int(host.id) not in accessible_host_ids)
783 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000784 raise AclAccessViolation("%s does not have access to %s" %
785 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000786
showard9dbdcda2008-10-14 17:34:36 +0000787
788 @staticmethod
showarddc817512008-11-12 18:16:41 +0000789 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800790 """Look for queue entries that aren't abortable by the current user.
791
792 An entry is not abortable if:
793 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +0000794 * the machine isn't ACL-accessible, or
795 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800796
797 @param queue_entries: The queue entries to check.
798 @raises AclAccessViolation if a queue entry is not abortable by the
799 current user.
showarddc817512008-11-12 18:16:41 +0000800 """
showard64a95952010-01-13 21:27:16 +0000801 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000802 if user.is_superuser():
803 return
showarddc817512008-11-12 18:16:41 +0000804 not_owned = queue_entries.exclude(job__owner=user.login)
805 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000806 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
807 # 1.0.
808 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000809 accessible_ids = set(
810 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000811 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000812 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000813 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000814 cannot_abort = [entry for entry in not_owned.select_related()
815 if entry.id not in accessible_ids
816 or entry.id in public_ids]
817 if len(cannot_abort) == 0:
818 return
819 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000820 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000821 for entry in cannot_abort)
822 raise AclAccessViolation('You cannot abort the following job entries: '
823 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000824
825
showard3dd47c22008-07-10 00:41:36 +0000826 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800827 """Verifies the current user has acces to this ACL group.
828
829 @raises AclAccessViolation if the current user doesn't have access to
830 this ACL group.
831 """
showard64a95952010-01-13 21:27:16 +0000832 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000833 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000834 return
835 if self.name == 'Everyone':
836 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000837 if not user in self.users.all():
838 raise AclAccessViolation("You do not have access to %s"
839 % self.name)
840
841 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000842 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800843 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +0000844 everyone = AclGroup.objects.get(name='Everyone')
845
showard3dd47c22008-07-10 00:41:36 +0000846 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000847 # TODO(showard): this is a bit of a hack, since the fact that this query
848 # works is kind of a coincidence of Django internals. This trick
849 # doesn't work in general (on all foreign key relationships). I'll
850 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000851 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000852 everyone.hosts.add(*orphaned_hosts.distinct())
853
854 # find hosts in both Everyone and another ACL group, and remove them
855 # from Everyone
showarda5288b42009-07-28 20:06:08 +0000856 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
857 acled_hosts = set()
858 for host in hosts_in_everyone:
859 # Has an ACL group other than Everyone
860 if host.aclgroup_set.count() > 1:
861 acled_hosts.add(host)
862 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +0000863
864
865 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000866 if (self.name == 'Everyone'):
867 raise AclAccessViolation("You cannot delete 'Everyone'!")
868 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000869 super(AclGroup, self).delete()
870 self.on_host_membership_change()
871
872
showard04f2cd82008-07-25 20:53:31 +0000873 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800874 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +0000875 if not self.users.count():
showard64a95952010-01-13 21:27:16 +0000876 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +0000877
878
showard8cbaf1e2009-09-08 16:27:04 +0000879 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800880 """Called after a save.
881
882 @param change: Whether there was a change.
883 """
showard8cbaf1e2009-09-08 16:27:04 +0000884 if not change:
showard64a95952010-01-13 21:27:16 +0000885 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +0000886 self.add_current_user_if_empty()
887 self.on_host_membership_change()
888
889
890 def save(self, *args, **kwargs):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700891 change = bool(self.id)
892 if change:
showard8cbaf1e2009-09-08 16:27:04 +0000893 # Check the original object for an ACL violation
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700894 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
showard8cbaf1e2009-09-08 16:27:04 +0000895 super(AclGroup, self).save(*args, **kwargs)
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700896 self.perform_after_save(change)
showard8cbaf1e2009-09-08 16:27:04 +0000897
showardeb3be4d2008-04-21 20:59:26 +0000898
jadmanski0afbb632008-06-06 21:10:57 +0000899 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800900 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +0000901 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000902
showarda5288b42009-07-28 20:06:08 +0000903 def __unicode__(self):
904 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000905
mblighe8819cd2008-02-15 16:48:40 +0000906
jamesren4a41e012010-07-16 22:33:48 +0000907class Kernel(dbmodels.Model):
908 """
909 A kernel configuration for a parameterized job
910 """
911 version = dbmodels.CharField(max_length=255)
912 cmdline = dbmodels.CharField(max_length=255, blank=True)
913
914 @classmethod
915 def create_kernels(cls, kernel_list):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800916 """Creates all kernels in the kernel list.
jamesren4a41e012010-07-16 22:33:48 +0000917
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800918 @param cls: Implicit class object.
919 @param kernel_list: A list of dictionaries that describe the kernels,
920 in the same format as the 'kernel' argument to
921 rpc_interface.generate_control_file.
922 @return A list of the created kernels.
jamesren4a41e012010-07-16 22:33:48 +0000923 """
924 if not kernel_list:
925 return None
926 return [cls._create(kernel) for kernel in kernel_list]
927
928
929 @classmethod
930 def _create(cls, kernel_dict):
931 version = kernel_dict.pop('version')
932 cmdline = kernel_dict.pop('cmdline', '')
933
934 if kernel_dict:
935 raise Exception('Extraneous kernel arguments remain: %r'
936 % kernel_dict)
937
938 kernel, _ = cls.objects.get_or_create(version=version,
939 cmdline=cmdline)
940 return kernel
941
942
943 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800944 """Metadata for class Kernel."""
jamesren4a41e012010-07-16 22:33:48 +0000945 db_table = 'afe_kernels'
946 unique_together = ('version', 'cmdline')
947
948 def __unicode__(self):
949 return u'%s %s' % (self.version, self.cmdline)
950
951
952class ParameterizedJob(dbmodels.Model):
953 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800954 Auxiliary configuration for a parameterized job.
jamesren4a41e012010-07-16 22:33:48 +0000955 """
956 test = dbmodels.ForeignKey(Test)
957 label = dbmodels.ForeignKey(Label, null=True)
958 use_container = dbmodels.BooleanField(default=False)
959 profile_only = dbmodels.BooleanField(default=False)
960 upload_kernel_config = dbmodels.BooleanField(default=False)
961
962 kernels = dbmodels.ManyToManyField(
963 Kernel, db_table='afe_parameterized_job_kernels')
964 profilers = dbmodels.ManyToManyField(
965 Profiler, through='ParameterizedJobProfiler')
966
967
968 @classmethod
969 def smart_get(cls, id_or_name, *args, **kwargs):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800970 """For compatibility with Job.add_object.
971
972 @param cls: Implicit class object.
973 @param id_or_name: The ID or name to get.
974 @param args: Non-keyword arguments.
975 @param kwargs: Keyword arguments.
976 """
jamesren4a41e012010-07-16 22:33:48 +0000977 return cls.objects.get(pk=id_or_name)
978
979
980 def job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800981 """Returns the job if it exists, or else None."""
jamesren4a41e012010-07-16 22:33:48 +0000982 jobs = self.job_set.all()
983 assert jobs.count() <= 1
984 return jobs and jobs[0] or None
985
986
987 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800988 """Metadata for class ParameterizedJob."""
jamesren4a41e012010-07-16 22:33:48 +0000989 db_table = 'afe_parameterized_jobs'
990
991 def __unicode__(self):
992 return u'%s (parameterized) - %s' % (self.test.name, self.job())
993
994
995class ParameterizedJobProfiler(dbmodels.Model):
996 """
997 A profiler to run on a parameterized job
998 """
999 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1000 profiler = dbmodels.ForeignKey(Profiler)
1001
1002 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001003 """Metedata for class ParameterizedJobProfiler."""
jamesren4a41e012010-07-16 22:33:48 +00001004 db_table = 'afe_parameterized_jobs_profilers'
1005 unique_together = ('parameterized_job', 'profiler')
1006
1007
1008class ParameterizedJobProfilerParameter(dbmodels.Model):
1009 """
1010 A parameter for a profiler in a parameterized job
1011 """
1012 parameterized_job_profiler = dbmodels.ForeignKey(ParameterizedJobProfiler)
1013 parameter_name = dbmodels.CharField(max_length=255)
1014 parameter_value = dbmodels.TextField()
1015 parameter_type = dbmodels.CharField(
1016 max_length=8, choices=model_attributes.ParameterTypes.choices())
1017
1018 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001019 """Metadata for class ParameterizedJobProfilerParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001020 db_table = 'afe_parameterized_job_profiler_parameters'
1021 unique_together = ('parameterized_job_profiler', 'parameter_name')
1022
1023 def __unicode__(self):
1024 return u'%s - %s' % (self.parameterized_job_profiler.profiler.name,
1025 self.parameter_name)
1026
1027
1028class ParameterizedJobParameter(dbmodels.Model):
1029 """
1030 Parameters for a parameterized job
1031 """
1032 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1033 test_parameter = dbmodels.ForeignKey(TestParameter)
1034 parameter_value = dbmodels.TextField()
1035 parameter_type = dbmodels.CharField(
1036 max_length=8, choices=model_attributes.ParameterTypes.choices())
1037
1038 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001039 """Metadata for class ParameterizedJobParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001040 db_table = 'afe_parameterized_job_parameters'
1041 unique_together = ('parameterized_job', 'test_parameter')
1042
1043 def __unicode__(self):
1044 return u'%s - %s' % (self.parameterized_job.job().name,
1045 self.test_parameter.name)
1046
1047
showard7c785282008-05-29 19:45:12 +00001048class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +00001049 'Custom manager to provide efficient status counts querying.'
1050 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001051 """Returns a dict mapping the given job IDs to their status count dicts.
1052
1053 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +00001054 """
1055 if not job_ids:
1056 return {}
1057 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1058 cursor = connection.cursor()
1059 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +00001060 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +00001061 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +00001062 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +00001063 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +00001064 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +00001065 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +00001066 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +00001067 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +00001068 full_status = HostQueueEntry.compute_full_status(status, aborted,
1069 complete)
showardb6d16622009-05-26 19:35:29 +00001070 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +00001071 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +00001072 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +00001073
1074
showard7c785282008-05-29 19:45:12 +00001075class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001076 """\
1077 owner: username of job owner
1078 name: job name (does not have to be unique)
Alex Miller7d658cf2013-09-04 16:00:35 -07001079 priority: Integer priority value. Higher is more important.
jadmanski0afbb632008-06-06 21:10:57 +00001080 control_file: contents of control file
1081 control_type: Client or Server
1082 created_on: date of job creation
1083 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +00001084 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +00001085 run_verify: Whether or not to run the verify phase
Dan Shi07e09af2013-04-12 09:31:29 -07001086 run_reset: Whether or not to run the reset phase
Simran Basi94dc0032013-11-12 14:09:46 -08001087 timeout: DEPRECATED - hours from queuing time until job times out
1088 timeout_mins: minutes from job queuing time until the job times out
Simran Basi34217022012-11-06 13:43:15 -08001089 max_runtime_hrs: DEPRECATED - hours from job starting time until job
1090 times out
1091 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +00001092 email_list: list of people to email on completion delimited by any of:
1093 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +00001094 dependency_labels: many-to-many relationship with labels corresponding to
1095 job dependencies
showard21baa452008-10-21 00:08:39 +00001096 reboot_before: Never, If dirty, or Always
1097 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001098 parse_failed_repair: if True, a failed repair launched by this job will have
1099 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001100 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001101 parent_job: Parent job (optional)
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001102 test_retry: Number of times to retry test if the test did not complete
1103 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +00001104 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001105
1106 # TODO: Investigate, if jobkeyval_set is really needed.
1107 # dynamic_suite will write them into an attached file for the drone, but
1108 # it doesn't seem like they are actually used. If they aren't used, remove
1109 # jobkeyval_set here.
1110 SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1111 'hostqueueentry_set',
1112 'jobkeyval_set',
1113 'shard'])
1114
1115
Jakob Juelichf88fa932014-09-03 17:58:04 -07001116 def _deserialize_relation(self, link, data):
1117 if link in ['hostqueueentry_set', 'jobkeyval_set']:
1118 for obj in data:
1119 obj['job_id'] = self.id
1120
1121 super(Job, self)._deserialize_relation(link, data)
1122
1123
1124 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001125 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -07001126 self.shard = Shard.deserialize(data)
1127
1128
Jakob Juelicha94efe62014-09-18 16:02:49 -07001129 def sanity_check_update_from_shard(self, shard, updated_serialized):
Jakob Juelich8421d592014-09-17 15:27:06 -07001130 if self.shard_id != shard.id:
Jakob Juelicha94efe62014-09-18 16:02:49 -07001131 raise error.UnallowedRecordsSentToMaster(
1132 'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1133 'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1134 shard.id))
1135
1136
Simran Basi94dc0032013-11-12 14:09:46 -08001137 # TIMEOUT is deprecated.
showardb1e51872008-10-07 11:08:18 +00001138 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
Simran Basi94dc0032013-11-12 14:09:46 -08001139 'AUTOTEST_WEB', 'job_timeout_default', default=24)
1140 DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1141 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
Simran Basi34217022012-11-06 13:43:15 -08001142 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1143 # completed.
showard12f3e322009-05-13 21:27:42 +00001144 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1145 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001146 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1147 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001148 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1149 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1150 default=False)
showardb1e51872008-10-07 11:08:18 +00001151
showarda5288b42009-07-28 20:06:08 +00001152 owner = dbmodels.CharField(max_length=255)
1153 name = dbmodels.CharField(max_length=255)
Alex Miller7d658cf2013-09-04 16:00:35 -07001154 priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
jamesren4a41e012010-07-16 22:33:48 +00001155 control_file = dbmodels.TextField(null=True, blank=True)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001156 control_type = dbmodels.SmallIntegerField(
1157 choices=control_data.CONTROL_TYPE.choices(),
1158 blank=True, # to allow 0
1159 default=control_data.CONTROL_TYPE.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001160 created_on = dbmodels.DateTimeField()
Simran Basi8d89b642014-05-02 17:33:20 -07001161 synch_count = dbmodels.IntegerField(blank=True, default=0)
showardb1e51872008-10-07 11:08:18 +00001162 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
Dan Shi07e09af2013-04-12 09:31:29 -07001163 run_verify = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001164 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001165 dependency_labels = (
1166 dbmodels.ManyToManyField(Label, blank=True,
1167 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001168 reboot_before = dbmodels.SmallIntegerField(
1169 choices=model_attributes.RebootBefore.choices(), blank=True,
1170 default=DEFAULT_REBOOT_BEFORE)
1171 reboot_after = dbmodels.SmallIntegerField(
1172 choices=model_attributes.RebootAfter.choices(), blank=True,
1173 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001174 parse_failed_repair = dbmodels.BooleanField(
1175 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001176 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1177 # completed.
showard12f3e322009-05-13 21:27:42 +00001178 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001179 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001180 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001181
jamesren4a41e012010-07-16 22:33:48 +00001182 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1183 blank=True)
1184
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001185 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001186
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001187 test_retry = dbmodels.IntegerField(blank=True, default=0)
1188
Dan Shi07e09af2013-04-12 09:31:29 -07001189 run_reset = dbmodels.BooleanField(default=True)
1190
Simran Basi94dc0032013-11-12 14:09:46 -08001191 timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1192
Jakob Juelich8421d592014-09-17 15:27:06 -07001193 # If this is None on the master, a slave should be found.
1194 # If this is None on a slave, it should be synced back to the master
Jakob Jülich92c06332014-08-25 19:06:57 +00001195 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1196
jadmanski0afbb632008-06-06 21:10:57 +00001197 # custom manager
1198 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001199
1200
Alex Millerec212252014-02-28 16:48:34 -08001201 @decorators.cached_property
1202 def labels(self):
1203 """All the labels of this job"""
1204 # We need to convert dependency_labels to a list, because all() gives us
1205 # back an iterator, and storing/caching an iterator means we'd only be
1206 # able to read from it once.
1207 return list(self.dependency_labels.all())
1208
1209
jadmanski0afbb632008-06-06 21:10:57 +00001210 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001211 """Returns whether this job is of type server."""
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001212 return self.control_type == control_data.CONTROL_TYPE.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001213
1214
jadmanski0afbb632008-06-06 21:10:57 +00001215 @classmethod
jamesren4a41e012010-07-16 22:33:48 +00001216 def parameterized_jobs_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001217 """Returns whether parameterized jobs are enabled.
1218
1219 @param cls: Implicit class object.
1220 """
jamesren4a41e012010-07-16 22:33:48 +00001221 return global_config.global_config.get_config_value(
1222 'AUTOTEST_WEB', 'parameterized_jobs', type=bool)
1223
1224
1225 @classmethod
1226 def check_parameterized_job(cls, control_file, parameterized_job):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001227 """Checks that the job is valid given the global config settings.
jamesren4a41e012010-07-16 22:33:48 +00001228
1229 First, either control_file must be set, or parameterized_job must be
1230 set, but not both. Second, parameterized_job must be set if and only if
1231 the parameterized_jobs option in the global config is set to True.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001232
1233 @param cls: Implict class object.
1234 @param control_file: A control file.
1235 @param parameterized_job: A parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001236 """
1237 if not (bool(control_file) ^ bool(parameterized_job)):
1238 raise Exception('Job must have either control file or '
1239 'parameterization, but not both')
1240
1241 parameterized_jobs_enabled = cls.parameterized_jobs_enabled()
1242 if control_file and parameterized_jobs_enabled:
1243 raise Exception('Control file specified, but parameterized jobs '
1244 'are enabled')
1245 if parameterized_job and not parameterized_jobs_enabled:
1246 raise Exception('Parameterized job specified, but parameterized '
1247 'jobs are not enabled')
1248
1249
1250 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001251 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001252 """Creates a job.
1253
1254 The job is created by taking some information (the listed args) and
1255 filling in the rest of the necessary information.
1256
1257 @param cls: Implicit class object.
1258 @param owner: The owner for the job.
1259 @param options: An options object.
1260 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001261 """
showard3dd47c22008-07-10 00:41:36 +00001262 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001263
jamesren4a41e012010-07-16 22:33:48 +00001264 control_file = options.get('control_file')
1265 parameterized_job = options.get('parameterized_job')
jamesren4a41e012010-07-16 22:33:48 +00001266
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001267 # The current implementation of parameterized jobs requires that only
1268 # control files or parameterized jobs are used. Using the image
1269 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1270 # parameterized jobs and control files jobs, it does muck enough with
1271 # normal jobs by adding a parameterized id to them that this check will
1272 # fail. So for now we just skip this check.
1273 # cls.check_parameterized_job(control_file=control_file,
1274 # parameterized_job=parameterized_job)
showard4d233752010-01-20 19:06:40 +00001275 user = User.current_user()
1276 if options.get('reboot_before') is None:
1277 options['reboot_before'] = user.get_reboot_before_display()
1278 if options.get('reboot_after') is None:
1279 options['reboot_after'] = user.get_reboot_after_display()
1280
jamesren76fcf192010-04-21 20:39:50 +00001281 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1282
Simran Basi94dc0032013-11-12 14:09:46 -08001283 if options.get('timeout_mins') is None and options.get('timeout'):
1284 options['timeout_mins'] = options['timeout'] * 60
1285
jadmanski0afbb632008-06-06 21:10:57 +00001286 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001287 owner=owner,
1288 name=options['name'],
1289 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001290 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001291 control_type=options['control_type'],
1292 synch_count=options.get('synch_count'),
Simran Basi94dc0032013-11-12 14:09:46 -08001293 # timeout needs to be deleted in the future.
showarda1e74b32009-05-12 17:32:04 +00001294 timeout=options.get('timeout'),
Simran Basi94dc0032013-11-12 14:09:46 -08001295 timeout_mins=options.get('timeout_mins'),
Simran Basi34217022012-11-06 13:43:15 -08001296 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001297 run_verify=options.get('run_verify'),
1298 email_list=options.get('email_list'),
1299 reboot_before=options.get('reboot_before'),
1300 reboot_after=options.get('reboot_after'),
1301 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001302 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001303 drone_set=drone_set,
Aviv Keshet18308922013-02-19 17:49:49 -08001304 parameterized_job=parameterized_job,
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001305 parent_job=options.get('parent_job_id'),
Dan Shi07e09af2013-04-12 09:31:29 -07001306 test_retry=options.get('test_retry'),
1307 run_reset=options.get('run_reset'))
mblighe8819cd2008-02-15 16:48:40 +00001308
showarda1e74b32009-05-12 17:32:04 +00001309 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001310
jamesrend8b6e172010-04-16 23:45:00 +00001311 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001312 for key, value in options['keyvals'].iteritems():
1313 JobKeyval.objects.create(job=job, key=key, value=value)
1314
jadmanski0afbb632008-06-06 21:10:57 +00001315 return job
mblighe8819cd2008-02-15 16:48:40 +00001316
1317
Jakob Juelich59cfe542014-09-02 16:37:46 -07001318 @classmethod
1319 def assign_to_shard(cls, shard):
1320 """Assigns unassigned jobs to a shard.
1321
1322 All jobs that have the platform label that was assigned to the given
1323 shard are assigned to the shard and returned.
1324 @param shard: The shard to assign jobs to.
1325 @returns The job objects that should be sent to the shard.
1326 """
1327 # Disclaimer: Concurrent heartbeats should not occur in today's setup.
1328 # If this changes or they are triggered manually, this applies:
1329 # Jobs may be returned more than once by concurrent calls of this
1330 # function, as there is a race condition between SELECT and UPDATE.
Jakob Juelich852ec0d2014-09-15 18:28:23 -07001331 unassigned_or_aborted_query = (
1332 dbmodels.Q(shard=None) | dbmodels.Q(hostqueueentry__aborted=True))
Jakob Juelich59cfe542014-09-02 16:37:46 -07001333 job_ids = list(Job.objects.filter(
Jakob Juelich852ec0d2014-09-15 18:28:23 -07001334 unassigned_or_aborted_query,
Jakob Juelich59cfe542014-09-02 16:37:46 -07001335 dependency_labels=shard.labels.all()
1336 ).exclude(
1337 hostqueueentry__complete=True
1338 ).exclude(
1339 hostqueueentry__active=True
Jakob Juelich852ec0d2014-09-15 18:28:23 -07001340 ).distinct().values_list('pk', flat=True))
Jakob Juelich59cfe542014-09-02 16:37:46 -07001341 if job_ids:
1342 Job.objects.filter(pk__in=job_ids).update(shard=shard)
1343 return list(Job.objects.filter(pk__in=job_ids).all())
1344 return []
1345
1346
jamesren4a41e012010-07-16 22:33:48 +00001347 def save(self, *args, **kwargs):
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001348 # The current implementation of parameterized jobs requires that only
1349 # control files or parameterized jobs are used. Using the image
1350 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1351 # parameterized jobs and control files jobs, it does muck enough with
1352 # normal jobs by adding a parameterized id to them that this check will
1353 # fail. So for now we just skip this check.
1354 # cls.check_parameterized_job(control_file=self.control_file,
1355 # parameterized_job=self.parameterized_job)
jamesren4a41e012010-07-16 22:33:48 +00001356 super(Job, self).save(*args, **kwargs)
1357
1358
showard29f7cd22009-04-29 21:16:24 +00001359 def queue(self, hosts, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001360 """Enqueue a job on the given hosts.
1361
1362 @param hosts: The hosts to use.
1363 @param atomic_group: The associated atomic group.
1364 @param is_template: Whether the status should be "Template".
1365 """
showarda9545c02009-12-18 22:44:26 +00001366 if not hosts:
1367 if atomic_group:
1368 # No hosts or labels are required to queue an atomic group
1369 # Job. However, if they are given, we respect them below.
1370 atomic_group.enqueue_job(self, is_template=is_template)
1371 else:
1372 # hostless job
1373 entry = HostQueueEntry.create(job=self, is_template=is_template)
1374 entry.save()
1375 return
1376
jadmanski0afbb632008-06-06 21:10:57 +00001377 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +00001378 host.enqueue_job(self, atomic_group=atomic_group,
1379 is_template=is_template)
1380
1381
1382 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001383 """Creates a recurring job.
1384
1385 @param start_date: The starting date of the job.
1386 @param loop_period: How often to re-run the job, in seconds.
1387 @param loop_count: The re-run count.
1388 @param owner: The owner of the job.
1389 """
showard29f7cd22009-04-29 21:16:24 +00001390 rec = RecurringRun(job=self, start_date=start_date,
1391 loop_period=loop_period,
1392 loop_count=loop_count,
1393 owner=User.objects.get(login=owner))
1394 rec.save()
1395 return rec.id
mblighe8819cd2008-02-15 16:48:40 +00001396
1397
jadmanski0afbb632008-06-06 21:10:57 +00001398 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001399 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001400 try:
1401 return User.objects.get(login=self.owner)
1402 except self.DoesNotExist:
1403 return None
mblighe8819cd2008-02-15 16:48:40 +00001404
1405
showard64a95952010-01-13 21:27:16 +00001406 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001407 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001408 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001409 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001410
1411
showardd1195652009-12-08 22:21:02 +00001412 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001413 """Returns a string tag for this job."""
showardd1195652009-12-08 22:21:02 +00001414 return '%s-%s' % (self.id, self.owner)
1415
1416
showardc1a98d12010-01-15 00:22:22 +00001417 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001418 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001419 return dict((keyval.key, keyval.value)
1420 for keyval in self.jobkeyval_set.all())
1421
1422
jadmanski0afbb632008-06-06 21:10:57 +00001423 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001424 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001425 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001426
showarda5288b42009-07-28 20:06:08 +00001427 def __unicode__(self):
1428 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001429
1430
showardc1a98d12010-01-15 00:22:22 +00001431class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1432 """Keyvals associated with jobs"""
1433 job = dbmodels.ForeignKey(Job)
1434 key = dbmodels.CharField(max_length=90)
1435 value = dbmodels.CharField(max_length=300)
1436
1437 objects = model_logic.ExtendedManager()
1438
1439 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001440 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001441 db_table = 'afe_job_keyvals'
1442
1443
showard7c785282008-05-29 19:45:12 +00001444class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001445 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001446 job = dbmodels.ForeignKey(Job)
1447 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001448
jadmanski0afbb632008-06-06 21:10:57 +00001449 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001450
jadmanski0afbb632008-06-06 21:10:57 +00001451 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001452 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001453 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001454
mblighe8819cd2008-02-15 16:48:40 +00001455
showard7c785282008-05-29 19:45:12 +00001456class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001457 """Represents a host queue entry."""
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001458
1459 SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
Jakob Juelichf865d332014-09-29 10:47:49 -07001460 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001461
Jakob Juelichf88fa932014-09-03 17:58:04 -07001462
1463 def custom_deserialize_relation(self, link, data):
1464 assert link == 'meta_host'
1465 self.meta_host = Label.deserialize(data)
1466
1467
Jakob Juelicha94efe62014-09-18 16:02:49 -07001468 def sanity_check_update_from_shard(self, shard, updated_serialized,
1469 job_ids_sent):
1470 if self.job_id not in job_ids_sent:
1471 raise error.UnallowedRecordsSentToMaster(
1472 'Sent HostQueueEntry without corresponding '
1473 'job entry: %s' % updated_serialized)
1474
1475
showardeaa408e2009-09-11 18:45:31 +00001476 Status = host_queue_entry_states.Status
1477 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001478 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001479
jadmanski0afbb632008-06-06 21:10:57 +00001480 job = dbmodels.ForeignKey(Job)
1481 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001482 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001483 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1484 db_column='meta_host')
1485 active = dbmodels.BooleanField(default=False)
1486 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001487 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001488 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1489 default='')
showard89f84db2009-03-12 20:39:13 +00001490 # If atomic_group is set, this is a virtual HostQueueEntry that will
1491 # be expanded into many actual hosts within the group at schedule time.
1492 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001493 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001494 started_on = dbmodels.DateTimeField(null=True, blank=True)
Fang Deng51599032014-06-23 17:24:27 -07001495 finished_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001496
jadmanski0afbb632008-06-06 21:10:57 +00001497 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001498
mblighe8819cd2008-02-15 16:48:40 +00001499
showard2bab8f42008-11-12 18:15:22 +00001500 def __init__(self, *args, **kwargs):
1501 super(HostQueueEntry, self).__init__(*args, **kwargs)
1502 self._record_attributes(['status'])
1503
1504
showard29f7cd22009-04-29 21:16:24 +00001505 @classmethod
1506 def create(cls, job, host=None, meta_host=None, atomic_group=None,
1507 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001508 """Creates a new host queue entry.
1509
1510 @param cls: Implicit class object.
1511 @param job: The associated job.
1512 @param host: The associated host.
1513 @param meta_host: The associated meta host.
1514 @param atomic_group: The associated atomic group.
1515 @param is_template: Whether the status should be "Template".
1516 """
showard29f7cd22009-04-29 21:16:24 +00001517 if is_template:
1518 status = cls.Status.TEMPLATE
1519 else:
1520 status = cls.Status.QUEUED
1521
1522 return cls(job=job, host=host, meta_host=meta_host,
1523 atomic_group=atomic_group, status=status)
1524
1525
showarda5288b42009-07-28 20:06:08 +00001526 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001527 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001528 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001529 self._check_for_updated_attributes()
1530
1531
showardc0ac3a72009-07-08 21:14:45 +00001532 def execution_path(self):
1533 """
1534 Path to this entry's results (relative to the base results directory).
1535 """
showardd1195652009-12-08 22:21:02 +00001536 return os.path.join(self.job.tag(), self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001537
1538
showard3f15eed2008-11-14 22:40:48 +00001539 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001540 """Returns the first non-None name found in priority order.
1541
1542 The priority order checked is: (1) host name; (2) meta host name; and
1543 (3) atomic group name.
1544 """
showard3f15eed2008-11-14 22:40:48 +00001545 if self.host:
1546 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001547 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +00001548 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +00001549 else:
1550 assert self.atomic_group, "no host, meta_host or atomic group!"
1551 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +00001552
1553
showard2bab8f42008-11-12 18:15:22 +00001554 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001555 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001556 self.active, self.complete = True, False
1557 elif self.status in self.COMPLETE_STATUSES:
1558 self.active, self.complete = False, True
1559 else:
1560 self.active, self.complete = False, False
1561
1562
1563 def on_attribute_changed(self, attribute, old_value):
1564 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001565 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1566 self.status)
showard2bab8f42008-11-12 18:15:22 +00001567
1568
jadmanski0afbb632008-06-06 21:10:57 +00001569 def is_meta_host_entry(self):
1570 'True if this is a entry has a meta_host instead of a host.'
1571 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001572
showarda3ab0d52008-11-03 19:03:47 +00001573
Simran Basic1b26762013-06-26 14:23:21 -07001574 # This code is shared between rpc_interface and models.HostQueueEntry.
1575 # Sadly due to circular imports between the 2 (crbug.com/230100) making it
1576 # a class method was the best way to refactor it. Attempting to put it in
1577 # rpc_utils or a new utils module failed as that would require us to import
1578 # models.py but to call it from here we would have to import the utils.py
1579 # thus creating a cycle.
1580 @classmethod
1581 def abort_host_queue_entries(cls, host_queue_entries):
1582 """Aborts a collection of host_queue_entries.
1583
1584 Abort these host queue entry and all host queue entries of jobs created
1585 by them.
1586
1587 @param host_queue_entries: List of host queue entries we want to abort.
1588 """
1589 # This isn't completely immune to race conditions since it's not atomic,
1590 # but it should be safe given the scheduler's behavior.
1591
1592 # TODO(milleral): crbug.com/230100
1593 # The |abort_host_queue_entries| rpc does nearly exactly this,
1594 # however, trying to re-use the code generates some horrible
1595 # circular import error. I'd be nice to refactor things around
1596 # sometime so the code could be reused.
1597
1598 # Fixpoint algorithm to find the whole tree of HQEs to abort to
1599 # minimize the total number of database queries:
1600 children = set()
1601 new_children = set(host_queue_entries)
1602 while new_children:
1603 children.update(new_children)
1604 new_child_ids = [hqe.job_id for hqe in new_children]
1605 new_children = HostQueueEntry.objects.filter(
1606 job__parent_job__in=new_child_ids,
1607 complete=False, aborted=False).all()
1608 # To handle circular parental relationships
1609 new_children = set(new_children) - children
1610
1611 # Associate a user with the host queue entries that we're about
1612 # to abort so that we can look up who to blame for the aborts.
1613 now = datetime.now()
1614 user = User.current_user()
1615 aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
1616 aborted_by=user, aborted_on=now) for hqe in children]
1617 AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
1618 # Bulk update all of the HQEs to set the abort bit.
1619 child_ids = [hqe.id for hqe in children]
1620 HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
1621
1622
Scott Zawalski23041432013-04-17 07:39:09 -07001623 def abort(self):
Alex Millerdea67042013-04-22 17:23:34 -07001624 """ Aborts this host queue entry.
Simran Basic1b26762013-06-26 14:23:21 -07001625
Alex Millerdea67042013-04-22 17:23:34 -07001626 Abort this host queue entry and all host queue entries of jobs created by
1627 this one.
1628
1629 """
showardd3dc1992009-04-22 21:01:40 +00001630 if not self.complete and not self.aborted:
Simran Basi97582a22013-06-27 12:03:21 -07001631 HostQueueEntry.abort_host_queue_entries([self])
mblighe8819cd2008-02-15 16:48:40 +00001632
showardd3dc1992009-04-22 21:01:40 +00001633
1634 @classmethod
1635 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001636 """Returns a modified status msg if the host queue entry was aborted.
1637
1638 @param cls: Implicit class object.
1639 @param status: The original status message.
1640 @param aborted: Whether the host queue entry was aborted.
1641 @param complete: Whether the host queue entry was completed.
1642 """
showardd3dc1992009-04-22 21:01:40 +00001643 if aborted and not complete:
1644 return 'Aborted (%s)' % status
1645 return status
1646
1647
1648 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001649 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00001650 return self.compute_full_status(self.status, self.aborted,
1651 self.complete)
1652
1653
1654 def _postprocess_object_dict(self, object_dict):
1655 object_dict['full_status'] = self.full_status()
1656
1657
jadmanski0afbb632008-06-06 21:10:57 +00001658 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001659 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001660 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00001661
showard12f3e322009-05-13 21:27:42 +00001662
showard4c119042008-09-29 19:16:18 +00001663
showarda5288b42009-07-28 20:06:08 +00001664 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00001665 hostname = None
1666 if self.host:
1667 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00001668 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00001669
1670
showard4c119042008-09-29 19:16:18 +00001671class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001672 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00001673 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
1674 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00001675 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00001676
1677 objects = model_logic.ExtendedManager()
1678
showard68c7aa02008-10-09 16:49:11 +00001679
showarda5288b42009-07-28 20:06:08 +00001680 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00001681 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00001682 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00001683
showard4c119042008-09-29 19:16:18 +00001684 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001685 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001686 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00001687
1688
1689class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
1690 """\
1691 job: job to use as a template
1692 owner: owner of the instantiated template
1693 start_date: Run the job at scheduled date
1694 loop_period: Re-run (loop) the job periodically
1695 (in every loop_period seconds)
1696 loop_count: Re-run (loop) count
1697 """
1698
1699 job = dbmodels.ForeignKey(Job)
1700 owner = dbmodels.ForeignKey(User)
1701 start_date = dbmodels.DateTimeField()
1702 loop_period = dbmodels.IntegerField(blank=True)
1703 loop_count = dbmodels.IntegerField(blank=True)
1704
1705 objects = model_logic.ExtendedManager()
1706
1707 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001708 """Metadata for class RecurringRun."""
showardeab66ce2009-12-23 00:03:56 +00001709 db_table = 'afe_recurring_run'
showard29f7cd22009-04-29 21:16:24 +00001710
showarda5288b42009-07-28 20:06:08 +00001711 def __unicode__(self):
1712 return u'RecurringRun(job %s, start %s, period %s, count %s)' % (
showard29f7cd22009-04-29 21:16:24 +00001713 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +00001714
1715
1716class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
1717 """\
1718 Tasks to run on hosts at the next time they are in the Ready state. Use this
1719 for high-priority tasks, such as forced repair or forced reinstall.
1720
1721 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00001722 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00001723 time_requested: date and time the request for this task was made
1724 is_active: task is currently running
1725 is_complete: task has finished running
beeps8bb1f7d2013-08-05 01:30:09 -07001726 is_aborted: task was aborted
showard2fe3f1d2009-07-06 20:19:11 +00001727 time_started: date and time the task started
Dan Shid0725542014-06-23 15:34:27 -07001728 time_finished: date and time the task finished
showard2fe3f1d2009-07-06 20:19:11 +00001729 queue_entry: Host queue entry waiting on this task (or None, if task was not
1730 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00001731 """
Alex Millerdfff2fd2013-05-28 13:05:06 -07001732 Task = enum.Enum('Verify', 'Cleanup', 'Repair', 'Reset', 'Provision',
Dan Shi07e09af2013-04-12 09:31:29 -07001733 string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00001734
1735 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00001736 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00001737 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00001738 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00001739 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
1740 null=False)
1741 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
1742 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
beeps8bb1f7d2013-08-05 01:30:09 -07001743 is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00001744 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00001745 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00001746 success = dbmodels.BooleanField(default=False, blank=False, null=False)
Dan Shid0725542014-06-23 15:34:27 -07001747 time_finished = dbmodels.DateTimeField(null=True, blank=True)
showard6d7b2ff2009-06-10 00:16:47 +00001748
1749 objects = model_logic.ExtendedManager()
1750
1751
showard9bb960b2009-11-19 01:02:11 +00001752 def save(self, **kwargs):
1753 if self.queue_entry:
1754 self.requested_by = User.objects.get(
1755 login=self.queue_entry.job.owner)
1756 super(SpecialTask, self).save(**kwargs)
1757
1758
showarded2afea2009-07-07 20:54:07 +00001759 def execution_path(self):
showardc0ac3a72009-07-08 21:14:45 +00001760 """@see HostQueueEntry.execution_path()"""
showarded2afea2009-07-07 20:54:07 +00001761 return 'hosts/%s/%s-%s' % (self.host.hostname, self.id,
1762 self.task.lower())
1763
1764
showardc0ac3a72009-07-08 21:14:45 +00001765 # property to emulate HostQueueEntry.status
1766 @property
1767 def status(self):
1768 """
1769 Return a host queue entry status appropriate for this task. Although
1770 SpecialTasks are not HostQueueEntries, it is helpful to the user to
1771 present similar statuses.
1772 """
1773 if self.is_complete:
showarde60e44e2009-11-13 20:45:38 +00001774 if self.success:
1775 return HostQueueEntry.Status.COMPLETED
1776 return HostQueueEntry.Status.FAILED
showardc0ac3a72009-07-08 21:14:45 +00001777 if self.is_active:
1778 return HostQueueEntry.Status.RUNNING
1779 return HostQueueEntry.Status.QUEUED
1780
1781
1782 # property to emulate HostQueueEntry.started_on
1783 @property
1784 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001785 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00001786 return self.time_started
1787
1788
showard6d7b2ff2009-06-10 00:16:47 +00001789 @classmethod
showardc5103442010-01-15 00:20:26 +00001790 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001791 """Schedules a special task on a host if not already scheduled.
1792
1793 @param cls: Implicit class object.
1794 @param host: The host to use.
1795 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00001796 """
showardc5103442010-01-15 00:20:26 +00001797 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
1798 is_active=False,
1799 is_complete=False)
1800 if existing_tasks:
1801 return existing_tasks[0]
1802
1803 special_task = SpecialTask(host=host, task=task,
1804 requested_by=User.current_user())
1805 special_task.save()
1806 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00001807
1808
beeps8bb1f7d2013-08-05 01:30:09 -07001809 def abort(self):
1810 """ Abort this special task."""
1811 self.is_aborted = True
1812 self.save()
1813
1814
showarded2afea2009-07-07 20:54:07 +00001815 def activate(self):
showard474d1362009-08-20 23:32:01 +00001816 """
1817 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00001818 """
showard97446882009-07-20 22:37:28 +00001819 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00001820 self.is_active = True
1821 self.time_started = datetime.now()
1822 self.save()
1823
1824
showarde60e44e2009-11-13 20:45:38 +00001825 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001826 """Sets a task as completed.
1827
1828 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00001829 """
showard97446882009-07-20 22:37:28 +00001830 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00001831 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00001832 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00001833 self.success = success
Dan Shid85d6112014-07-14 10:32:55 -07001834 if self.time_started:
1835 self.time_finished = datetime.now()
showard2fe3f1d2009-07-06 20:19:11 +00001836 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00001837
1838
1839 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001840 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00001841 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00001842
showard474d1362009-08-20 23:32:01 +00001843
showarda5288b42009-07-28 20:06:08 +00001844 def __unicode__(self):
1845 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00001846 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00001847 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00001848 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00001849 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00001850 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00001851
1852 return result