blob: ff8c28bd7905b245f8c70c02dda568b8be580c84 [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
Jakob Juelich1b525742014-09-30 13:08:07 -0700436 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -0700437 """Assigns hosts to a shard.
438
Jakob Juelich1b525742014-09-30 13:08:07 -0700439 For all labels that have been assigned to this shard, all hosts that
440 have this label, are assigned to this shard.
441
442 Hosts that are assigned to the shard but aren't already present on the
443 shard are returned.
Jakob Juelich59cfe542014-09-02 16:37:46 -0700444
445 @param shard: The shard object to assign labels/hosts for.
Jakob Juelich1b525742014-09-30 13:08:07 -0700446 @param known_ids: List of all host-ids the shard already knows.
447 This is used to figure out which hosts should be sent
448 to the shard. If shard_ids were used instead, hosts
449 would only be transferred once, even if the client
450 failed persisting them.
451 The number of hosts usually lies in O(100), so the
452 overhead is acceptable.
453
Jakob Juelich59cfe542014-09-02 16:37:46 -0700454 @returns the hosts objects that should be sent to the shard.
455 """
456
457 # Disclaimer: concurrent heartbeats should theoretically not occur in
458 # the current setup. As they may be introduced in the near future,
459 # this comment will be left here.
460
461 # Sending stuff twice is acceptable, but forgetting something isn't.
462 # Detecting duplicates on the client is easy, but here it's harder. The
463 # following options were considered:
464 # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
465 # than select returned, as concurrently more hosts might have been
466 # inserted
467 # - UPDATE and then SELECT WHERE shard=shard: select always returns all
468 # hosts for the shard, this is overhead
469 # - SELECT and then UPDATE only selected without requerying afterwards:
470 # returns the old state of the records.
471 host_ids = list(Host.objects.filter(
Jakob Juelich59cfe542014-09-02 16:37:46 -0700472 labels=shard.labels.all(),
473 leased=False
Jakob Juelich1b525742014-09-30 13:08:07 -0700474 ).exclude(
475 id__in=known_ids,
Jakob Juelich59cfe542014-09-02 16:37:46 -0700476 ).values_list('pk', flat=True))
477
478 if host_ids:
479 Host.objects.filter(pk__in=host_ids).update(shard=shard)
480 return list(Host.objects.filter(pk__in=host_ids).all())
481 return []
482
showardafd97de2009-10-01 18:45:09 +0000483 def resurrect_object(self, old_object):
484 super(Host, self).resurrect_object(old_object)
485 # invalid hosts can be in use by the scheduler (as one-time hosts), so
486 # don't change the status
487 self.status = old_object.status
488
489
jadmanski0afbb632008-06-06 21:10:57 +0000490 def clean_object(self):
491 self.aclgroup_set.clear()
492 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000493
494
Dan Shia0acfbc2014-10-14 15:56:23 -0700495 def record_state(self, type_str, state, value, other_metadata=None):
496 """Record metadata in elasticsearch.
497
498 @param type_str: sets the _type field in elasticsearch db.
499 @param state: string representing what state we are recording,
500 e.g. 'locked'
501 @param value: value of the state, e.g. True
502 @param other_metadata: Other metadata to store in metaDB.
503 """
504 metadata = {
505 state: value,
506 'hostname': self.hostname,
507 }
508 if other_metadata:
509 metadata = dict(metadata.items() + other_metadata.items())
510 es_utils.ESMetadata().post(type_str=type_str, metadata=metadata)
511
512
showarda5288b42009-07-28 20:06:08 +0000513 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000514 # extra spaces in the hostname can be a sneaky source of errors
515 self.hostname = self.hostname.strip()
516 # is this a new object being saved for the first time?
517 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000518 if not first_time:
519 AclGroup.check_for_acl_violation_hosts([self])
Dan Shia0acfbc2014-10-14 15:56:23 -0700520 # If locked is changed, send its status and user made the change to
521 # metaDB. Locks are important in host history because if a device is
522 # locked then we don't really care what state it is in.
showardfb2a7fa2008-07-17 17:04:12 +0000523 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000524 self.locked_by = User.current_user()
showardfb2a7fa2008-07-17 17:04:12 +0000525 self.lock_time = datetime.now()
Dan Shia0acfbc2014-10-14 15:56:23 -0700526 self.record_state('lock_history', 'locked', self.locked,
527 {'changed_by': self.locked_by.login})
showard21baa452008-10-21 00:08:39 +0000528 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000529 elif not self.locked and self.locked_by:
Dan Shia0acfbc2014-10-14 15:56:23 -0700530 self.record_state('lock_history', 'locked', self.locked,
531 {'changed_by': self.locked_by.login})
showardfb2a7fa2008-07-17 17:04:12 +0000532 self.locked_by = None
533 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000534 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000535 if first_time:
536 everyone = AclGroup.objects.get(name='Everyone')
537 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000538 self._check_for_updated_attributes()
539
mblighe8819cd2008-02-15 16:48:40 +0000540
showardb8471e32008-07-03 19:51:08 +0000541 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000542 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000543 for queue_entry in self.hostqueueentry_set.all():
544 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000545 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000546 super(Host, self).delete()
547
mblighe8819cd2008-02-15 16:48:40 +0000548
showard2bab8f42008-11-12 18:15:22 +0000549 def on_attribute_changed(self, attribute, old_value):
550 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000551 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000552
553
showard29f7cd22009-04-29 21:16:24 +0000554 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800555 """Enqueue a job on this host.
556
557 @param job: A job to enqueue.
558 @param atomic_group: The associated atomic group.
559 @param is_template: Whther the status should be "Template".
560 """
showard29f7cd22009-04-29 21:16:24 +0000561 queue_entry = HostQueueEntry.create(host=self, job=job,
562 is_template=is_template,
563 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000564 # allow recovery of dead hosts from the frontend
565 if not self.active_queue_entry() and self.is_dead():
566 self.status = Host.Status.READY
567 self.save()
568 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000569
showard08f981b2008-06-24 21:59:03 +0000570 block = IneligibleHostQueue(job=job, host=self)
571 block.save()
572
mblighe8819cd2008-02-15 16:48:40 +0000573
jadmanski0afbb632008-06-06 21:10:57 +0000574 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800575 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000576 # TODO(showard): slighly hacky?
577 platforms = self.labels.filter(platform=True)
578 if len(platforms) == 0:
579 return None
580 return platforms[0]
581 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000582
583
showardcafd16e2009-05-29 18:37:49 +0000584 @classmethod
585 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800586 """Verify the specified hosts have no associated platforms.
587
588 @param cls: Implicit class object.
589 @param hosts: The hosts to verify.
590 @raises model_logic.ValidationError if any hosts already have a
591 platform.
592 """
showardcafd16e2009-05-29 18:37:49 +0000593 Host.objects.populate_relationships(hosts, Label, 'label_list')
594 errors = []
595 for host in hosts:
596 platforms = [label.name for label in host.label_list
597 if label.platform]
598 if platforms:
599 # do a join, just in case this host has multiple platforms,
600 # we'll be able to see it
601 errors.append('Host %s already has a platform: %s' % (
602 host.hostname, ', '.join(platforms)))
603 if errors:
604 raise model_logic.ValidationError({'labels': '; '.join(errors)})
605
606
jadmanski0afbb632008-06-06 21:10:57 +0000607 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800608 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000609 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000610
611
jadmanski0afbb632008-06-06 21:10:57 +0000612 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800613 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000614 active = list(self.hostqueueentry_set.filter(active=True))
615 if not active:
616 return None
617 assert len(active) == 1, ('More than one active entry for '
618 'host ' + self.hostname)
619 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000620
621
showardf8b19042009-05-12 17:22:49 +0000622 def _get_attribute_model_and_args(self, attribute):
623 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000624
625
jadmanski0afbb632008-06-06 21:10:57 +0000626 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800627 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000628 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000629
showarda5288b42009-07-28 20:06:08 +0000630 def __unicode__(self):
631 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000632
633
MK Ryuacf35922014-10-03 14:56:49 -0700634class HostAttribute(dbmodels.Model, model_logic.ModelExtensions):
showard0957a842009-05-11 19:25:08 +0000635 """Arbitrary keyvals associated with hosts."""
636 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000637 attribute = dbmodels.CharField(max_length=90)
638 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000639
640 objects = model_logic.ExtendedManager()
641
642 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800643 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000644 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000645
646
showard7c785282008-05-29 19:45:12 +0000647class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000648 """\
649 Required:
showard909c7a62008-07-15 21:52:38 +0000650 author: author name
651 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000652 name: test name
showard909c7a62008-07-15 21:52:38 +0000653 time: short, medium, long
654 test_class: This describes the class for your the test belongs in.
655 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000656 test_type: Client or Server
657 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000658 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
659 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000660 i.e. if sync_count = 2 it is a sync job that requires two
661 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000662 Optional:
showard909c7a62008-07-15 21:52:38 +0000663 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000664 dependency_labels: many-to-many relationship with labels corresponding to
665 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000666 experimental: If this is set to True production servers will ignore the test
667 run_verify: Whether or not the scheduler should run the verify stage
Dan Shi07e09af2013-04-12 09:31:29 -0700668 run_reset: Whether or not the scheduler should run the reset stage
Aviv Keshet9af96d32013-03-05 12:56:24 -0800669 test_retry: Number of times to retry test if the test did not complete
670 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +0000671 """
showard909c7a62008-07-15 21:52:38 +0000672 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000673
showarda5288b42009-07-28 20:06:08 +0000674 name = dbmodels.CharField(max_length=255, unique=True)
675 author = dbmodels.CharField(max_length=255)
676 test_class = dbmodels.CharField(max_length=255)
677 test_category = dbmodels.CharField(max_length=255)
678 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000679 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000680 experimental = dbmodels.BooleanField(default=True)
Dan Shi07e09af2013-04-12 09:31:29 -0700681 run_verify = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +0000682 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
683 default=TestTime.MEDIUM)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700684 test_type = dbmodels.SmallIntegerField(
685 choices=control_data.CONTROL_TYPE.choices())
showard909c7a62008-07-15 21:52:38 +0000686 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000687 path = dbmodels.CharField(max_length=255, unique=True)
Aviv Keshet9af96d32013-03-05 12:56:24 -0800688 test_retry = dbmodels.IntegerField(blank=True, default=0)
Dan Shi07e09af2013-04-12 09:31:29 -0700689 run_reset = dbmodels.BooleanField(default=True)
mblighe8819cd2008-02-15 16:48:40 +0000690
showardeab66ce2009-12-23 00:03:56 +0000691 dependency_labels = (
692 dbmodels.ManyToManyField(Label, blank=True,
693 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000694 name_field = 'name'
695 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000696
697
jamesren35a70222010-02-16 19:30:46 +0000698 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800699 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +0000700 escaped_description = saxutils.escape(self.description)
701 return '<span style="white-space:pre">%s</span>' % escaped_description
702 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000703 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000704
705
jadmanski0afbb632008-06-06 21:10:57 +0000706 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800707 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +0000708 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000709
showarda5288b42009-07-28 20:06:08 +0000710 def __unicode__(self):
711 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000712
713
jamesren4a41e012010-07-16 22:33:48 +0000714class TestParameter(dbmodels.Model):
715 """
716 A declared parameter of a test
717 """
718 test = dbmodels.ForeignKey(Test)
719 name = dbmodels.CharField(max_length=255)
720
721 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800722 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000723 db_table = 'afe_test_parameters'
724 unique_together = ('test', 'name')
725
726 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -0700727 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +0000728
729
showard2b9a88b2008-06-13 20:55:03 +0000730class Profiler(dbmodels.Model, model_logic.ModelExtensions):
731 """\
732 Required:
733 name: profiler name
734 test_type: Client or Server
735
736 Optional:
737 description: arbirary text description
738 """
showarda5288b42009-07-28 20:06:08 +0000739 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000740 description = dbmodels.TextField(blank=True)
741
742 name_field = 'name'
743 objects = model_logic.ExtendedManager()
744
745
746 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800747 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +0000748 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000749
showarda5288b42009-07-28 20:06:08 +0000750 def __unicode__(self):
751 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000752
753
showard7c785282008-05-29 19:45:12 +0000754class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000755 """\
756 Required:
757 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000758
jadmanski0afbb632008-06-06 21:10:57 +0000759 Optional:
760 description: arbitrary description of group
761 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700762
763 SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
764
showarda5288b42009-07-28 20:06:08 +0000765 name = dbmodels.CharField(max_length=255, unique=True)
766 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000767 users = dbmodels.ManyToManyField(User, blank=False,
768 db_table='afe_acl_groups_users')
769 hosts = dbmodels.ManyToManyField(Host, blank=True,
770 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000771
jadmanski0afbb632008-06-06 21:10:57 +0000772 name_field = 'name'
773 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000774
showard08f981b2008-06-24 21:59:03 +0000775 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000776 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800777 """Verify the current user has access to the specified hosts.
778
779 @param hosts: The hosts to verify against.
780 @raises AclAccessViolation if the current user doesn't have access
781 to a host.
782 """
showard64a95952010-01-13 21:27:16 +0000783 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000784 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000785 return
showard3dd47c22008-07-10 00:41:36 +0000786 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000787 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000788 for host in hosts:
789 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800790 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +0000791 no_access = (isinstance(host, Host)
792 and not host.invalid
793 and int(host.id) not in accessible_host_ids)
794 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000795 raise AclAccessViolation("%s does not have access to %s" %
796 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000797
showard9dbdcda2008-10-14 17:34:36 +0000798
799 @staticmethod
showarddc817512008-11-12 18:16:41 +0000800 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800801 """Look for queue entries that aren't abortable by the current user.
802
803 An entry is not abortable if:
804 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +0000805 * the machine isn't ACL-accessible, or
806 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800807
808 @param queue_entries: The queue entries to check.
809 @raises AclAccessViolation if a queue entry is not abortable by the
810 current user.
showarddc817512008-11-12 18:16:41 +0000811 """
showard64a95952010-01-13 21:27:16 +0000812 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000813 if user.is_superuser():
814 return
showarddc817512008-11-12 18:16:41 +0000815 not_owned = queue_entries.exclude(job__owner=user.login)
816 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000817 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
818 # 1.0.
819 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000820 accessible_ids = set(
821 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000822 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000823 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000824 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000825 cannot_abort = [entry for entry in not_owned.select_related()
826 if entry.id not in accessible_ids
827 or entry.id in public_ids]
828 if len(cannot_abort) == 0:
829 return
830 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000831 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000832 for entry in cannot_abort)
833 raise AclAccessViolation('You cannot abort the following job entries: '
834 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000835
836
showard3dd47c22008-07-10 00:41:36 +0000837 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800838 """Verifies the current user has acces to this ACL group.
839
840 @raises AclAccessViolation if the current user doesn't have access to
841 this ACL group.
842 """
showard64a95952010-01-13 21:27:16 +0000843 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000844 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000845 return
846 if self.name == 'Everyone':
847 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000848 if not user in self.users.all():
849 raise AclAccessViolation("You do not have access to %s"
850 % self.name)
851
852 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000853 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800854 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +0000855 everyone = AclGroup.objects.get(name='Everyone')
856
showard3dd47c22008-07-10 00:41:36 +0000857 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000858 # TODO(showard): this is a bit of a hack, since the fact that this query
859 # works is kind of a coincidence of Django internals. This trick
860 # doesn't work in general (on all foreign key relationships). I'll
861 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000862 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000863 everyone.hosts.add(*orphaned_hosts.distinct())
864
865 # find hosts in both Everyone and another ACL group, and remove them
866 # from Everyone
showarda5288b42009-07-28 20:06:08 +0000867 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
868 acled_hosts = set()
869 for host in hosts_in_everyone:
870 # Has an ACL group other than Everyone
871 if host.aclgroup_set.count() > 1:
872 acled_hosts.add(host)
873 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +0000874
875
876 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000877 if (self.name == 'Everyone'):
878 raise AclAccessViolation("You cannot delete 'Everyone'!")
879 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000880 super(AclGroup, self).delete()
881 self.on_host_membership_change()
882
883
showard04f2cd82008-07-25 20:53:31 +0000884 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800885 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +0000886 if not self.users.count():
showard64a95952010-01-13 21:27:16 +0000887 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +0000888
889
showard8cbaf1e2009-09-08 16:27:04 +0000890 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800891 """Called after a save.
892
893 @param change: Whether there was a change.
894 """
showard8cbaf1e2009-09-08 16:27:04 +0000895 if not change:
showard64a95952010-01-13 21:27:16 +0000896 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +0000897 self.add_current_user_if_empty()
898 self.on_host_membership_change()
899
900
901 def save(self, *args, **kwargs):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700902 change = bool(self.id)
903 if change:
showard8cbaf1e2009-09-08 16:27:04 +0000904 # Check the original object for an ACL violation
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700905 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
showard8cbaf1e2009-09-08 16:27:04 +0000906 super(AclGroup, self).save(*args, **kwargs)
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700907 self.perform_after_save(change)
showard8cbaf1e2009-09-08 16:27:04 +0000908
showardeb3be4d2008-04-21 20:59:26 +0000909
jadmanski0afbb632008-06-06 21:10:57 +0000910 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800911 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +0000912 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000913
showarda5288b42009-07-28 20:06:08 +0000914 def __unicode__(self):
915 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000916
mblighe8819cd2008-02-15 16:48:40 +0000917
jamesren4a41e012010-07-16 22:33:48 +0000918class Kernel(dbmodels.Model):
919 """
920 A kernel configuration for a parameterized job
921 """
922 version = dbmodels.CharField(max_length=255)
923 cmdline = dbmodels.CharField(max_length=255, blank=True)
924
925 @classmethod
926 def create_kernels(cls, kernel_list):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800927 """Creates all kernels in the kernel list.
jamesren4a41e012010-07-16 22:33:48 +0000928
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800929 @param cls: Implicit class object.
930 @param kernel_list: A list of dictionaries that describe the kernels,
931 in the same format as the 'kernel' argument to
932 rpc_interface.generate_control_file.
933 @return A list of the created kernels.
jamesren4a41e012010-07-16 22:33:48 +0000934 """
935 if not kernel_list:
936 return None
937 return [cls._create(kernel) for kernel in kernel_list]
938
939
940 @classmethod
941 def _create(cls, kernel_dict):
942 version = kernel_dict.pop('version')
943 cmdline = kernel_dict.pop('cmdline', '')
944
945 if kernel_dict:
946 raise Exception('Extraneous kernel arguments remain: %r'
947 % kernel_dict)
948
949 kernel, _ = cls.objects.get_or_create(version=version,
950 cmdline=cmdline)
951 return kernel
952
953
954 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800955 """Metadata for class Kernel."""
jamesren4a41e012010-07-16 22:33:48 +0000956 db_table = 'afe_kernels'
957 unique_together = ('version', 'cmdline')
958
959 def __unicode__(self):
960 return u'%s %s' % (self.version, self.cmdline)
961
962
963class ParameterizedJob(dbmodels.Model):
964 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800965 Auxiliary configuration for a parameterized job.
jamesren4a41e012010-07-16 22:33:48 +0000966 """
967 test = dbmodels.ForeignKey(Test)
968 label = dbmodels.ForeignKey(Label, null=True)
969 use_container = dbmodels.BooleanField(default=False)
970 profile_only = dbmodels.BooleanField(default=False)
971 upload_kernel_config = dbmodels.BooleanField(default=False)
972
973 kernels = dbmodels.ManyToManyField(
974 Kernel, db_table='afe_parameterized_job_kernels')
975 profilers = dbmodels.ManyToManyField(
976 Profiler, through='ParameterizedJobProfiler')
977
978
979 @classmethod
980 def smart_get(cls, id_or_name, *args, **kwargs):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800981 """For compatibility with Job.add_object.
982
983 @param cls: Implicit class object.
984 @param id_or_name: The ID or name to get.
985 @param args: Non-keyword arguments.
986 @param kwargs: Keyword arguments.
987 """
jamesren4a41e012010-07-16 22:33:48 +0000988 return cls.objects.get(pk=id_or_name)
989
990
991 def job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800992 """Returns the job if it exists, or else None."""
jamesren4a41e012010-07-16 22:33:48 +0000993 jobs = self.job_set.all()
994 assert jobs.count() <= 1
995 return jobs and jobs[0] or None
996
997
998 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800999 """Metadata for class ParameterizedJob."""
jamesren4a41e012010-07-16 22:33:48 +00001000 db_table = 'afe_parameterized_jobs'
1001
1002 def __unicode__(self):
1003 return u'%s (parameterized) - %s' % (self.test.name, self.job())
1004
1005
1006class ParameterizedJobProfiler(dbmodels.Model):
1007 """
1008 A profiler to run on a parameterized job
1009 """
1010 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1011 profiler = dbmodels.ForeignKey(Profiler)
1012
1013 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001014 """Metedata for class ParameterizedJobProfiler."""
jamesren4a41e012010-07-16 22:33:48 +00001015 db_table = 'afe_parameterized_jobs_profilers'
1016 unique_together = ('parameterized_job', 'profiler')
1017
1018
1019class ParameterizedJobProfilerParameter(dbmodels.Model):
1020 """
1021 A parameter for a profiler in a parameterized job
1022 """
1023 parameterized_job_profiler = dbmodels.ForeignKey(ParameterizedJobProfiler)
1024 parameter_name = dbmodels.CharField(max_length=255)
1025 parameter_value = dbmodels.TextField()
1026 parameter_type = dbmodels.CharField(
1027 max_length=8, choices=model_attributes.ParameterTypes.choices())
1028
1029 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001030 """Metadata for class ParameterizedJobProfilerParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001031 db_table = 'afe_parameterized_job_profiler_parameters'
1032 unique_together = ('parameterized_job_profiler', 'parameter_name')
1033
1034 def __unicode__(self):
1035 return u'%s - %s' % (self.parameterized_job_profiler.profiler.name,
1036 self.parameter_name)
1037
1038
1039class ParameterizedJobParameter(dbmodels.Model):
1040 """
1041 Parameters for a parameterized job
1042 """
1043 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1044 test_parameter = dbmodels.ForeignKey(TestParameter)
1045 parameter_value = dbmodels.TextField()
1046 parameter_type = dbmodels.CharField(
1047 max_length=8, choices=model_attributes.ParameterTypes.choices())
1048
1049 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001050 """Metadata for class ParameterizedJobParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001051 db_table = 'afe_parameterized_job_parameters'
1052 unique_together = ('parameterized_job', 'test_parameter')
1053
1054 def __unicode__(self):
1055 return u'%s - %s' % (self.parameterized_job.job().name,
1056 self.test_parameter.name)
1057
1058
showard7c785282008-05-29 19:45:12 +00001059class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +00001060 'Custom manager to provide efficient status counts querying.'
1061 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001062 """Returns a dict mapping the given job IDs to their status count dicts.
1063
1064 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +00001065 """
1066 if not job_ids:
1067 return {}
1068 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1069 cursor = connection.cursor()
1070 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +00001071 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +00001072 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +00001073 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +00001074 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +00001075 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +00001076 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +00001077 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +00001078 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +00001079 full_status = HostQueueEntry.compute_full_status(status, aborted,
1080 complete)
showardb6d16622009-05-26 19:35:29 +00001081 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +00001082 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +00001083 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +00001084
1085
showard7c785282008-05-29 19:45:12 +00001086class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001087 """\
1088 owner: username of job owner
1089 name: job name (does not have to be unique)
Alex Miller7d658cf2013-09-04 16:00:35 -07001090 priority: Integer priority value. Higher is more important.
jadmanski0afbb632008-06-06 21:10:57 +00001091 control_file: contents of control file
1092 control_type: Client or Server
1093 created_on: date of job creation
1094 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +00001095 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +00001096 run_verify: Whether or not to run the verify phase
Dan Shi07e09af2013-04-12 09:31:29 -07001097 run_reset: Whether or not to run the reset phase
Simran Basi94dc0032013-11-12 14:09:46 -08001098 timeout: DEPRECATED - hours from queuing time until job times out
1099 timeout_mins: minutes from job queuing time until the job times out
Simran Basi34217022012-11-06 13:43:15 -08001100 max_runtime_hrs: DEPRECATED - hours from job starting time until job
1101 times out
1102 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +00001103 email_list: list of people to email on completion delimited by any of:
1104 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +00001105 dependency_labels: many-to-many relationship with labels corresponding to
1106 job dependencies
showard21baa452008-10-21 00:08:39 +00001107 reboot_before: Never, If dirty, or Always
1108 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001109 parse_failed_repair: if True, a failed repair launched by this job will have
1110 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001111 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001112 parent_job: Parent job (optional)
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001113 test_retry: Number of times to retry test if the test did not complete
1114 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +00001115 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001116
1117 # TODO: Investigate, if jobkeyval_set is really needed.
1118 # dynamic_suite will write them into an attached file for the drone, but
1119 # it doesn't seem like they are actually used. If they aren't used, remove
1120 # jobkeyval_set here.
1121 SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1122 'hostqueueentry_set',
1123 'jobkeyval_set',
1124 'shard'])
1125
1126
Jakob Juelichf88fa932014-09-03 17:58:04 -07001127 def _deserialize_relation(self, link, data):
1128 if link in ['hostqueueentry_set', 'jobkeyval_set']:
1129 for obj in data:
1130 obj['job_id'] = self.id
1131
1132 super(Job, self)._deserialize_relation(link, data)
1133
1134
1135 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001136 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -07001137 self.shard = Shard.deserialize(data)
1138
1139
Jakob Juelicha94efe62014-09-18 16:02:49 -07001140 def sanity_check_update_from_shard(self, shard, updated_serialized):
Jakob Juelich02e61292014-10-17 12:36:55 -07001141 # If the job got aborted on the master after the client fetched it
1142 # no shard_id will be set. The shard might still push updates though,
1143 # as the job might complete before the abort bit syncs to the shard.
1144 # Alternative considered: The master scheduler could be changed to not
1145 # set aborted jobs to completed that are sharded out. But that would
1146 # require database queries and seemed more complicated to implement.
1147 # This seems safe to do, as there won't be updates pushed from the wrong
1148 # shards should be powered off and wiped hen they are removed from the
1149 # master.
1150 if self.shard_id and self.shard_id != shard.id:
Jakob Juelicha94efe62014-09-18 16:02:49 -07001151 raise error.UnallowedRecordsSentToMaster(
1152 'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1153 'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1154 shard.id))
1155
1156
Simran Basi94dc0032013-11-12 14:09:46 -08001157 # TIMEOUT is deprecated.
showardb1e51872008-10-07 11:08:18 +00001158 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
Simran Basi94dc0032013-11-12 14:09:46 -08001159 'AUTOTEST_WEB', 'job_timeout_default', default=24)
1160 DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1161 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
Simran Basi34217022012-11-06 13:43:15 -08001162 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1163 # completed.
showard12f3e322009-05-13 21:27:42 +00001164 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1165 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001166 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1167 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001168 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1169 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1170 default=False)
showardb1e51872008-10-07 11:08:18 +00001171
showarda5288b42009-07-28 20:06:08 +00001172 owner = dbmodels.CharField(max_length=255)
1173 name = dbmodels.CharField(max_length=255)
Alex Miller7d658cf2013-09-04 16:00:35 -07001174 priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
jamesren4a41e012010-07-16 22:33:48 +00001175 control_file = dbmodels.TextField(null=True, blank=True)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001176 control_type = dbmodels.SmallIntegerField(
1177 choices=control_data.CONTROL_TYPE.choices(),
1178 blank=True, # to allow 0
1179 default=control_data.CONTROL_TYPE.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001180 created_on = dbmodels.DateTimeField()
Simran Basi8d89b642014-05-02 17:33:20 -07001181 synch_count = dbmodels.IntegerField(blank=True, default=0)
showardb1e51872008-10-07 11:08:18 +00001182 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
Dan Shi07e09af2013-04-12 09:31:29 -07001183 run_verify = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001184 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001185 dependency_labels = (
1186 dbmodels.ManyToManyField(Label, blank=True,
1187 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001188 reboot_before = dbmodels.SmallIntegerField(
1189 choices=model_attributes.RebootBefore.choices(), blank=True,
1190 default=DEFAULT_REBOOT_BEFORE)
1191 reboot_after = dbmodels.SmallIntegerField(
1192 choices=model_attributes.RebootAfter.choices(), blank=True,
1193 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001194 parse_failed_repair = dbmodels.BooleanField(
1195 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001196 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1197 # completed.
showard12f3e322009-05-13 21:27:42 +00001198 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001199 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001200 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001201
jamesren4a41e012010-07-16 22:33:48 +00001202 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1203 blank=True)
1204
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001205 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001206
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001207 test_retry = dbmodels.IntegerField(blank=True, default=0)
1208
Dan Shi07e09af2013-04-12 09:31:29 -07001209 run_reset = dbmodels.BooleanField(default=True)
1210
Simran Basi94dc0032013-11-12 14:09:46 -08001211 timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1212
Jakob Juelich8421d592014-09-17 15:27:06 -07001213 # If this is None on the master, a slave should be found.
1214 # If this is None on a slave, it should be synced back to the master
Jakob Jülich92c06332014-08-25 19:06:57 +00001215 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1216
jadmanski0afbb632008-06-06 21:10:57 +00001217 # custom manager
1218 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001219
1220
Alex Millerec212252014-02-28 16:48:34 -08001221 @decorators.cached_property
1222 def labels(self):
1223 """All the labels of this job"""
1224 # We need to convert dependency_labels to a list, because all() gives us
1225 # back an iterator, and storing/caching an iterator means we'd only be
1226 # able to read from it once.
1227 return list(self.dependency_labels.all())
1228
1229
jadmanski0afbb632008-06-06 21:10:57 +00001230 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001231 """Returns whether this job is of type server."""
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001232 return self.control_type == control_data.CONTROL_TYPE.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001233
1234
jadmanski0afbb632008-06-06 21:10:57 +00001235 @classmethod
jamesren4a41e012010-07-16 22:33:48 +00001236 def parameterized_jobs_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001237 """Returns whether parameterized jobs are enabled.
1238
1239 @param cls: Implicit class object.
1240 """
jamesren4a41e012010-07-16 22:33:48 +00001241 return global_config.global_config.get_config_value(
1242 'AUTOTEST_WEB', 'parameterized_jobs', type=bool)
1243
1244
1245 @classmethod
1246 def check_parameterized_job(cls, control_file, parameterized_job):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001247 """Checks that the job is valid given the global config settings.
jamesren4a41e012010-07-16 22:33:48 +00001248
1249 First, either control_file must be set, or parameterized_job must be
1250 set, but not both. Second, parameterized_job must be set if and only if
1251 the parameterized_jobs option in the global config is set to True.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001252
1253 @param cls: Implict class object.
1254 @param control_file: A control file.
1255 @param parameterized_job: A parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001256 """
1257 if not (bool(control_file) ^ bool(parameterized_job)):
1258 raise Exception('Job must have either control file or '
1259 'parameterization, but not both')
1260
1261 parameterized_jobs_enabled = cls.parameterized_jobs_enabled()
1262 if control_file and parameterized_jobs_enabled:
1263 raise Exception('Control file specified, but parameterized jobs '
1264 'are enabled')
1265 if parameterized_job and not parameterized_jobs_enabled:
1266 raise Exception('Parameterized job specified, but parameterized '
1267 'jobs are not enabled')
1268
1269
1270 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001271 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001272 """Creates a job.
1273
1274 The job is created by taking some information (the listed args) and
1275 filling in the rest of the necessary information.
1276
1277 @param cls: Implicit class object.
1278 @param owner: The owner for the job.
1279 @param options: An options object.
1280 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001281 """
showard3dd47c22008-07-10 00:41:36 +00001282 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001283
jamesren4a41e012010-07-16 22:33:48 +00001284 control_file = options.get('control_file')
1285 parameterized_job = options.get('parameterized_job')
jamesren4a41e012010-07-16 22:33:48 +00001286
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001287 # The current implementation of parameterized jobs requires that only
1288 # control files or parameterized jobs are used. Using the image
1289 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1290 # parameterized jobs and control files jobs, it does muck enough with
1291 # normal jobs by adding a parameterized id to them that this check will
1292 # fail. So for now we just skip this check.
1293 # cls.check_parameterized_job(control_file=control_file,
1294 # parameterized_job=parameterized_job)
showard4d233752010-01-20 19:06:40 +00001295 user = User.current_user()
1296 if options.get('reboot_before') is None:
1297 options['reboot_before'] = user.get_reboot_before_display()
1298 if options.get('reboot_after') is None:
1299 options['reboot_after'] = user.get_reboot_after_display()
1300
jamesren76fcf192010-04-21 20:39:50 +00001301 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1302
Simran Basi94dc0032013-11-12 14:09:46 -08001303 if options.get('timeout_mins') is None and options.get('timeout'):
1304 options['timeout_mins'] = options['timeout'] * 60
1305
jadmanski0afbb632008-06-06 21:10:57 +00001306 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001307 owner=owner,
1308 name=options['name'],
1309 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001310 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001311 control_type=options['control_type'],
1312 synch_count=options.get('synch_count'),
Simran Basi94dc0032013-11-12 14:09:46 -08001313 # timeout needs to be deleted in the future.
showarda1e74b32009-05-12 17:32:04 +00001314 timeout=options.get('timeout'),
Simran Basi94dc0032013-11-12 14:09:46 -08001315 timeout_mins=options.get('timeout_mins'),
Simran Basi34217022012-11-06 13:43:15 -08001316 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001317 run_verify=options.get('run_verify'),
1318 email_list=options.get('email_list'),
1319 reboot_before=options.get('reboot_before'),
1320 reboot_after=options.get('reboot_after'),
1321 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001322 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001323 drone_set=drone_set,
Aviv Keshet18308922013-02-19 17:49:49 -08001324 parameterized_job=parameterized_job,
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001325 parent_job=options.get('parent_job_id'),
Dan Shi07e09af2013-04-12 09:31:29 -07001326 test_retry=options.get('test_retry'),
1327 run_reset=options.get('run_reset'))
mblighe8819cd2008-02-15 16:48:40 +00001328
showarda1e74b32009-05-12 17:32:04 +00001329 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001330
jamesrend8b6e172010-04-16 23:45:00 +00001331 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001332 for key, value in options['keyvals'].iteritems():
1333 JobKeyval.objects.create(job=job, key=key, value=value)
1334
jadmanski0afbb632008-06-06 21:10:57 +00001335 return job
mblighe8819cd2008-02-15 16:48:40 +00001336
1337
Jakob Juelich59cfe542014-09-02 16:37:46 -07001338 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -07001339 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -07001340 """Assigns unassigned jobs to a shard.
1341
Jakob Juelich1b525742014-09-30 13:08:07 -07001342 For all labels that have been assigned to this shard, all jobs that
1343 have this label, are assigned to this shard.
1344
1345 Jobs that are assigned to the shard but aren't already present on the
1346 shard are returned.
1347
Jakob Juelich59cfe542014-09-02 16:37:46 -07001348 @param shard: The shard to assign jobs to.
Jakob Juelich1b525742014-09-30 13:08:07 -07001349 @param known_ids: List of all ids of incomplete jobs, the shard already
1350 knows about.
1351 This is used to figure out which jobs should be sent
1352 to the shard. If shard_ids were used instead, jobs
1353 would only be transferred once, even if the client
1354 failed persisting them.
1355 The number of unfinished jobs usually lies in O(1000).
1356 Assuming one id takes 8 chars in the json, this means
1357 overhead that lies in the lower kilobyte range.
1358 A not in query with 5000 id's takes about 30ms.
1359
Jakob Juelich59cfe542014-09-02 16:37:46 -07001360 @returns The job objects that should be sent to the shard.
1361 """
1362 # Disclaimer: Concurrent heartbeats should not occur in today's setup.
1363 # If this changes or they are triggered manually, this applies:
1364 # Jobs may be returned more than once by concurrent calls of this
1365 # function, as there is a race condition between SELECT and UPDATE.
1366 job_ids = list(Job.objects.filter(
Jakob Juelich59cfe542014-09-02 16:37:46 -07001367 dependency_labels=shard.labels.all()
1368 ).exclude(
Jakob Juelich1b525742014-09-30 13:08:07 -07001369 id__in=known_ids,
1370 hostqueueentry__aborted=False
1371 ).exclude(
Jakob Juelich59cfe542014-09-02 16:37:46 -07001372 hostqueueentry__complete=True
1373 ).exclude(
1374 hostqueueentry__active=True
Jakob Juelich852ec0d2014-09-15 18:28:23 -07001375 ).distinct().values_list('pk', flat=True))
Jakob Juelich59cfe542014-09-02 16:37:46 -07001376 if job_ids:
1377 Job.objects.filter(pk__in=job_ids).update(shard=shard)
1378 return list(Job.objects.filter(pk__in=job_ids).all())
1379 return []
1380
1381
jamesren4a41e012010-07-16 22:33:48 +00001382 def save(self, *args, **kwargs):
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001383 # The current implementation of parameterized jobs requires that only
1384 # control files or parameterized jobs are used. Using the image
1385 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1386 # parameterized jobs and control files jobs, it does muck enough with
1387 # normal jobs by adding a parameterized id to them that this check will
1388 # fail. So for now we just skip this check.
1389 # cls.check_parameterized_job(control_file=self.control_file,
1390 # parameterized_job=self.parameterized_job)
jamesren4a41e012010-07-16 22:33:48 +00001391 super(Job, self).save(*args, **kwargs)
1392
1393
showard29f7cd22009-04-29 21:16:24 +00001394 def queue(self, hosts, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001395 """Enqueue a job on the given hosts.
1396
1397 @param hosts: The hosts to use.
1398 @param atomic_group: The associated atomic group.
1399 @param is_template: Whether the status should be "Template".
1400 """
showarda9545c02009-12-18 22:44:26 +00001401 if not hosts:
1402 if atomic_group:
1403 # No hosts or labels are required to queue an atomic group
1404 # Job. However, if they are given, we respect them below.
1405 atomic_group.enqueue_job(self, is_template=is_template)
1406 else:
1407 # hostless job
1408 entry = HostQueueEntry.create(job=self, is_template=is_template)
1409 entry.save()
1410 return
1411
jadmanski0afbb632008-06-06 21:10:57 +00001412 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +00001413 host.enqueue_job(self, atomic_group=atomic_group,
1414 is_template=is_template)
1415
1416
1417 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001418 """Creates a recurring job.
1419
1420 @param start_date: The starting date of the job.
1421 @param loop_period: How often to re-run the job, in seconds.
1422 @param loop_count: The re-run count.
1423 @param owner: The owner of the job.
1424 """
showard29f7cd22009-04-29 21:16:24 +00001425 rec = RecurringRun(job=self, start_date=start_date,
1426 loop_period=loop_period,
1427 loop_count=loop_count,
1428 owner=User.objects.get(login=owner))
1429 rec.save()
1430 return rec.id
mblighe8819cd2008-02-15 16:48:40 +00001431
1432
jadmanski0afbb632008-06-06 21:10:57 +00001433 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001434 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001435 try:
1436 return User.objects.get(login=self.owner)
1437 except self.DoesNotExist:
1438 return None
mblighe8819cd2008-02-15 16:48:40 +00001439
1440
showard64a95952010-01-13 21:27:16 +00001441 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001442 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001443 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001444 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001445
1446
showardd1195652009-12-08 22:21:02 +00001447 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001448 """Returns a string tag for this job."""
showardd1195652009-12-08 22:21:02 +00001449 return '%s-%s' % (self.id, self.owner)
1450
1451
showardc1a98d12010-01-15 00:22:22 +00001452 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001453 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001454 return dict((keyval.key, keyval.value)
1455 for keyval in self.jobkeyval_set.all())
1456
1457
jadmanski0afbb632008-06-06 21:10:57 +00001458 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001459 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001460 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001461
showarda5288b42009-07-28 20:06:08 +00001462 def __unicode__(self):
1463 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001464
1465
showardc1a98d12010-01-15 00:22:22 +00001466class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1467 """Keyvals associated with jobs"""
1468 job = dbmodels.ForeignKey(Job)
1469 key = dbmodels.CharField(max_length=90)
1470 value = dbmodels.CharField(max_length=300)
1471
1472 objects = model_logic.ExtendedManager()
1473
1474 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001475 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001476 db_table = 'afe_job_keyvals'
1477
1478
showard7c785282008-05-29 19:45:12 +00001479class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001480 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001481 job = dbmodels.ForeignKey(Job)
1482 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001483
jadmanski0afbb632008-06-06 21:10:57 +00001484 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001485
jadmanski0afbb632008-06-06 21:10:57 +00001486 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001487 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001488 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001489
mblighe8819cd2008-02-15 16:48:40 +00001490
showard7c785282008-05-29 19:45:12 +00001491class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001492 """Represents a host queue entry."""
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001493
1494 SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
Jakob Juelichf865d332014-09-29 10:47:49 -07001495 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001496
Jakob Juelichf88fa932014-09-03 17:58:04 -07001497
1498 def custom_deserialize_relation(self, link, data):
1499 assert link == 'meta_host'
1500 self.meta_host = Label.deserialize(data)
1501
1502
Jakob Juelicha94efe62014-09-18 16:02:49 -07001503 def sanity_check_update_from_shard(self, shard, updated_serialized,
1504 job_ids_sent):
1505 if self.job_id not in job_ids_sent:
1506 raise error.UnallowedRecordsSentToMaster(
1507 'Sent HostQueueEntry without corresponding '
1508 'job entry: %s' % updated_serialized)
1509
1510
showardeaa408e2009-09-11 18:45:31 +00001511 Status = host_queue_entry_states.Status
1512 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001513 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001514
jadmanski0afbb632008-06-06 21:10:57 +00001515 job = dbmodels.ForeignKey(Job)
1516 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001517 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001518 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1519 db_column='meta_host')
1520 active = dbmodels.BooleanField(default=False)
1521 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001522 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001523 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1524 default='')
showard89f84db2009-03-12 20:39:13 +00001525 # If atomic_group is set, this is a virtual HostQueueEntry that will
1526 # be expanded into many actual hosts within the group at schedule time.
1527 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001528 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001529 started_on = dbmodels.DateTimeField(null=True, blank=True)
Fang Deng51599032014-06-23 17:24:27 -07001530 finished_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001531
jadmanski0afbb632008-06-06 21:10:57 +00001532 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001533
mblighe8819cd2008-02-15 16:48:40 +00001534
showard2bab8f42008-11-12 18:15:22 +00001535 def __init__(self, *args, **kwargs):
1536 super(HostQueueEntry, self).__init__(*args, **kwargs)
1537 self._record_attributes(['status'])
1538
1539
showard29f7cd22009-04-29 21:16:24 +00001540 @classmethod
1541 def create(cls, job, host=None, meta_host=None, atomic_group=None,
1542 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001543 """Creates a new host queue entry.
1544
1545 @param cls: Implicit class object.
1546 @param job: The associated job.
1547 @param host: The associated host.
1548 @param meta_host: The associated meta host.
1549 @param atomic_group: The associated atomic group.
1550 @param is_template: Whether the status should be "Template".
1551 """
showard29f7cd22009-04-29 21:16:24 +00001552 if is_template:
1553 status = cls.Status.TEMPLATE
1554 else:
1555 status = cls.Status.QUEUED
1556
1557 return cls(job=job, host=host, meta_host=meta_host,
1558 atomic_group=atomic_group, status=status)
1559
1560
showarda5288b42009-07-28 20:06:08 +00001561 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001562 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001563 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001564 self._check_for_updated_attributes()
1565
1566
showardc0ac3a72009-07-08 21:14:45 +00001567 def execution_path(self):
1568 """
1569 Path to this entry's results (relative to the base results directory).
1570 """
showardd1195652009-12-08 22:21:02 +00001571 return os.path.join(self.job.tag(), self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001572
1573
showard3f15eed2008-11-14 22:40:48 +00001574 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001575 """Returns the first non-None name found in priority order.
1576
1577 The priority order checked is: (1) host name; (2) meta host name; and
1578 (3) atomic group name.
1579 """
showard3f15eed2008-11-14 22:40:48 +00001580 if self.host:
1581 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001582 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +00001583 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +00001584 else:
1585 assert self.atomic_group, "no host, meta_host or atomic group!"
1586 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +00001587
1588
showard2bab8f42008-11-12 18:15:22 +00001589 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001590 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001591 self.active, self.complete = True, False
1592 elif self.status in self.COMPLETE_STATUSES:
1593 self.active, self.complete = False, True
1594 else:
1595 self.active, self.complete = False, False
1596
1597
1598 def on_attribute_changed(self, attribute, old_value):
1599 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001600 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1601 self.status)
showard2bab8f42008-11-12 18:15:22 +00001602
1603
jadmanski0afbb632008-06-06 21:10:57 +00001604 def is_meta_host_entry(self):
1605 'True if this is a entry has a meta_host instead of a host.'
1606 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001607
showarda3ab0d52008-11-03 19:03:47 +00001608
Simran Basic1b26762013-06-26 14:23:21 -07001609 # This code is shared between rpc_interface and models.HostQueueEntry.
1610 # Sadly due to circular imports between the 2 (crbug.com/230100) making it
1611 # a class method was the best way to refactor it. Attempting to put it in
1612 # rpc_utils or a new utils module failed as that would require us to import
1613 # models.py but to call it from here we would have to import the utils.py
1614 # thus creating a cycle.
1615 @classmethod
1616 def abort_host_queue_entries(cls, host_queue_entries):
1617 """Aborts a collection of host_queue_entries.
1618
1619 Abort these host queue entry and all host queue entries of jobs created
1620 by them.
1621
1622 @param host_queue_entries: List of host queue entries we want to abort.
1623 """
1624 # This isn't completely immune to race conditions since it's not atomic,
1625 # but it should be safe given the scheduler's behavior.
1626
1627 # TODO(milleral): crbug.com/230100
1628 # The |abort_host_queue_entries| rpc does nearly exactly this,
1629 # however, trying to re-use the code generates some horrible
1630 # circular import error. I'd be nice to refactor things around
1631 # sometime so the code could be reused.
1632
1633 # Fixpoint algorithm to find the whole tree of HQEs to abort to
1634 # minimize the total number of database queries:
1635 children = set()
1636 new_children = set(host_queue_entries)
1637 while new_children:
1638 children.update(new_children)
1639 new_child_ids = [hqe.job_id for hqe in new_children]
1640 new_children = HostQueueEntry.objects.filter(
1641 job__parent_job__in=new_child_ids,
1642 complete=False, aborted=False).all()
1643 # To handle circular parental relationships
1644 new_children = set(new_children) - children
1645
1646 # Associate a user with the host queue entries that we're about
1647 # to abort so that we can look up who to blame for the aborts.
1648 now = datetime.now()
1649 user = User.current_user()
1650 aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
1651 aborted_by=user, aborted_on=now) for hqe in children]
1652 AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
1653 # Bulk update all of the HQEs to set the abort bit.
1654 child_ids = [hqe.id for hqe in children]
1655 HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
1656
1657
Scott Zawalski23041432013-04-17 07:39:09 -07001658 def abort(self):
Alex Millerdea67042013-04-22 17:23:34 -07001659 """ Aborts this host queue entry.
Simran Basic1b26762013-06-26 14:23:21 -07001660
Alex Millerdea67042013-04-22 17:23:34 -07001661 Abort this host queue entry and all host queue entries of jobs created by
1662 this one.
1663
1664 """
showardd3dc1992009-04-22 21:01:40 +00001665 if not self.complete and not self.aborted:
Simran Basi97582a22013-06-27 12:03:21 -07001666 HostQueueEntry.abort_host_queue_entries([self])
mblighe8819cd2008-02-15 16:48:40 +00001667
showardd3dc1992009-04-22 21:01:40 +00001668
1669 @classmethod
1670 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001671 """Returns a modified status msg if the host queue entry was aborted.
1672
1673 @param cls: Implicit class object.
1674 @param status: The original status message.
1675 @param aborted: Whether the host queue entry was aborted.
1676 @param complete: Whether the host queue entry was completed.
1677 """
showardd3dc1992009-04-22 21:01:40 +00001678 if aborted and not complete:
1679 return 'Aborted (%s)' % status
1680 return status
1681
1682
1683 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001684 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00001685 return self.compute_full_status(self.status, self.aborted,
1686 self.complete)
1687
1688
1689 def _postprocess_object_dict(self, object_dict):
1690 object_dict['full_status'] = self.full_status()
1691
1692
jadmanski0afbb632008-06-06 21:10:57 +00001693 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001694 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001695 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00001696
showard12f3e322009-05-13 21:27:42 +00001697
showard4c119042008-09-29 19:16:18 +00001698
showarda5288b42009-07-28 20:06:08 +00001699 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00001700 hostname = None
1701 if self.host:
1702 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00001703 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00001704
1705
showard4c119042008-09-29 19:16:18 +00001706class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001707 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00001708 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
1709 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00001710 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00001711
1712 objects = model_logic.ExtendedManager()
1713
showard68c7aa02008-10-09 16:49:11 +00001714
showarda5288b42009-07-28 20:06:08 +00001715 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00001716 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00001717 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00001718
showard4c119042008-09-29 19:16:18 +00001719 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001720 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001721 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00001722
1723
1724class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
1725 """\
1726 job: job to use as a template
1727 owner: owner of the instantiated template
1728 start_date: Run the job at scheduled date
1729 loop_period: Re-run (loop) the job periodically
1730 (in every loop_period seconds)
1731 loop_count: Re-run (loop) count
1732 """
1733
1734 job = dbmodels.ForeignKey(Job)
1735 owner = dbmodels.ForeignKey(User)
1736 start_date = dbmodels.DateTimeField()
1737 loop_period = dbmodels.IntegerField(blank=True)
1738 loop_count = dbmodels.IntegerField(blank=True)
1739
1740 objects = model_logic.ExtendedManager()
1741
1742 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001743 """Metadata for class RecurringRun."""
showardeab66ce2009-12-23 00:03:56 +00001744 db_table = 'afe_recurring_run'
showard29f7cd22009-04-29 21:16:24 +00001745
showarda5288b42009-07-28 20:06:08 +00001746 def __unicode__(self):
1747 return u'RecurringRun(job %s, start %s, period %s, count %s)' % (
showard29f7cd22009-04-29 21:16:24 +00001748 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +00001749
1750
1751class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
1752 """\
1753 Tasks to run on hosts at the next time they are in the Ready state. Use this
1754 for high-priority tasks, such as forced repair or forced reinstall.
1755
1756 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00001757 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00001758 time_requested: date and time the request for this task was made
1759 is_active: task is currently running
1760 is_complete: task has finished running
beeps8bb1f7d2013-08-05 01:30:09 -07001761 is_aborted: task was aborted
showard2fe3f1d2009-07-06 20:19:11 +00001762 time_started: date and time the task started
Dan Shid0725542014-06-23 15:34:27 -07001763 time_finished: date and time the task finished
showard2fe3f1d2009-07-06 20:19:11 +00001764 queue_entry: Host queue entry waiting on this task (or None, if task was not
1765 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00001766 """
Alex Millerdfff2fd2013-05-28 13:05:06 -07001767 Task = enum.Enum('Verify', 'Cleanup', 'Repair', 'Reset', 'Provision',
Dan Shi07e09af2013-04-12 09:31:29 -07001768 string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00001769
1770 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00001771 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00001772 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00001773 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00001774 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
1775 null=False)
1776 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
1777 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
beeps8bb1f7d2013-08-05 01:30:09 -07001778 is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00001779 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00001780 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00001781 success = dbmodels.BooleanField(default=False, blank=False, null=False)
Dan Shid0725542014-06-23 15:34:27 -07001782 time_finished = dbmodels.DateTimeField(null=True, blank=True)
showard6d7b2ff2009-06-10 00:16:47 +00001783
1784 objects = model_logic.ExtendedManager()
1785
1786
showard9bb960b2009-11-19 01:02:11 +00001787 def save(self, **kwargs):
1788 if self.queue_entry:
1789 self.requested_by = User.objects.get(
1790 login=self.queue_entry.job.owner)
1791 super(SpecialTask, self).save(**kwargs)
1792
1793
showarded2afea2009-07-07 20:54:07 +00001794 def execution_path(self):
Prashanth Balasubramaniande87dea2014-11-09 17:47:10 -08001795 """Get the execution path of the SpecialTask.
1796
1797 This method returns different paths depending on where a
1798 the task ran:
1799 * Master: hosts/hostname/task_id-task_type
1800 * Shard: Master_path/time_created
1801 This is to work around the fact that a shard can fail independent
1802 of the master, and be replaced by another shard that has the same
1803 hosts. Without the time_created stamp the logs of the tasks running
1804 on the second shard will clobber the logs from the first in google
1805 storage, because task ids are not globally unique.
1806
1807 @return: An execution path for the task.
1808 """
1809 results_path = 'hosts/%s/%s-%s' % (self.host.hostname, self.id,
1810 self.task.lower())
1811
1812 # If we do this on the master it will break backward compatibility,
1813 # as there are tasks that currently don't have timestamps. If a host
1814 # or job has been sent to a shard, the rpc for that host/job will
1815 # be redirected to the shard, so this global_config check will happen
1816 # on the shard the logs are on.
1817 is_shard = global_config.global_config.get_config_value(
1818 'SHARD', 'shard_hostname', type=str, default='')
1819 if not is_shard:
1820 return results_path
1821
1822 # Generate a uid to disambiguate special task result directories
1823 # in case this shard fails. The simplest uid is the job_id, however
1824 # in rare cases tasks do not have jobs associated with them (eg:
1825 # frontend verify), so just use the creation timestamp. The clocks
1826 # between a shard and master should always be in sync. Any discrepancies
1827 # will be brought to our attention in the form of job timeouts.
1828 uid = self.time_requested.strftime('%Y%d%m%H%M%S')
1829
1830 # TODO: This is a hack, however it is the easiest way to achieve
1831 # correctness. There is currently some debate over the future of
1832 # tasks in our infrastructure and refactoring everything right
1833 # now isn't worth the time.
1834 return '%s/%s' % (results_path, uid)
showarded2afea2009-07-07 20:54:07 +00001835
1836
showardc0ac3a72009-07-08 21:14:45 +00001837 # property to emulate HostQueueEntry.status
1838 @property
1839 def status(self):
1840 """
1841 Return a host queue entry status appropriate for this task. Although
1842 SpecialTasks are not HostQueueEntries, it is helpful to the user to
1843 present similar statuses.
1844 """
1845 if self.is_complete:
showarde60e44e2009-11-13 20:45:38 +00001846 if self.success:
1847 return HostQueueEntry.Status.COMPLETED
1848 return HostQueueEntry.Status.FAILED
showardc0ac3a72009-07-08 21:14:45 +00001849 if self.is_active:
1850 return HostQueueEntry.Status.RUNNING
1851 return HostQueueEntry.Status.QUEUED
1852
1853
1854 # property to emulate HostQueueEntry.started_on
1855 @property
1856 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001857 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00001858 return self.time_started
1859
1860
showard6d7b2ff2009-06-10 00:16:47 +00001861 @classmethod
showardc5103442010-01-15 00:20:26 +00001862 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001863 """Schedules a special task on a host if not already scheduled.
1864
1865 @param cls: Implicit class object.
1866 @param host: The host to use.
1867 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00001868 """
showardc5103442010-01-15 00:20:26 +00001869 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
1870 is_active=False,
1871 is_complete=False)
1872 if existing_tasks:
1873 return existing_tasks[0]
1874
1875 special_task = SpecialTask(host=host, task=task,
1876 requested_by=User.current_user())
1877 special_task.save()
1878 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00001879
1880
beeps8bb1f7d2013-08-05 01:30:09 -07001881 def abort(self):
1882 """ Abort this special task."""
1883 self.is_aborted = True
1884 self.save()
1885
1886
showarded2afea2009-07-07 20:54:07 +00001887 def activate(self):
showard474d1362009-08-20 23:32:01 +00001888 """
1889 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00001890 """
showard97446882009-07-20 22:37:28 +00001891 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00001892 self.is_active = True
1893 self.time_started = datetime.now()
1894 self.save()
1895
1896
showarde60e44e2009-11-13 20:45:38 +00001897 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001898 """Sets a task as completed.
1899
1900 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00001901 """
showard97446882009-07-20 22:37:28 +00001902 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00001903 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00001904 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00001905 self.success = success
Dan Shid85d6112014-07-14 10:32:55 -07001906 if self.time_started:
1907 self.time_finished = datetime.now()
showard2fe3f1d2009-07-06 20:19:11 +00001908 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00001909
1910
1911 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001912 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00001913 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00001914
showard474d1362009-08-20 23:32:01 +00001915
showarda5288b42009-07-28 20:06:08 +00001916 def __unicode__(self):
1917 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00001918 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00001919 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00001920 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00001921 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00001922 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00001923
1924 return result