blob: f8a83f8dcab3f7d04b8ec7180715bbe61653d4c4 [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'])
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800385 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['invalid'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700386
Jakob Juelichf88fa932014-09-03 17:58:04 -0700387
388 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700389 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -0700390 self.shard = Shard.deserialize(data)
391
392
Prashanth B489b91d2014-03-15 12:17:16 -0700393 # Note: Only specify foreign keys here, specify all native host columns in
394 # rdb_model_extensions instead.
395 Protection = host_protections.Protection
showardeab66ce2009-12-23 00:03:56 +0000396 labels = dbmodels.ManyToManyField(Label, blank=True,
397 db_table='afe_hosts_labels')
showardfb2a7fa2008-07-17 17:04:12 +0000398 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
jadmanski0afbb632008-06-06 21:10:57 +0000399 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000400 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000401 valid_objects = model_logic.ValidObjectsManager()
beepscc9fc702013-12-02 12:45:38 -0800402 leased_objects = model_logic.LeasedHostManager()
mbligh5244cbb2008-04-24 20:39:52 +0000403
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700404 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
showard2bab8f42008-11-12 18:15:22 +0000405
406 def __init__(self, *args, **kwargs):
407 super(Host, self).__init__(*args, **kwargs)
408 self._record_attributes(['status'])
409
410
showardb8471e32008-07-03 19:51:08 +0000411 @staticmethod
412 def create_one_time_host(hostname):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800413 """Creates a one-time host.
414
415 @param hostname: The name for the host.
416 """
showardb8471e32008-07-03 19:51:08 +0000417 query = Host.objects.filter(hostname=hostname)
418 if query.count() == 0:
419 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000420 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000421 else:
422 host = query[0]
423 if not host.invalid:
424 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000425 'hostname' : '%s already exists in the autotest DB. '
426 'Select it rather than entering it as a one time '
427 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000428 })
showard1ab512b2008-07-30 23:39:04 +0000429 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000430 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000431 host.save()
showard2924b0a2009-06-18 23:16:15 +0000432 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000433 return host
mbligh5244cbb2008-04-24 20:39:52 +0000434
showard1ff7b2e2009-05-15 23:17:18 +0000435
Jakob Juelich59cfe542014-09-02 16:37:46 -0700436 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -0700437 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -0700438 """Assigns hosts to a shard.
439
Jakob Juelich1b525742014-09-30 13:08:07 -0700440 For all labels that have been assigned to this shard, all hosts that
441 have this label, are assigned to this shard.
442
443 Hosts that are assigned to the shard but aren't already present on the
444 shard are returned.
Jakob Juelich59cfe542014-09-02 16:37:46 -0700445
446 @param shard: The shard object to assign labels/hosts for.
Jakob Juelich1b525742014-09-30 13:08:07 -0700447 @param known_ids: List of all host-ids the shard already knows.
448 This is used to figure out which hosts should be sent
449 to the shard. If shard_ids were used instead, hosts
450 would only be transferred once, even if the client
451 failed persisting them.
452 The number of hosts usually lies in O(100), so the
453 overhead is acceptable.
454
Jakob Juelich59cfe542014-09-02 16:37:46 -0700455 @returns the hosts objects that should be sent to the shard.
456 """
457
458 # Disclaimer: concurrent heartbeats should theoretically not occur in
459 # the current setup. As they may be introduced in the near future,
460 # this comment will be left here.
461
462 # Sending stuff twice is acceptable, but forgetting something isn't.
463 # Detecting duplicates on the client is easy, but here it's harder. The
464 # following options were considered:
465 # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
466 # than select returned, as concurrently more hosts might have been
467 # inserted
468 # - UPDATE and then SELECT WHERE shard=shard: select always returns all
469 # hosts for the shard, this is overhead
470 # - SELECT and then UPDATE only selected without requerying afterwards:
471 # returns the old state of the records.
472 host_ids = list(Host.objects.filter(
Jakob Juelich59cfe542014-09-02 16:37:46 -0700473 labels=shard.labels.all(),
474 leased=False
Jakob Juelich1b525742014-09-30 13:08:07 -0700475 ).exclude(
476 id__in=known_ids,
Jakob Juelich59cfe542014-09-02 16:37:46 -0700477 ).values_list('pk', flat=True))
478
479 if host_ids:
480 Host.objects.filter(pk__in=host_ids).update(shard=shard)
481 return list(Host.objects.filter(pk__in=host_ids).all())
482 return []
483
showardafd97de2009-10-01 18:45:09 +0000484 def resurrect_object(self, old_object):
485 super(Host, self).resurrect_object(old_object)
486 # invalid hosts can be in use by the scheduler (as one-time hosts), so
487 # don't change the status
488 self.status = old_object.status
489
490
jadmanski0afbb632008-06-06 21:10:57 +0000491 def clean_object(self):
492 self.aclgroup_set.clear()
493 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000494
495
Dan Shia0acfbc2014-10-14 15:56:23 -0700496 def record_state(self, type_str, state, value, other_metadata=None):
497 """Record metadata in elasticsearch.
498
499 @param type_str: sets the _type field in elasticsearch db.
500 @param state: string representing what state we are recording,
501 e.g. 'locked'
502 @param value: value of the state, e.g. True
503 @param other_metadata: Other metadata to store in metaDB.
504 """
505 metadata = {
506 state: value,
507 'hostname': self.hostname,
508 }
509 if other_metadata:
510 metadata = dict(metadata.items() + other_metadata.items())
511 es_utils.ESMetadata().post(type_str=type_str, metadata=metadata)
512
513
showarda5288b42009-07-28 20:06:08 +0000514 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000515 # extra spaces in the hostname can be a sneaky source of errors
516 self.hostname = self.hostname.strip()
517 # is this a new object being saved for the first time?
518 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000519 if not first_time:
520 AclGroup.check_for_acl_violation_hosts([self])
Dan Shia0acfbc2014-10-14 15:56:23 -0700521 # If locked is changed, send its status and user made the change to
522 # metaDB. Locks are important in host history because if a device is
523 # locked then we don't really care what state it is in.
showardfb2a7fa2008-07-17 17:04:12 +0000524 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000525 self.locked_by = User.current_user()
showardfb2a7fa2008-07-17 17:04:12 +0000526 self.lock_time = datetime.now()
Dan Shia0acfbc2014-10-14 15:56:23 -0700527 self.record_state('lock_history', 'locked', self.locked,
528 {'changed_by': self.locked_by.login})
showard21baa452008-10-21 00:08:39 +0000529 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000530 elif not self.locked and self.locked_by:
Dan Shia0acfbc2014-10-14 15:56:23 -0700531 self.record_state('lock_history', 'locked', self.locked,
532 {'changed_by': self.locked_by.login})
showardfb2a7fa2008-07-17 17:04:12 +0000533 self.locked_by = None
534 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000535 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000536 if first_time:
537 everyone = AclGroup.objects.get(name='Everyone')
538 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000539 self._check_for_updated_attributes()
540
mblighe8819cd2008-02-15 16:48:40 +0000541
showardb8471e32008-07-03 19:51:08 +0000542 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000543 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000544 for queue_entry in self.hostqueueentry_set.all():
545 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000546 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000547 super(Host, self).delete()
548
mblighe8819cd2008-02-15 16:48:40 +0000549
showard2bab8f42008-11-12 18:15:22 +0000550 def on_attribute_changed(self, attribute, old_value):
551 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000552 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000553
554
showard29f7cd22009-04-29 21:16:24 +0000555 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800556 """Enqueue a job on this host.
557
558 @param job: A job to enqueue.
559 @param atomic_group: The associated atomic group.
560 @param is_template: Whther the status should be "Template".
561 """
showard29f7cd22009-04-29 21:16:24 +0000562 queue_entry = HostQueueEntry.create(host=self, job=job,
563 is_template=is_template,
564 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000565 # allow recovery of dead hosts from the frontend
566 if not self.active_queue_entry() and self.is_dead():
567 self.status = Host.Status.READY
568 self.save()
569 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000570
showard08f981b2008-06-24 21:59:03 +0000571 block = IneligibleHostQueue(job=job, host=self)
572 block.save()
573
mblighe8819cd2008-02-15 16:48:40 +0000574
jadmanski0afbb632008-06-06 21:10:57 +0000575 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800576 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000577 # TODO(showard): slighly hacky?
578 platforms = self.labels.filter(platform=True)
579 if len(platforms) == 0:
580 return None
581 return platforms[0]
582 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000583
584
showardcafd16e2009-05-29 18:37:49 +0000585 @classmethod
586 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800587 """Verify the specified hosts have no associated platforms.
588
589 @param cls: Implicit class object.
590 @param hosts: The hosts to verify.
591 @raises model_logic.ValidationError if any hosts already have a
592 platform.
593 """
showardcafd16e2009-05-29 18:37:49 +0000594 Host.objects.populate_relationships(hosts, Label, 'label_list')
595 errors = []
596 for host in hosts:
597 platforms = [label.name for label in host.label_list
598 if label.platform]
599 if platforms:
600 # do a join, just in case this host has multiple platforms,
601 # we'll be able to see it
602 errors.append('Host %s already has a platform: %s' % (
603 host.hostname, ', '.join(platforms)))
604 if errors:
605 raise model_logic.ValidationError({'labels': '; '.join(errors)})
606
607
jadmanski0afbb632008-06-06 21:10:57 +0000608 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800609 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000610 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000611
612
jadmanski0afbb632008-06-06 21:10:57 +0000613 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800614 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000615 active = list(self.hostqueueentry_set.filter(active=True))
616 if not active:
617 return None
618 assert len(active) == 1, ('More than one active entry for '
619 'host ' + self.hostname)
620 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000621
622
showardf8b19042009-05-12 17:22:49 +0000623 def _get_attribute_model_and_args(self, attribute):
624 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000625
626
jadmanski0afbb632008-06-06 21:10:57 +0000627 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800628 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000629 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000630
showarda5288b42009-07-28 20:06:08 +0000631 def __unicode__(self):
632 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000633
634
MK Ryuacf35922014-10-03 14:56:49 -0700635class HostAttribute(dbmodels.Model, model_logic.ModelExtensions):
showard0957a842009-05-11 19:25:08 +0000636 """Arbitrary keyvals associated with hosts."""
637 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000638 attribute = dbmodels.CharField(max_length=90)
639 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000640
641 objects = model_logic.ExtendedManager()
642
643 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800644 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000645 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000646
647
showard7c785282008-05-29 19:45:12 +0000648class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000649 """\
650 Required:
showard909c7a62008-07-15 21:52:38 +0000651 author: author name
652 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000653 name: test name
showard909c7a62008-07-15 21:52:38 +0000654 time: short, medium, long
655 test_class: This describes the class for your the test belongs in.
656 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000657 test_type: Client or Server
658 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000659 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
660 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000661 i.e. if sync_count = 2 it is a sync job that requires two
662 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000663 Optional:
showard909c7a62008-07-15 21:52:38 +0000664 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000665 dependency_labels: many-to-many relationship with labels corresponding to
666 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000667 experimental: If this is set to True production servers will ignore the test
668 run_verify: Whether or not the scheduler should run the verify stage
Dan Shi07e09af2013-04-12 09:31:29 -0700669 run_reset: Whether or not the scheduler should run the reset stage
Aviv Keshet9af96d32013-03-05 12:56:24 -0800670 test_retry: Number of times to retry test if the test did not complete
671 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +0000672 """
showard909c7a62008-07-15 21:52:38 +0000673 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000674
showarda5288b42009-07-28 20:06:08 +0000675 name = dbmodels.CharField(max_length=255, unique=True)
676 author = dbmodels.CharField(max_length=255)
677 test_class = dbmodels.CharField(max_length=255)
678 test_category = dbmodels.CharField(max_length=255)
679 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000680 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000681 experimental = dbmodels.BooleanField(default=True)
Dan Shi07e09af2013-04-12 09:31:29 -0700682 run_verify = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +0000683 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
684 default=TestTime.MEDIUM)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700685 test_type = dbmodels.SmallIntegerField(
686 choices=control_data.CONTROL_TYPE.choices())
showard909c7a62008-07-15 21:52:38 +0000687 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000688 path = dbmodels.CharField(max_length=255, unique=True)
Aviv Keshet9af96d32013-03-05 12:56:24 -0800689 test_retry = dbmodels.IntegerField(blank=True, default=0)
Dan Shi07e09af2013-04-12 09:31:29 -0700690 run_reset = dbmodels.BooleanField(default=True)
mblighe8819cd2008-02-15 16:48:40 +0000691
showardeab66ce2009-12-23 00:03:56 +0000692 dependency_labels = (
693 dbmodels.ManyToManyField(Label, blank=True,
694 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000695 name_field = 'name'
696 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000697
698
jamesren35a70222010-02-16 19:30:46 +0000699 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800700 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +0000701 escaped_description = saxutils.escape(self.description)
702 return '<span style="white-space:pre">%s</span>' % escaped_description
703 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000704 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000705
706
jadmanski0afbb632008-06-06 21:10:57 +0000707 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800708 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +0000709 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000710
showarda5288b42009-07-28 20:06:08 +0000711 def __unicode__(self):
712 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000713
714
jamesren4a41e012010-07-16 22:33:48 +0000715class TestParameter(dbmodels.Model):
716 """
717 A declared parameter of a test
718 """
719 test = dbmodels.ForeignKey(Test)
720 name = dbmodels.CharField(max_length=255)
721
722 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800723 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000724 db_table = 'afe_test_parameters'
725 unique_together = ('test', 'name')
726
727 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -0700728 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +0000729
730
showard2b9a88b2008-06-13 20:55:03 +0000731class Profiler(dbmodels.Model, model_logic.ModelExtensions):
732 """\
733 Required:
734 name: profiler name
735 test_type: Client or Server
736
737 Optional:
738 description: arbirary text description
739 """
showarda5288b42009-07-28 20:06:08 +0000740 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000741 description = dbmodels.TextField(blank=True)
742
743 name_field = 'name'
744 objects = model_logic.ExtendedManager()
745
746
747 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800748 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +0000749 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000750
showarda5288b42009-07-28 20:06:08 +0000751 def __unicode__(self):
752 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000753
754
showard7c785282008-05-29 19:45:12 +0000755class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000756 """\
757 Required:
758 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000759
jadmanski0afbb632008-06-06 21:10:57 +0000760 Optional:
761 description: arbitrary description of group
762 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700763
764 SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
765
showarda5288b42009-07-28 20:06:08 +0000766 name = dbmodels.CharField(max_length=255, unique=True)
767 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000768 users = dbmodels.ManyToManyField(User, blank=False,
769 db_table='afe_acl_groups_users')
770 hosts = dbmodels.ManyToManyField(Host, blank=True,
771 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000772
jadmanski0afbb632008-06-06 21:10:57 +0000773 name_field = 'name'
774 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000775
showard08f981b2008-06-24 21:59:03 +0000776 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000777 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800778 """Verify the current user has access to the specified hosts.
779
780 @param hosts: The hosts to verify against.
781 @raises AclAccessViolation if the current user doesn't have access
782 to a host.
783 """
showard64a95952010-01-13 21:27:16 +0000784 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000785 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000786 return
showard3dd47c22008-07-10 00:41:36 +0000787 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000788 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000789 for host in hosts:
790 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800791 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +0000792 no_access = (isinstance(host, Host)
793 and not host.invalid
794 and int(host.id) not in accessible_host_ids)
795 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000796 raise AclAccessViolation("%s does not have access to %s" %
797 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000798
showard9dbdcda2008-10-14 17:34:36 +0000799
800 @staticmethod
showarddc817512008-11-12 18:16:41 +0000801 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800802 """Look for queue entries that aren't abortable by the current user.
803
804 An entry is not abortable if:
805 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +0000806 * the machine isn't ACL-accessible, or
807 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800808
809 @param queue_entries: The queue entries to check.
810 @raises AclAccessViolation if a queue entry is not abortable by the
811 current user.
showarddc817512008-11-12 18:16:41 +0000812 """
showard64a95952010-01-13 21:27:16 +0000813 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000814 if user.is_superuser():
815 return
showarddc817512008-11-12 18:16:41 +0000816 not_owned = queue_entries.exclude(job__owner=user.login)
817 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000818 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
819 # 1.0.
820 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000821 accessible_ids = set(
822 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000823 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000824 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000825 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000826 cannot_abort = [entry for entry in not_owned.select_related()
827 if entry.id not in accessible_ids
828 or entry.id in public_ids]
829 if len(cannot_abort) == 0:
830 return
831 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000832 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000833 for entry in cannot_abort)
834 raise AclAccessViolation('You cannot abort the following job entries: '
835 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000836
837
showard3dd47c22008-07-10 00:41:36 +0000838 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800839 """Verifies the current user has acces to this ACL group.
840
841 @raises AclAccessViolation if the current user doesn't have access to
842 this ACL group.
843 """
showard64a95952010-01-13 21:27:16 +0000844 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000845 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000846 return
847 if self.name == 'Everyone':
848 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000849 if not user in self.users.all():
850 raise AclAccessViolation("You do not have access to %s"
851 % self.name)
852
853 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000854 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800855 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +0000856 everyone = AclGroup.objects.get(name='Everyone')
857
showard3dd47c22008-07-10 00:41:36 +0000858 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000859 # TODO(showard): this is a bit of a hack, since the fact that this query
860 # works is kind of a coincidence of Django internals. This trick
861 # doesn't work in general (on all foreign key relationships). I'll
862 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000863 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000864 everyone.hosts.add(*orphaned_hosts.distinct())
865
866 # find hosts in both Everyone and another ACL group, and remove them
867 # from Everyone
showarda5288b42009-07-28 20:06:08 +0000868 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
869 acled_hosts = set()
870 for host in hosts_in_everyone:
871 # Has an ACL group other than Everyone
872 if host.aclgroup_set.count() > 1:
873 acled_hosts.add(host)
874 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +0000875
876
877 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000878 if (self.name == 'Everyone'):
879 raise AclAccessViolation("You cannot delete 'Everyone'!")
880 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000881 super(AclGroup, self).delete()
882 self.on_host_membership_change()
883
884
showard04f2cd82008-07-25 20:53:31 +0000885 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800886 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +0000887 if not self.users.count():
showard64a95952010-01-13 21:27:16 +0000888 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +0000889
890
showard8cbaf1e2009-09-08 16:27:04 +0000891 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800892 """Called after a save.
893
894 @param change: Whether there was a change.
895 """
showard8cbaf1e2009-09-08 16:27:04 +0000896 if not change:
showard64a95952010-01-13 21:27:16 +0000897 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +0000898 self.add_current_user_if_empty()
899 self.on_host_membership_change()
900
901
902 def save(self, *args, **kwargs):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700903 change = bool(self.id)
904 if change:
showard8cbaf1e2009-09-08 16:27:04 +0000905 # Check the original object for an ACL violation
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700906 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
showard8cbaf1e2009-09-08 16:27:04 +0000907 super(AclGroup, self).save(*args, **kwargs)
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700908 self.perform_after_save(change)
showard8cbaf1e2009-09-08 16:27:04 +0000909
showardeb3be4d2008-04-21 20:59:26 +0000910
jadmanski0afbb632008-06-06 21:10:57 +0000911 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800912 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +0000913 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000914
showarda5288b42009-07-28 20:06:08 +0000915 def __unicode__(self):
916 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000917
mblighe8819cd2008-02-15 16:48:40 +0000918
jamesren4a41e012010-07-16 22:33:48 +0000919class Kernel(dbmodels.Model):
920 """
921 A kernel configuration for a parameterized job
922 """
923 version = dbmodels.CharField(max_length=255)
924 cmdline = dbmodels.CharField(max_length=255, blank=True)
925
926 @classmethod
927 def create_kernels(cls, kernel_list):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800928 """Creates all kernels in the kernel list.
jamesren4a41e012010-07-16 22:33:48 +0000929
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800930 @param cls: Implicit class object.
931 @param kernel_list: A list of dictionaries that describe the kernels,
932 in the same format as the 'kernel' argument to
933 rpc_interface.generate_control_file.
934 @return A list of the created kernels.
jamesren4a41e012010-07-16 22:33:48 +0000935 """
936 if not kernel_list:
937 return None
938 return [cls._create(kernel) for kernel in kernel_list]
939
940
941 @classmethod
942 def _create(cls, kernel_dict):
943 version = kernel_dict.pop('version')
944 cmdline = kernel_dict.pop('cmdline', '')
945
946 if kernel_dict:
947 raise Exception('Extraneous kernel arguments remain: %r'
948 % kernel_dict)
949
950 kernel, _ = cls.objects.get_or_create(version=version,
951 cmdline=cmdline)
952 return kernel
953
954
955 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800956 """Metadata for class Kernel."""
jamesren4a41e012010-07-16 22:33:48 +0000957 db_table = 'afe_kernels'
958 unique_together = ('version', 'cmdline')
959
960 def __unicode__(self):
961 return u'%s %s' % (self.version, self.cmdline)
962
963
964class ParameterizedJob(dbmodels.Model):
965 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800966 Auxiliary configuration for a parameterized job.
jamesren4a41e012010-07-16 22:33:48 +0000967 """
968 test = dbmodels.ForeignKey(Test)
969 label = dbmodels.ForeignKey(Label, null=True)
970 use_container = dbmodels.BooleanField(default=False)
971 profile_only = dbmodels.BooleanField(default=False)
972 upload_kernel_config = dbmodels.BooleanField(default=False)
973
974 kernels = dbmodels.ManyToManyField(
975 Kernel, db_table='afe_parameterized_job_kernels')
976 profilers = dbmodels.ManyToManyField(
977 Profiler, through='ParameterizedJobProfiler')
978
979
980 @classmethod
981 def smart_get(cls, id_or_name, *args, **kwargs):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800982 """For compatibility with Job.add_object.
983
984 @param cls: Implicit class object.
985 @param id_or_name: The ID or name to get.
986 @param args: Non-keyword arguments.
987 @param kwargs: Keyword arguments.
988 """
jamesren4a41e012010-07-16 22:33:48 +0000989 return cls.objects.get(pk=id_or_name)
990
991
992 def job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800993 """Returns the job if it exists, or else None."""
jamesren4a41e012010-07-16 22:33:48 +0000994 jobs = self.job_set.all()
995 assert jobs.count() <= 1
996 return jobs and jobs[0] or None
997
998
999 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001000 """Metadata for class ParameterizedJob."""
jamesren4a41e012010-07-16 22:33:48 +00001001 db_table = 'afe_parameterized_jobs'
1002
1003 def __unicode__(self):
1004 return u'%s (parameterized) - %s' % (self.test.name, self.job())
1005
1006
1007class ParameterizedJobProfiler(dbmodels.Model):
1008 """
1009 A profiler to run on a parameterized job
1010 """
1011 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1012 profiler = dbmodels.ForeignKey(Profiler)
1013
1014 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001015 """Metedata for class ParameterizedJobProfiler."""
jamesren4a41e012010-07-16 22:33:48 +00001016 db_table = 'afe_parameterized_jobs_profilers'
1017 unique_together = ('parameterized_job', 'profiler')
1018
1019
1020class ParameterizedJobProfilerParameter(dbmodels.Model):
1021 """
1022 A parameter for a profiler in a parameterized job
1023 """
1024 parameterized_job_profiler = dbmodels.ForeignKey(ParameterizedJobProfiler)
1025 parameter_name = dbmodels.CharField(max_length=255)
1026 parameter_value = dbmodels.TextField()
1027 parameter_type = dbmodels.CharField(
1028 max_length=8, choices=model_attributes.ParameterTypes.choices())
1029
1030 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001031 """Metadata for class ParameterizedJobProfilerParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001032 db_table = 'afe_parameterized_job_profiler_parameters'
1033 unique_together = ('parameterized_job_profiler', 'parameter_name')
1034
1035 def __unicode__(self):
1036 return u'%s - %s' % (self.parameterized_job_profiler.profiler.name,
1037 self.parameter_name)
1038
1039
1040class ParameterizedJobParameter(dbmodels.Model):
1041 """
1042 Parameters for a parameterized job
1043 """
1044 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1045 test_parameter = dbmodels.ForeignKey(TestParameter)
1046 parameter_value = dbmodels.TextField()
1047 parameter_type = dbmodels.CharField(
1048 max_length=8, choices=model_attributes.ParameterTypes.choices())
1049
1050 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001051 """Metadata for class ParameterizedJobParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001052 db_table = 'afe_parameterized_job_parameters'
1053 unique_together = ('parameterized_job', 'test_parameter')
1054
1055 def __unicode__(self):
1056 return u'%s - %s' % (self.parameterized_job.job().name,
1057 self.test_parameter.name)
1058
1059
showard7c785282008-05-29 19:45:12 +00001060class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +00001061 'Custom manager to provide efficient status counts querying.'
1062 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001063 """Returns a dict mapping the given job IDs to their status count dicts.
1064
1065 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +00001066 """
1067 if not job_ids:
1068 return {}
1069 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1070 cursor = connection.cursor()
1071 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +00001072 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +00001073 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +00001074 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +00001075 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +00001076 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +00001077 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +00001078 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +00001079 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +00001080 full_status = HostQueueEntry.compute_full_status(status, aborted,
1081 complete)
showardb6d16622009-05-26 19:35:29 +00001082 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +00001083 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +00001084 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +00001085
1086
showard7c785282008-05-29 19:45:12 +00001087class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001088 """\
1089 owner: username of job owner
1090 name: job name (does not have to be unique)
Alex Miller7d658cf2013-09-04 16:00:35 -07001091 priority: Integer priority value. Higher is more important.
jadmanski0afbb632008-06-06 21:10:57 +00001092 control_file: contents of control file
1093 control_type: Client or Server
1094 created_on: date of job creation
1095 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +00001096 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +00001097 run_verify: Whether or not to run the verify phase
Dan Shi07e09af2013-04-12 09:31:29 -07001098 run_reset: Whether or not to run the reset phase
Simran Basi94dc0032013-11-12 14:09:46 -08001099 timeout: DEPRECATED - hours from queuing time until job times out
1100 timeout_mins: minutes from job queuing time until the job times out
Simran Basi34217022012-11-06 13:43:15 -08001101 max_runtime_hrs: DEPRECATED - hours from job starting time until job
1102 times out
1103 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +00001104 email_list: list of people to email on completion delimited by any of:
1105 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +00001106 dependency_labels: many-to-many relationship with labels corresponding to
1107 job dependencies
showard21baa452008-10-21 00:08:39 +00001108 reboot_before: Never, If dirty, or Always
1109 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001110 parse_failed_repair: if True, a failed repair launched by this job will have
1111 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001112 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001113 parent_job: Parent job (optional)
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001114 test_retry: Number of times to retry test if the test did not complete
1115 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +00001116 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001117
1118 # TODO: Investigate, if jobkeyval_set is really needed.
1119 # dynamic_suite will write them into an attached file for the drone, but
1120 # it doesn't seem like they are actually used. If they aren't used, remove
1121 # jobkeyval_set here.
1122 SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1123 'hostqueueentry_set',
1124 'jobkeyval_set',
1125 'shard'])
1126
1127
Jakob Juelichf88fa932014-09-03 17:58:04 -07001128 def _deserialize_relation(self, link, data):
1129 if link in ['hostqueueentry_set', 'jobkeyval_set']:
1130 for obj in data:
1131 obj['job_id'] = self.id
1132
1133 super(Job, self)._deserialize_relation(link, data)
1134
1135
1136 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001137 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -07001138 self.shard = Shard.deserialize(data)
1139
1140
Jakob Juelicha94efe62014-09-18 16:02:49 -07001141 def sanity_check_update_from_shard(self, shard, updated_serialized):
Jakob Juelich02e61292014-10-17 12:36:55 -07001142 # If the job got aborted on the master after the client fetched it
1143 # no shard_id will be set. The shard might still push updates though,
1144 # as the job might complete before the abort bit syncs to the shard.
1145 # Alternative considered: The master scheduler could be changed to not
1146 # set aborted jobs to completed that are sharded out. But that would
1147 # require database queries and seemed more complicated to implement.
1148 # This seems safe to do, as there won't be updates pushed from the wrong
1149 # shards should be powered off and wiped hen they are removed from the
1150 # master.
1151 if self.shard_id and self.shard_id != shard.id:
Jakob Juelicha94efe62014-09-18 16:02:49 -07001152 raise error.UnallowedRecordsSentToMaster(
1153 'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1154 'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1155 shard.id))
1156
1157
Simran Basi94dc0032013-11-12 14:09:46 -08001158 # TIMEOUT is deprecated.
showardb1e51872008-10-07 11:08:18 +00001159 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
Simran Basi94dc0032013-11-12 14:09:46 -08001160 'AUTOTEST_WEB', 'job_timeout_default', default=24)
1161 DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1162 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
Simran Basi34217022012-11-06 13:43:15 -08001163 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1164 # completed.
showard12f3e322009-05-13 21:27:42 +00001165 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1166 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001167 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1168 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001169 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1170 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1171 default=False)
showardb1e51872008-10-07 11:08:18 +00001172
showarda5288b42009-07-28 20:06:08 +00001173 owner = dbmodels.CharField(max_length=255)
1174 name = dbmodels.CharField(max_length=255)
Alex Miller7d658cf2013-09-04 16:00:35 -07001175 priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
jamesren4a41e012010-07-16 22:33:48 +00001176 control_file = dbmodels.TextField(null=True, blank=True)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001177 control_type = dbmodels.SmallIntegerField(
1178 choices=control_data.CONTROL_TYPE.choices(),
1179 blank=True, # to allow 0
1180 default=control_data.CONTROL_TYPE.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001181 created_on = dbmodels.DateTimeField()
Simran Basi8d89b642014-05-02 17:33:20 -07001182 synch_count = dbmodels.IntegerField(blank=True, default=0)
showardb1e51872008-10-07 11:08:18 +00001183 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
Dan Shi07e09af2013-04-12 09:31:29 -07001184 run_verify = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001185 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001186 dependency_labels = (
1187 dbmodels.ManyToManyField(Label, blank=True,
1188 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001189 reboot_before = dbmodels.SmallIntegerField(
1190 choices=model_attributes.RebootBefore.choices(), blank=True,
1191 default=DEFAULT_REBOOT_BEFORE)
1192 reboot_after = dbmodels.SmallIntegerField(
1193 choices=model_attributes.RebootAfter.choices(), blank=True,
1194 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001195 parse_failed_repair = dbmodels.BooleanField(
1196 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001197 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1198 # completed.
showard12f3e322009-05-13 21:27:42 +00001199 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001200 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001201 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001202
jamesren4a41e012010-07-16 22:33:48 +00001203 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1204 blank=True)
1205
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001206 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001207
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001208 test_retry = dbmodels.IntegerField(blank=True, default=0)
1209
Dan Shi07e09af2013-04-12 09:31:29 -07001210 run_reset = dbmodels.BooleanField(default=True)
1211
Simran Basi94dc0032013-11-12 14:09:46 -08001212 timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1213
Jakob Juelich8421d592014-09-17 15:27:06 -07001214 # If this is None on the master, a slave should be found.
1215 # If this is None on a slave, it should be synced back to the master
Jakob Jülich92c06332014-08-25 19:06:57 +00001216 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1217
jadmanski0afbb632008-06-06 21:10:57 +00001218 # custom manager
1219 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001220
1221
Alex Millerec212252014-02-28 16:48:34 -08001222 @decorators.cached_property
1223 def labels(self):
1224 """All the labels of this job"""
1225 # We need to convert dependency_labels to a list, because all() gives us
1226 # back an iterator, and storing/caching an iterator means we'd only be
1227 # able to read from it once.
1228 return list(self.dependency_labels.all())
1229
1230
jadmanski0afbb632008-06-06 21:10:57 +00001231 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001232 """Returns whether this job is of type server."""
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001233 return self.control_type == control_data.CONTROL_TYPE.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001234
1235
jadmanski0afbb632008-06-06 21:10:57 +00001236 @classmethod
jamesren4a41e012010-07-16 22:33:48 +00001237 def parameterized_jobs_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001238 """Returns whether parameterized jobs are enabled.
1239
1240 @param cls: Implicit class object.
1241 """
jamesren4a41e012010-07-16 22:33:48 +00001242 return global_config.global_config.get_config_value(
1243 'AUTOTEST_WEB', 'parameterized_jobs', type=bool)
1244
1245
1246 @classmethod
1247 def check_parameterized_job(cls, control_file, parameterized_job):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001248 """Checks that the job is valid given the global config settings.
jamesren4a41e012010-07-16 22:33:48 +00001249
1250 First, either control_file must be set, or parameterized_job must be
1251 set, but not both. Second, parameterized_job must be set if and only if
1252 the parameterized_jobs option in the global config is set to True.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001253
1254 @param cls: Implict class object.
1255 @param control_file: A control file.
1256 @param parameterized_job: A parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001257 """
1258 if not (bool(control_file) ^ bool(parameterized_job)):
1259 raise Exception('Job must have either control file or '
1260 'parameterization, but not both')
1261
1262 parameterized_jobs_enabled = cls.parameterized_jobs_enabled()
1263 if control_file and parameterized_jobs_enabled:
1264 raise Exception('Control file specified, but parameterized jobs '
1265 'are enabled')
1266 if parameterized_job and not parameterized_jobs_enabled:
1267 raise Exception('Parameterized job specified, but parameterized '
1268 'jobs are not enabled')
1269
1270
1271 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001272 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001273 """Creates a job.
1274
1275 The job is created by taking some information (the listed args) and
1276 filling in the rest of the necessary information.
1277
1278 @param cls: Implicit class object.
1279 @param owner: The owner for the job.
1280 @param options: An options object.
1281 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001282 """
showard3dd47c22008-07-10 00:41:36 +00001283 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001284
jamesren4a41e012010-07-16 22:33:48 +00001285 control_file = options.get('control_file')
1286 parameterized_job = options.get('parameterized_job')
jamesren4a41e012010-07-16 22:33:48 +00001287
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001288 # The current implementation of parameterized jobs requires that only
1289 # control files or parameterized jobs are used. Using the image
1290 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1291 # parameterized jobs and control files jobs, it does muck enough with
1292 # normal jobs by adding a parameterized id to them that this check will
1293 # fail. So for now we just skip this check.
1294 # cls.check_parameterized_job(control_file=control_file,
1295 # parameterized_job=parameterized_job)
showard4d233752010-01-20 19:06:40 +00001296 user = User.current_user()
1297 if options.get('reboot_before') is None:
1298 options['reboot_before'] = user.get_reboot_before_display()
1299 if options.get('reboot_after') is None:
1300 options['reboot_after'] = user.get_reboot_after_display()
1301
jamesren76fcf192010-04-21 20:39:50 +00001302 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1303
Simran Basi94dc0032013-11-12 14:09:46 -08001304 if options.get('timeout_mins') is None and options.get('timeout'):
1305 options['timeout_mins'] = options['timeout'] * 60
1306
jadmanski0afbb632008-06-06 21:10:57 +00001307 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001308 owner=owner,
1309 name=options['name'],
1310 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001311 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001312 control_type=options['control_type'],
1313 synch_count=options.get('synch_count'),
Simran Basi94dc0032013-11-12 14:09:46 -08001314 # timeout needs to be deleted in the future.
showarda1e74b32009-05-12 17:32:04 +00001315 timeout=options.get('timeout'),
Simran Basi94dc0032013-11-12 14:09:46 -08001316 timeout_mins=options.get('timeout_mins'),
Simran Basi34217022012-11-06 13:43:15 -08001317 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001318 run_verify=options.get('run_verify'),
1319 email_list=options.get('email_list'),
1320 reboot_before=options.get('reboot_before'),
1321 reboot_after=options.get('reboot_after'),
1322 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001323 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001324 drone_set=drone_set,
Aviv Keshet18308922013-02-19 17:49:49 -08001325 parameterized_job=parameterized_job,
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001326 parent_job=options.get('parent_job_id'),
Dan Shi07e09af2013-04-12 09:31:29 -07001327 test_retry=options.get('test_retry'),
1328 run_reset=options.get('run_reset'))
mblighe8819cd2008-02-15 16:48:40 +00001329
showarda1e74b32009-05-12 17:32:04 +00001330 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001331
jamesrend8b6e172010-04-16 23:45:00 +00001332 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001333 for key, value in options['keyvals'].iteritems():
1334 JobKeyval.objects.create(job=job, key=key, value=value)
1335
jadmanski0afbb632008-06-06 21:10:57 +00001336 return job
mblighe8819cd2008-02-15 16:48:40 +00001337
1338
Jakob Juelich59cfe542014-09-02 16:37:46 -07001339 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -07001340 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -07001341 """Assigns unassigned jobs to a shard.
1342
Jakob Juelich1b525742014-09-30 13:08:07 -07001343 For all labels that have been assigned to this shard, all jobs that
1344 have this label, are assigned to this shard.
1345
1346 Jobs that are assigned to the shard but aren't already present on the
1347 shard are returned.
1348
Jakob Juelich59cfe542014-09-02 16:37:46 -07001349 @param shard: The shard to assign jobs to.
Jakob Juelich1b525742014-09-30 13:08:07 -07001350 @param known_ids: List of all ids of incomplete jobs, the shard already
1351 knows about.
1352 This is used to figure out which jobs should be sent
1353 to the shard. If shard_ids were used instead, jobs
1354 would only be transferred once, even if the client
1355 failed persisting them.
1356 The number of unfinished jobs usually lies in O(1000).
1357 Assuming one id takes 8 chars in the json, this means
1358 overhead that lies in the lower kilobyte range.
1359 A not in query with 5000 id's takes about 30ms.
1360
Jakob Juelich59cfe542014-09-02 16:37:46 -07001361 @returns The job objects that should be sent to the shard.
1362 """
1363 # Disclaimer: Concurrent heartbeats should not occur in today's setup.
1364 # If this changes or they are triggered manually, this applies:
1365 # Jobs may be returned more than once by concurrent calls of this
1366 # function, as there is a race condition between SELECT and UPDATE.
1367 job_ids = list(Job.objects.filter(
Jakob Juelich59cfe542014-09-02 16:37:46 -07001368 dependency_labels=shard.labels.all()
1369 ).exclude(
Jakob Juelich1b525742014-09-30 13:08:07 -07001370 id__in=known_ids,
1371 hostqueueentry__aborted=False
1372 ).exclude(
Jakob Juelich59cfe542014-09-02 16:37:46 -07001373 hostqueueentry__complete=True
1374 ).exclude(
1375 hostqueueentry__active=True
Jakob Juelich852ec0d2014-09-15 18:28:23 -07001376 ).distinct().values_list('pk', flat=True))
Jakob Juelich59cfe542014-09-02 16:37:46 -07001377 if job_ids:
1378 Job.objects.filter(pk__in=job_ids).update(shard=shard)
1379 return list(Job.objects.filter(pk__in=job_ids).all())
1380 return []
1381
1382
jamesren4a41e012010-07-16 22:33:48 +00001383 def save(self, *args, **kwargs):
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001384 # The current implementation of parameterized jobs requires that only
1385 # control files or parameterized jobs are used. Using the image
1386 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1387 # parameterized jobs and control files jobs, it does muck enough with
1388 # normal jobs by adding a parameterized id to them that this check will
1389 # fail. So for now we just skip this check.
1390 # cls.check_parameterized_job(control_file=self.control_file,
1391 # parameterized_job=self.parameterized_job)
jamesren4a41e012010-07-16 22:33:48 +00001392 super(Job, self).save(*args, **kwargs)
1393
1394
showard29f7cd22009-04-29 21:16:24 +00001395 def queue(self, hosts, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001396 """Enqueue a job on the given hosts.
1397
1398 @param hosts: The hosts to use.
1399 @param atomic_group: The associated atomic group.
1400 @param is_template: Whether the status should be "Template".
1401 """
showarda9545c02009-12-18 22:44:26 +00001402 if not hosts:
1403 if atomic_group:
1404 # No hosts or labels are required to queue an atomic group
1405 # Job. However, if they are given, we respect them below.
1406 atomic_group.enqueue_job(self, is_template=is_template)
1407 else:
1408 # hostless job
1409 entry = HostQueueEntry.create(job=self, is_template=is_template)
1410 entry.save()
1411 return
1412
jadmanski0afbb632008-06-06 21:10:57 +00001413 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +00001414 host.enqueue_job(self, atomic_group=atomic_group,
1415 is_template=is_template)
1416
1417
1418 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001419 """Creates a recurring job.
1420
1421 @param start_date: The starting date of the job.
1422 @param loop_period: How often to re-run the job, in seconds.
1423 @param loop_count: The re-run count.
1424 @param owner: The owner of the job.
1425 """
showard29f7cd22009-04-29 21:16:24 +00001426 rec = RecurringRun(job=self, start_date=start_date,
1427 loop_period=loop_period,
1428 loop_count=loop_count,
1429 owner=User.objects.get(login=owner))
1430 rec.save()
1431 return rec.id
mblighe8819cd2008-02-15 16:48:40 +00001432
1433
jadmanski0afbb632008-06-06 21:10:57 +00001434 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001435 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001436 try:
1437 return User.objects.get(login=self.owner)
1438 except self.DoesNotExist:
1439 return None
mblighe8819cd2008-02-15 16:48:40 +00001440
1441
showard64a95952010-01-13 21:27:16 +00001442 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001443 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001444 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001445 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001446
1447
showardd1195652009-12-08 22:21:02 +00001448 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001449 """Returns a string tag for this job."""
showardd1195652009-12-08 22:21:02 +00001450 return '%s-%s' % (self.id, self.owner)
1451
1452
showardc1a98d12010-01-15 00:22:22 +00001453 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001454 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001455 return dict((keyval.key, keyval.value)
1456 for keyval in self.jobkeyval_set.all())
1457
1458
jadmanski0afbb632008-06-06 21:10:57 +00001459 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001460 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001461 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001462
showarda5288b42009-07-28 20:06:08 +00001463 def __unicode__(self):
1464 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001465
1466
showardc1a98d12010-01-15 00:22:22 +00001467class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1468 """Keyvals associated with jobs"""
1469 job = dbmodels.ForeignKey(Job)
1470 key = dbmodels.CharField(max_length=90)
1471 value = dbmodels.CharField(max_length=300)
1472
1473 objects = model_logic.ExtendedManager()
1474
1475 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001476 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001477 db_table = 'afe_job_keyvals'
1478
1479
showard7c785282008-05-29 19:45:12 +00001480class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001481 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001482 job = dbmodels.ForeignKey(Job)
1483 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001484
jadmanski0afbb632008-06-06 21:10:57 +00001485 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001486
jadmanski0afbb632008-06-06 21:10:57 +00001487 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001488 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001489 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001490
mblighe8819cd2008-02-15 16:48:40 +00001491
showard7c785282008-05-29 19:45:12 +00001492class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001493 """Represents a host queue entry."""
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001494
1495 SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
Jakob Juelichf865d332014-09-29 10:47:49 -07001496 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001497
Jakob Juelichf88fa932014-09-03 17:58:04 -07001498
1499 def custom_deserialize_relation(self, link, data):
1500 assert link == 'meta_host'
1501 self.meta_host = Label.deserialize(data)
1502
1503
Jakob Juelicha94efe62014-09-18 16:02:49 -07001504 def sanity_check_update_from_shard(self, shard, updated_serialized,
1505 job_ids_sent):
1506 if self.job_id not in job_ids_sent:
1507 raise error.UnallowedRecordsSentToMaster(
1508 'Sent HostQueueEntry without corresponding '
1509 'job entry: %s' % updated_serialized)
1510
1511
showardeaa408e2009-09-11 18:45:31 +00001512 Status = host_queue_entry_states.Status
1513 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001514 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001515
jadmanski0afbb632008-06-06 21:10:57 +00001516 job = dbmodels.ForeignKey(Job)
1517 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001518 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001519 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1520 db_column='meta_host')
1521 active = dbmodels.BooleanField(default=False)
1522 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001523 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001524 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1525 default='')
showard89f84db2009-03-12 20:39:13 +00001526 # If atomic_group is set, this is a virtual HostQueueEntry that will
1527 # be expanded into many actual hosts within the group at schedule time.
1528 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001529 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001530 started_on = dbmodels.DateTimeField(null=True, blank=True)
Fang Deng51599032014-06-23 17:24:27 -07001531 finished_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001532
jadmanski0afbb632008-06-06 21:10:57 +00001533 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001534
mblighe8819cd2008-02-15 16:48:40 +00001535
showard2bab8f42008-11-12 18:15:22 +00001536 def __init__(self, *args, **kwargs):
1537 super(HostQueueEntry, self).__init__(*args, **kwargs)
1538 self._record_attributes(['status'])
1539
1540
showard29f7cd22009-04-29 21:16:24 +00001541 @classmethod
1542 def create(cls, job, host=None, meta_host=None, atomic_group=None,
1543 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001544 """Creates a new host queue entry.
1545
1546 @param cls: Implicit class object.
1547 @param job: The associated job.
1548 @param host: The associated host.
1549 @param meta_host: The associated meta host.
1550 @param atomic_group: The associated atomic group.
1551 @param is_template: Whether the status should be "Template".
1552 """
showard29f7cd22009-04-29 21:16:24 +00001553 if is_template:
1554 status = cls.Status.TEMPLATE
1555 else:
1556 status = cls.Status.QUEUED
1557
1558 return cls(job=job, host=host, meta_host=meta_host,
1559 atomic_group=atomic_group, status=status)
1560
1561
showarda5288b42009-07-28 20:06:08 +00001562 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001563 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001564 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001565 self._check_for_updated_attributes()
1566
1567
showardc0ac3a72009-07-08 21:14:45 +00001568 def execution_path(self):
1569 """
1570 Path to this entry's results (relative to the base results directory).
1571 """
showardd1195652009-12-08 22:21:02 +00001572 return os.path.join(self.job.tag(), self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001573
1574
showard3f15eed2008-11-14 22:40:48 +00001575 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001576 """Returns the first non-None name found in priority order.
1577
1578 The priority order checked is: (1) host name; (2) meta host name; and
1579 (3) atomic group name.
1580 """
showard3f15eed2008-11-14 22:40:48 +00001581 if self.host:
1582 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001583 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +00001584 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +00001585 else:
1586 assert self.atomic_group, "no host, meta_host or atomic group!"
1587 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +00001588
1589
showard2bab8f42008-11-12 18:15:22 +00001590 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001591 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001592 self.active, self.complete = True, False
1593 elif self.status in self.COMPLETE_STATUSES:
1594 self.active, self.complete = False, True
1595 else:
1596 self.active, self.complete = False, False
1597
1598
1599 def on_attribute_changed(self, attribute, old_value):
1600 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001601 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1602 self.status)
showard2bab8f42008-11-12 18:15:22 +00001603
1604
jadmanski0afbb632008-06-06 21:10:57 +00001605 def is_meta_host_entry(self):
1606 'True if this is a entry has a meta_host instead of a host.'
1607 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001608
showarda3ab0d52008-11-03 19:03:47 +00001609
Simran Basic1b26762013-06-26 14:23:21 -07001610 # This code is shared between rpc_interface and models.HostQueueEntry.
1611 # Sadly due to circular imports between the 2 (crbug.com/230100) making it
1612 # a class method was the best way to refactor it. Attempting to put it in
1613 # rpc_utils or a new utils module failed as that would require us to import
1614 # models.py but to call it from here we would have to import the utils.py
1615 # thus creating a cycle.
1616 @classmethod
1617 def abort_host_queue_entries(cls, host_queue_entries):
1618 """Aborts a collection of host_queue_entries.
1619
1620 Abort these host queue entry and all host queue entries of jobs created
1621 by them.
1622
1623 @param host_queue_entries: List of host queue entries we want to abort.
1624 """
1625 # This isn't completely immune to race conditions since it's not atomic,
1626 # but it should be safe given the scheduler's behavior.
1627
1628 # TODO(milleral): crbug.com/230100
1629 # The |abort_host_queue_entries| rpc does nearly exactly this,
1630 # however, trying to re-use the code generates some horrible
1631 # circular import error. I'd be nice to refactor things around
1632 # sometime so the code could be reused.
1633
1634 # Fixpoint algorithm to find the whole tree of HQEs to abort to
1635 # minimize the total number of database queries:
1636 children = set()
1637 new_children = set(host_queue_entries)
1638 while new_children:
1639 children.update(new_children)
1640 new_child_ids = [hqe.job_id for hqe in new_children]
1641 new_children = HostQueueEntry.objects.filter(
1642 job__parent_job__in=new_child_ids,
1643 complete=False, aborted=False).all()
1644 # To handle circular parental relationships
1645 new_children = set(new_children) - children
1646
1647 # Associate a user with the host queue entries that we're about
1648 # to abort so that we can look up who to blame for the aborts.
1649 now = datetime.now()
1650 user = User.current_user()
1651 aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
1652 aborted_by=user, aborted_on=now) for hqe in children]
1653 AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
1654 # Bulk update all of the HQEs to set the abort bit.
1655 child_ids = [hqe.id for hqe in children]
1656 HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
1657
1658
Scott Zawalski23041432013-04-17 07:39:09 -07001659 def abort(self):
Alex Millerdea67042013-04-22 17:23:34 -07001660 """ Aborts this host queue entry.
Simran Basic1b26762013-06-26 14:23:21 -07001661
Alex Millerdea67042013-04-22 17:23:34 -07001662 Abort this host queue entry and all host queue entries of jobs created by
1663 this one.
1664
1665 """
showardd3dc1992009-04-22 21:01:40 +00001666 if not self.complete and not self.aborted:
Simran Basi97582a22013-06-27 12:03:21 -07001667 HostQueueEntry.abort_host_queue_entries([self])
mblighe8819cd2008-02-15 16:48:40 +00001668
showardd3dc1992009-04-22 21:01:40 +00001669
1670 @classmethod
1671 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001672 """Returns a modified status msg if the host queue entry was aborted.
1673
1674 @param cls: Implicit class object.
1675 @param status: The original status message.
1676 @param aborted: Whether the host queue entry was aborted.
1677 @param complete: Whether the host queue entry was completed.
1678 """
showardd3dc1992009-04-22 21:01:40 +00001679 if aborted and not complete:
1680 return 'Aborted (%s)' % status
1681 return status
1682
1683
1684 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001685 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00001686 return self.compute_full_status(self.status, self.aborted,
1687 self.complete)
1688
1689
1690 def _postprocess_object_dict(self, object_dict):
1691 object_dict['full_status'] = self.full_status()
1692
1693
jadmanski0afbb632008-06-06 21:10:57 +00001694 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001695 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001696 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00001697
showard12f3e322009-05-13 21:27:42 +00001698
showard4c119042008-09-29 19:16:18 +00001699
showarda5288b42009-07-28 20:06:08 +00001700 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00001701 hostname = None
1702 if self.host:
1703 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00001704 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00001705
1706
showard4c119042008-09-29 19:16:18 +00001707class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001708 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00001709 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
1710 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00001711 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00001712
1713 objects = model_logic.ExtendedManager()
1714
showard68c7aa02008-10-09 16:49:11 +00001715
showarda5288b42009-07-28 20:06:08 +00001716 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00001717 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00001718 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00001719
showard4c119042008-09-29 19:16:18 +00001720 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001721 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001722 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00001723
1724
1725class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
1726 """\
1727 job: job to use as a template
1728 owner: owner of the instantiated template
1729 start_date: Run the job at scheduled date
1730 loop_period: Re-run (loop) the job periodically
1731 (in every loop_period seconds)
1732 loop_count: Re-run (loop) count
1733 """
1734
1735 job = dbmodels.ForeignKey(Job)
1736 owner = dbmodels.ForeignKey(User)
1737 start_date = dbmodels.DateTimeField()
1738 loop_period = dbmodels.IntegerField(blank=True)
1739 loop_count = dbmodels.IntegerField(blank=True)
1740
1741 objects = model_logic.ExtendedManager()
1742
1743 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001744 """Metadata for class RecurringRun."""
showardeab66ce2009-12-23 00:03:56 +00001745 db_table = 'afe_recurring_run'
showard29f7cd22009-04-29 21:16:24 +00001746
showarda5288b42009-07-28 20:06:08 +00001747 def __unicode__(self):
1748 return u'RecurringRun(job %s, start %s, period %s, count %s)' % (
showard29f7cd22009-04-29 21:16:24 +00001749 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +00001750
1751
1752class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
1753 """\
1754 Tasks to run on hosts at the next time they are in the Ready state. Use this
1755 for high-priority tasks, such as forced repair or forced reinstall.
1756
1757 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00001758 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00001759 time_requested: date and time the request for this task was made
1760 is_active: task is currently running
1761 is_complete: task has finished running
beeps8bb1f7d2013-08-05 01:30:09 -07001762 is_aborted: task was aborted
showard2fe3f1d2009-07-06 20:19:11 +00001763 time_started: date and time the task started
Dan Shid0725542014-06-23 15:34:27 -07001764 time_finished: date and time the task finished
showard2fe3f1d2009-07-06 20:19:11 +00001765 queue_entry: Host queue entry waiting on this task (or None, if task was not
1766 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00001767 """
Alex Millerdfff2fd2013-05-28 13:05:06 -07001768 Task = enum.Enum('Verify', 'Cleanup', 'Repair', 'Reset', 'Provision',
Dan Shi07e09af2013-04-12 09:31:29 -07001769 string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00001770
1771 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00001772 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00001773 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00001774 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00001775 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
1776 null=False)
1777 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
1778 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
beeps8bb1f7d2013-08-05 01:30:09 -07001779 is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00001780 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00001781 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00001782 success = dbmodels.BooleanField(default=False, blank=False, null=False)
Dan Shid0725542014-06-23 15:34:27 -07001783 time_finished = dbmodels.DateTimeField(null=True, blank=True)
showard6d7b2ff2009-06-10 00:16:47 +00001784
1785 objects = model_logic.ExtendedManager()
1786
1787
showard9bb960b2009-11-19 01:02:11 +00001788 def save(self, **kwargs):
1789 if self.queue_entry:
1790 self.requested_by = User.objects.get(
1791 login=self.queue_entry.job.owner)
1792 super(SpecialTask, self).save(**kwargs)
1793
1794
showarded2afea2009-07-07 20:54:07 +00001795 def execution_path(self):
Prashanth Balasubramaniande87dea2014-11-09 17:47:10 -08001796 """Get the execution path of the SpecialTask.
1797
1798 This method returns different paths depending on where a
1799 the task ran:
1800 * Master: hosts/hostname/task_id-task_type
1801 * Shard: Master_path/time_created
1802 This is to work around the fact that a shard can fail independent
1803 of the master, and be replaced by another shard that has the same
1804 hosts. Without the time_created stamp the logs of the tasks running
1805 on the second shard will clobber the logs from the first in google
1806 storage, because task ids are not globally unique.
1807
1808 @return: An execution path for the task.
1809 """
1810 results_path = 'hosts/%s/%s-%s' % (self.host.hostname, self.id,
1811 self.task.lower())
1812
1813 # If we do this on the master it will break backward compatibility,
1814 # as there are tasks that currently don't have timestamps. If a host
1815 # or job has been sent to a shard, the rpc for that host/job will
1816 # be redirected to the shard, so this global_config check will happen
1817 # on the shard the logs are on.
1818 is_shard = global_config.global_config.get_config_value(
1819 'SHARD', 'shard_hostname', type=str, default='')
1820 if not is_shard:
1821 return results_path
1822
1823 # Generate a uid to disambiguate special task result directories
1824 # in case this shard fails. The simplest uid is the job_id, however
1825 # in rare cases tasks do not have jobs associated with them (eg:
1826 # frontend verify), so just use the creation timestamp. The clocks
1827 # between a shard and master should always be in sync. Any discrepancies
1828 # will be brought to our attention in the form of job timeouts.
1829 uid = self.time_requested.strftime('%Y%d%m%H%M%S')
1830
1831 # TODO: This is a hack, however it is the easiest way to achieve
1832 # correctness. There is currently some debate over the future of
1833 # tasks in our infrastructure and refactoring everything right
1834 # now isn't worth the time.
1835 return '%s/%s' % (results_path, uid)
showarded2afea2009-07-07 20:54:07 +00001836
1837
showardc0ac3a72009-07-08 21:14:45 +00001838 # property to emulate HostQueueEntry.status
1839 @property
1840 def status(self):
1841 """
1842 Return a host queue entry status appropriate for this task. Although
1843 SpecialTasks are not HostQueueEntries, it is helpful to the user to
1844 present similar statuses.
1845 """
1846 if self.is_complete:
showarde60e44e2009-11-13 20:45:38 +00001847 if self.success:
1848 return HostQueueEntry.Status.COMPLETED
1849 return HostQueueEntry.Status.FAILED
showardc0ac3a72009-07-08 21:14:45 +00001850 if self.is_active:
1851 return HostQueueEntry.Status.RUNNING
1852 return HostQueueEntry.Status.QUEUED
1853
1854
1855 # property to emulate HostQueueEntry.started_on
1856 @property
1857 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001858 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00001859 return self.time_started
1860
1861
showard6d7b2ff2009-06-10 00:16:47 +00001862 @classmethod
showardc5103442010-01-15 00:20:26 +00001863 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001864 """Schedules a special task on a host if not already scheduled.
1865
1866 @param cls: Implicit class object.
1867 @param host: The host to use.
1868 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00001869 """
showardc5103442010-01-15 00:20:26 +00001870 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
1871 is_active=False,
1872 is_complete=False)
1873 if existing_tasks:
1874 return existing_tasks[0]
1875
1876 special_task = SpecialTask(host=host, task=task,
1877 requested_by=User.current_user())
1878 special_task.save()
1879 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00001880
1881
beeps8bb1f7d2013-08-05 01:30:09 -07001882 def abort(self):
1883 """ Abort this special task."""
1884 self.is_aborted = True
1885 self.save()
1886
1887
showarded2afea2009-07-07 20:54:07 +00001888 def activate(self):
showard474d1362009-08-20 23:32:01 +00001889 """
1890 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00001891 """
showard97446882009-07-20 22:37:28 +00001892 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00001893 self.is_active = True
1894 self.time_started = datetime.now()
1895 self.save()
1896
1897
showarde60e44e2009-11-13 20:45:38 +00001898 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001899 """Sets a task as completed.
1900
1901 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00001902 """
showard97446882009-07-20 22:37:28 +00001903 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00001904 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00001905 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00001906 self.success = success
Dan Shid85d6112014-07-14 10:32:55 -07001907 if self.time_started:
1908 self.time_finished = datetime.now()
showard2fe3f1d2009-07-06 20:19:11 +00001909 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00001910
1911
1912 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001913 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00001914 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00001915
showard474d1362009-08-20 23:32:01 +00001916
showarda5288b42009-07-28 20:06:08 +00001917 def __unicode__(self):
1918 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00001919 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00001920 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00001921 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00001922 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00001923 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00001924
1925 return result