blob: 0d8b0ea16d6d57eb1ac01353a499e1b6804f1288 [file] [log] [blame]
Richard Barnetteb28def32017-03-07 13:23:08 -08001# pylint: disable=missing-docstring
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08002
Fang Deng2b79fd72015-05-20 19:11:36 -07003import logging
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
MK Ryu0c1a37d2015-04-30 12:00:55 -070023from autotest_lib.server import utils as server_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
Xixuan Wubc3e0c82017-12-18 13:39:10 -080029RESPECT_STATIC_LABELS = global_config.global_config.get_config_value(
30 'SKYLAB', 'respect_static_labels', type=bool, default=False)
31
Xixuan Wuc00f6032018-02-22 12:41:41 -080032RESPECT_STATIC_ATTRIBUTES = global_config.global_config.get_config_value(
33 'SKYLAB', 'respect_static_attributes', type=bool, default=False)
34
showard89f84db2009-03-12 20:39:13 +000035
mblighe8819cd2008-02-15 16:48:40 +000036class AclAccessViolation(Exception):
jadmanski0afbb632008-06-06 21:10:57 +000037 """\
38 Raised when an operation is attempted with proper permissions as
39 dictated by ACLs.
40 """
mblighe8819cd2008-02-15 16:48:40 +000041
42
showard205fd602009-03-21 00:17:35 +000043class AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model):
showard89f84db2009-03-12 20:39:13 +000044 """\
45 An atomic group defines a collection of hosts which must only be scheduled
46 all at once. Any host with a label having an atomic group will only be
47 scheduled for a job at the same time as other hosts sharing that label.
48
49 Required:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080050 name: A name for this atomic group, e.g. 'rack23' or 'funky_net'.
showard89f84db2009-03-12 20:39:13 +000051 max_number_of_machines: The maximum number of machines that will be
52 scheduled at once when scheduling jobs to this atomic group.
53 The job.synch_count is considered the minimum.
54
55 Optional:
56 description: Arbitrary text description of this group's purpose.
57 """
showarda5288b42009-07-28 20:06:08 +000058 name = dbmodels.CharField(max_length=255, unique=True)
showard89f84db2009-03-12 20:39:13 +000059 description = dbmodels.TextField(blank=True)
showarde9450c92009-06-30 01:58:52 +000060 # This magic value is the default to simplify the scheduler logic.
61 # It must be "large". The common use of atomic groups is to want all
62 # machines in the group to be used, limits on which subset used are
63 # often chosen via dependency labels.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080064 # TODO(dennisjeffrey): Revisit this so we don't have to assume that
65 # "infinity" is around 3.3 million.
showarde9450c92009-06-30 01:58:52 +000066 INFINITE_MACHINES = 333333333
67 max_number_of_machines = dbmodels.IntegerField(default=INFINITE_MACHINES)
showard205fd602009-03-21 00:17:35 +000068 invalid = dbmodels.BooleanField(default=False,
showarda5288b42009-07-28 20:06:08 +000069 editable=settings.FULL_ADMIN)
showard89f84db2009-03-12 20:39:13 +000070
showard89f84db2009-03-12 20:39:13 +000071 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +000072 objects = model_logic.ModelWithInvalidManager()
showard205fd602009-03-21 00:17:35 +000073 valid_objects = model_logic.ValidObjectsManager()
74
75
showard29f7cd22009-04-29 21:16:24 +000076 def enqueue_job(self, job, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080077 """Enqueue a job on an associated atomic group of hosts.
78
79 @param job: A job to enqueue.
80 @param is_template: Whether the status should be "Template".
81 """
showard29f7cd22009-04-29 21:16:24 +000082 queue_entry = HostQueueEntry.create(atomic_group=self, job=job,
83 is_template=is_template)
showardc92da832009-04-07 18:14:34 +000084 queue_entry.save()
85
86
showard205fd602009-03-21 00:17:35 +000087 def clean_object(self):
88 self.label_set.clear()
showard89f84db2009-03-12 20:39:13 +000089
90
91 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080092 """Metadata for class AtomicGroup."""
showardeab66ce2009-12-23 00:03:56 +000093 db_table = 'afe_atomic_groups'
showard89f84db2009-03-12 20:39:13 +000094
showard205fd602009-03-21 00:17:35 +000095
showarda5288b42009-07-28 20:06:08 +000096 def __unicode__(self):
97 return unicode(self.name)
showard89f84db2009-03-12 20:39:13 +000098
99
showard7c785282008-05-29 19:45:12 +0000100class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +0000101 """\
102 Required:
showard89f84db2009-03-12 20:39:13 +0000103 name: label name
mblighe8819cd2008-02-15 16:48:40 +0000104
jadmanski0afbb632008-06-06 21:10:57 +0000105 Optional:
showard89f84db2009-03-12 20:39:13 +0000106 kernel_config: URL/path to kernel config for jobs run on this label.
107 platform: If True, this is a platform label (defaults to False).
108 only_if_needed: If True, a Host with this label can only be used if that
109 label is requested by the job/test (either as the meta_host or
110 in the job_dependencies).
111 atomic_group: The atomic group associated with this label.
jadmanski0afbb632008-06-06 21:10:57 +0000112 """
showarda5288b42009-07-28 20:06:08 +0000113 name = dbmodels.CharField(max_length=255, unique=True)
114 kernel_config = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000115 platform = dbmodels.BooleanField(default=False)
116 invalid = dbmodels.BooleanField(default=False,
117 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +0000118 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +0000119
jadmanski0afbb632008-06-06 21:10:57 +0000120 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +0000121 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000122 valid_objects = model_logic.ValidObjectsManager()
showard89f84db2009-03-12 20:39:13 +0000123 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
124
mbligh5244cbb2008-04-24 20:39:52 +0000125
jadmanski0afbb632008-06-06 21:10:57 +0000126 def clean_object(self):
127 self.host_set.clear()
showard01a51672009-05-29 18:42:37 +0000128 self.test_set.clear()
mblighe8819cd2008-02-15 16:48:40 +0000129
130
Allen Li204eac12017-01-30 18:31:26 -0800131 def enqueue_job(self, job, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800132 """Enqueue a job on any host of this label.
133
134 @param job: A job to enqueue.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800135 @param is_template: Whether the status should be "Template".
136 """
showard29f7cd22009-04-29 21:16:24 +0000137 queue_entry = HostQueueEntry.create(meta_host=self, job=job,
Allen Li204eac12017-01-30 18:31:26 -0800138 is_template=is_template)
jadmanski0afbb632008-06-06 21:10:57 +0000139 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000140
141
Fang Dengff361592015-02-02 15:27:34 -0800142
jadmanski0afbb632008-06-06 21:10:57 +0000143 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800144 """Metadata for class Label."""
showardeab66ce2009-12-23 00:03:56 +0000145 db_table = 'afe_labels'
mblighe8819cd2008-02-15 16:48:40 +0000146
Fang Dengff361592015-02-02 15:27:34 -0800147
showarda5288b42009-07-28 20:06:08 +0000148 def __unicode__(self):
149 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000150
151
Xixuan Wu040fc8b2018-01-12 12:52:14 -0800152 def is_replaced_by_static(self):
153 """Detect whether a label is replaced by a static label.
154
155 'Static' means it can only be modified by skylab inventory tools.
156 """
157 if RESPECT_STATIC_LABELS:
158 replaced = ReplacedLabel.objects.filter(label__id=self.id)
159 if len(replaced) > 0:
160 return True
161
162 return False
163
164
Xixuan Wucc12f3e2018-01-02 15:50:14 -0800165class StaticLabel(model_logic.ModelWithInvalid, dbmodels.Model):
166 """\
167 Required:
168 name: label name
169
170 Optional:
171 kernel_config: URL/path to kernel config for jobs run on this label.
172 platform: If True, this is a platform label (defaults to False).
173 only_if_needed: Deprecated. This is always False.
174 atomic_group: Deprecated. This is always NULL.
175 """
176 name = dbmodels.CharField(max_length=255, unique=True)
177 kernel_config = dbmodels.CharField(max_length=255, blank=True)
178 platform = dbmodels.BooleanField(default=False)
179 invalid = dbmodels.BooleanField(default=False,
180 editable=settings.FULL_ADMIN)
181 only_if_needed = dbmodels.BooleanField(default=False)
182
183 name_field = 'name'
184 objects = model_logic.ModelWithInvalidManager()
185 valid_objects = model_logic.ValidObjectsManager()
186 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
187
188 def clean_object(self):
189 self.host_set.clear()
190 self.test_set.clear()
191
192
193 class Meta:
194 """Metadata for class StaticLabel."""
195 db_table = 'afe_static_labels'
196
197
198 def __unicode__(self):
199 return unicode(self.name)
200
201
202class ReplacedLabel(dbmodels.Model, model_logic.ModelExtensions):
203 """The tag to indicate Whether to replace labels with static labels."""
204 label = dbmodels.ForeignKey(Label)
205 objects = model_logic.ExtendedManager()
206
207
208 class Meta:
209 """Metadata for class ReplacedLabel."""
210 db_table = 'afe_replaced_labels'
211
212
213 def __unicode__(self):
Xixuan Wu76cd0582017-12-21 13:23:28 -0800214 return unicode(self.label)
Xixuan Wucc12f3e2018-01-02 15:50:14 -0800215
216
Jakob Jülich92c06332014-08-25 19:06:57 +0000217class Shard(dbmodels.Model, model_logic.ModelExtensions):
218
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700219 hostname = dbmodels.CharField(max_length=255, unique=True)
220
221 name_field = 'hostname'
222
Jakob Jülich92c06332014-08-25 19:06:57 +0000223 labels = dbmodels.ManyToManyField(Label, blank=True,
224 db_table='afe_shards_labels')
225
226 class Meta:
227 """Metadata for class ParameterizedJob."""
228 db_table = 'afe_shards'
229
230
jamesren76fcf192010-04-21 20:39:50 +0000231class Drone(dbmodels.Model, model_logic.ModelExtensions):
232 """
233 A scheduler drone
234
235 hostname: the drone's hostname
236 """
237 hostname = dbmodels.CharField(max_length=255, unique=True)
238
239 name_field = 'hostname'
240 objects = model_logic.ExtendedManager()
241
242
243 def save(self, *args, **kwargs):
244 if not User.current_user().is_superuser():
245 raise Exception('Only superusers may edit drones')
246 super(Drone, self).save(*args, **kwargs)
247
248
249 def delete(self):
250 if not User.current_user().is_superuser():
251 raise Exception('Only superusers may delete drones')
252 super(Drone, self).delete()
253
254
255 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800256 """Metadata for class Drone."""
jamesren76fcf192010-04-21 20:39:50 +0000257 db_table = 'afe_drones'
258
259 def __unicode__(self):
260 return unicode(self.hostname)
261
262
263class DroneSet(dbmodels.Model, model_logic.ModelExtensions):
264 """
265 A set of scheduler drones
266
267 These will be used by the scheduler to decide what drones a job is allowed
268 to run on.
269
270 name: the drone set's name
271 drones: the drones that are part of the set
272 """
273 DRONE_SETS_ENABLED = global_config.global_config.get_config_value(
274 'SCHEDULER', 'drone_sets_enabled', type=bool, default=False)
275 DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value(
276 'SCHEDULER', 'default_drone_set_name', default=None)
277
278 name = dbmodels.CharField(max_length=255, unique=True)
279 drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones')
280
281 name_field = 'name'
282 objects = model_logic.ExtendedManager()
283
284
285 def save(self, *args, **kwargs):
286 if not User.current_user().is_superuser():
287 raise Exception('Only superusers may edit drone sets')
288 super(DroneSet, self).save(*args, **kwargs)
289
290
291 def delete(self):
292 if not User.current_user().is_superuser():
293 raise Exception('Only superusers may delete drone sets')
294 super(DroneSet, self).delete()
295
296
297 @classmethod
298 def drone_sets_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800299 """Returns whether drone sets are enabled.
300
301 @param cls: Implicit class object.
302 """
jamesren76fcf192010-04-21 20:39:50 +0000303 return cls.DRONE_SETS_ENABLED
304
305
306 @classmethod
307 def default_drone_set_name(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800308 """Returns the default drone set name.
309
310 @param cls: Implicit class object.
311 """
jamesren76fcf192010-04-21 20:39:50 +0000312 return cls.DEFAULT_DRONE_SET_NAME
313
314
315 @classmethod
316 def get_default(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800317 """Gets the default drone set name, compatible with Job.add_object.
318
319 @param cls: Implicit class object.
320 """
jamesren76fcf192010-04-21 20:39:50 +0000321 return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME)
322
323
324 @classmethod
325 def resolve_name(cls, drone_set_name):
326 """
327 Returns the name of one of these, if not None, in order of preference:
328 1) the drone set given,
329 2) the current user's default drone set, or
330 3) the global default drone set
331
332 or returns None if drone sets are disabled
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800333
334 @param cls: Implicit class object.
335 @param drone_set_name: A drone set name.
jamesren76fcf192010-04-21 20:39:50 +0000336 """
337 if not cls.drone_sets_enabled():
338 return None
339
340 user = User.current_user()
341 user_drone_set_name = user.drone_set and user.drone_set.name
342
343 return drone_set_name or user_drone_set_name or cls.get_default().name
344
345
346 def get_drone_hostnames(self):
347 """
348 Gets the hostnames of all drones in this drone set
349 """
350 return set(self.drones.all().values_list('hostname', flat=True))
351
352
353 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800354 """Metadata for class DroneSet."""
jamesren76fcf192010-04-21 20:39:50 +0000355 db_table = 'afe_drone_sets'
356
357 def __unicode__(self):
358 return unicode(self.name)
359
360
showardfb2a7fa2008-07-17 17:04:12 +0000361class User(dbmodels.Model, model_logic.ModelExtensions):
362 """\
363 Required:
364 login :user login name
365
366 Optional:
367 access_level: 0=User (default), 1=Admin, 100=Root
368 """
369 ACCESS_ROOT = 100
370 ACCESS_ADMIN = 1
371 ACCESS_USER = 0
372
showard64a95952010-01-13 21:27:16 +0000373 AUTOTEST_SYSTEM = 'autotest_system'
374
showarda5288b42009-07-28 20:06:08 +0000375 login = dbmodels.CharField(max_length=255, unique=True)
showardfb2a7fa2008-07-17 17:04:12 +0000376 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
377
showard0fc38302008-10-23 00:44:07 +0000378 # user preferences
jamesrendd855242010-03-02 22:23:44 +0000379 reboot_before = dbmodels.SmallIntegerField(
380 choices=model_attributes.RebootBefore.choices(), blank=True,
381 default=DEFAULT_REBOOT_BEFORE)
382 reboot_after = dbmodels.SmallIntegerField(
383 choices=model_attributes.RebootAfter.choices(), blank=True,
384 default=DEFAULT_REBOOT_AFTER)
jamesren76fcf192010-04-21 20:39:50 +0000385 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
showard97db5ba2008-11-12 18:18:02 +0000386 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000387
showardfb2a7fa2008-07-17 17:04:12 +0000388 name_field = 'login'
389 objects = model_logic.ExtendedManager()
390
391
showarda5288b42009-07-28 20:06:08 +0000392 def save(self, *args, **kwargs):
showardfb2a7fa2008-07-17 17:04:12 +0000393 # is this a new object being saved for the first time?
394 first_time = (self.id is None)
395 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000396 if user and not user.is_superuser() and user.login != self.login:
397 raise AclAccessViolation("You cannot modify user " + self.login)
showarda5288b42009-07-28 20:06:08 +0000398 super(User, self).save(*args, **kwargs)
showardfb2a7fa2008-07-17 17:04:12 +0000399 if first_time:
400 everyone = AclGroup.objects.get(name='Everyone')
401 everyone.users.add(self)
402
403
404 def is_superuser(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800405 """Returns whether the user has superuser access."""
showardfb2a7fa2008-07-17 17:04:12 +0000406 return self.access_level >= self.ACCESS_ROOT
407
408
showard64a95952010-01-13 21:27:16 +0000409 @classmethod
410 def current_user(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800411 """Returns the current user.
412
413 @param cls: Implicit class object.
414 """
showard64a95952010-01-13 21:27:16 +0000415 user = thread_local.get_user()
416 if user is None:
showardcfcdd802010-01-15 00:16:33 +0000417 user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM)
showard64a95952010-01-13 21:27:16 +0000418 user.access_level = cls.ACCESS_ROOT
419 user.save()
420 return user
421
422
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -0800423 @classmethod
424 def get_record(cls, data):
425 """Check the database for an identical record.
426
427 Check for a record with matching id and login. If one exists,
428 return it. If one does not exist there is a possibility that
429 the following cases have happened:
430 1. Same id, different login
431 We received: "1 chromeos-test"
432 And we have: "1 debug-user"
433 In this case we need to delete "1 debug_user" and insert
434 "1 chromeos-test".
435
436 2. Same login, different id:
437 We received: "1 chromeos-test"
438 And we have: "2 chromeos-test"
439 In this case we need to delete "2 chromeos-test" and insert
440 "1 chromeos-test".
441
442 As long as this method deletes bad records and raises the
443 DoesNotExist exception the caller will handle creating the
444 new record.
445
446 @raises: DoesNotExist, if a record with the matching login and id
447 does not exist.
448 """
449
450 # Both the id and login should be uniqe but there are cases when
451 # we might already have a user with the same login/id because
452 # current_user will proactively create a user record if it doesn't
453 # exist. Since we want to avoid conflict between the master and
454 # shard, just delete any existing user records that don't match
455 # what we're about to deserialize from the master.
456 try:
457 return cls.objects.get(login=data['login'], id=data['id'])
458 except cls.DoesNotExist:
459 cls.delete_matching_record(login=data['login'])
460 cls.delete_matching_record(id=data['id'])
461 raise
462
463
showardfb2a7fa2008-07-17 17:04:12 +0000464 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800465 """Metadata for class User."""
showardeab66ce2009-12-23 00:03:56 +0000466 db_table = 'afe_users'
showardfb2a7fa2008-07-17 17:04:12 +0000467
showarda5288b42009-07-28 20:06:08 +0000468 def __unicode__(self):
469 return unicode(self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000470
471
Prashanth B489b91d2014-03-15 12:17:16 -0700472class Host(model_logic.ModelWithInvalid, rdb_model_extensions.AbstractHostModel,
showardf8b19042009-05-12 17:22:49 +0000473 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000474 """\
475 Required:
476 hostname
mblighe8819cd2008-02-15 16:48:40 +0000477
jadmanski0afbb632008-06-06 21:10:57 +0000478 optional:
showard21baa452008-10-21 00:08:39 +0000479 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000480
jadmanski0afbb632008-06-06 21:10:57 +0000481 Internal:
Prashanth B489b91d2014-03-15 12:17:16 -0700482 From AbstractHostModel:
Prashanth B489b91d2014-03-15 12:17:16 -0700483 status: string describing status of host
484 invalid: true if the host has been deleted
485 protection: indicates what can be done to this host during repair
486 lock_time: DateTime at which the host was locked
487 dirty: true if the host has been used without being rebooted
488 Local:
489 locked_by: user that locked the host, or null if the host is unlocked
jadmanski0afbb632008-06-06 21:10:57 +0000490 """
mblighe8819cd2008-02-15 16:48:40 +0000491
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700492 SERIALIZATION_LINKS_TO_FOLLOW = set(['aclgroup_set',
493 'hostattribute_set',
494 'labels',
495 'shard'])
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800496 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['invalid'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700497
Jakob Juelichf88fa932014-09-03 17:58:04 -0700498
499 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700500 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -0700501 self.shard = Shard.deserialize(data)
502
503
Prashanth B489b91d2014-03-15 12:17:16 -0700504 # Note: Only specify foreign keys here, specify all native host columns in
505 # rdb_model_extensions instead.
506 Protection = host_protections.Protection
showardeab66ce2009-12-23 00:03:56 +0000507 labels = dbmodels.ManyToManyField(Label, blank=True,
508 db_table='afe_hosts_labels')
Xixuan Wucc12f3e2018-01-02 15:50:14 -0800509 static_labels = dbmodels.ManyToManyField(
510 StaticLabel, blank=True, db_table='afe_static_hosts_labels')
showardfb2a7fa2008-07-17 17:04:12 +0000511 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
jadmanski0afbb632008-06-06 21:10:57 +0000512 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000513 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000514 valid_objects = model_logic.ValidObjectsManager()
beepscc9fc702013-12-02 12:45:38 -0800515 leased_objects = model_logic.LeasedHostManager()
mbligh5244cbb2008-04-24 20:39:52 +0000516
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700517 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
showard2bab8f42008-11-12 18:15:22 +0000518
519 def __init__(self, *args, **kwargs):
520 super(Host, self).__init__(*args, **kwargs)
521 self._record_attributes(['status'])
522
523
Xixuan Wubc3e0c82017-12-18 13:39:10 -0800524 @classmethod
Xixuan Wu8c7113a2018-01-09 17:14:38 -0800525 def classify_labels(cls, label_names):
Xixuan Wubc3e0c82017-12-18 13:39:10 -0800526 """Split labels to static & non-static.
527
Xixuan Wu8c7113a2018-01-09 17:14:38 -0800528 @label_names: a list of labels (string).
Xixuan Wubc3e0c82017-12-18 13:39:10 -0800529
530 @returns: a list of StaticLabel objects & a list of
531 (non-static) Label objects.
532 """
Xixuan Wu8c7113a2018-01-09 17:14:38 -0800533 if not label_names:
Xixuan Wubc3e0c82017-12-18 13:39:10 -0800534 return [], []
535
Xixuan Wu8c7113a2018-01-09 17:14:38 -0800536 labels = Label.objects.filter(name__in=label_names)
Xixuan Wu9d9549f2018-01-12 16:28:31 -0800537
Xixuan Wubc3e0c82017-12-18 13:39:10 -0800538 if not RESPECT_STATIC_LABELS:
539 return [], labels
540
Xixuan Wue7706032017-12-21 15:57:41 -0800541 return cls.classify_label_objects(labels)
542
543
544 @classmethod
545 def classify_label_objects(cls, label_objects):
546 replaced_labels = ReplacedLabel.objects.filter(label__in=label_objects)
Xixuan Wubc3e0c82017-12-18 13:39:10 -0800547 replaced_ids = [l.label.id for l in replaced_labels]
548 non_static_labels = [
Xixuan Wue7706032017-12-21 15:57:41 -0800549 l for l in label_objects if not l.id in replaced_ids]
Xixuan Wubc3e0c82017-12-18 13:39:10 -0800550 static_label_names = [
Xixuan Wue7706032017-12-21 15:57:41 -0800551 l.name for l in label_objects if l.id in replaced_ids]
Xixuan Wubc3e0c82017-12-18 13:39:10 -0800552 static_labels = StaticLabel.objects.filter(name__in=static_label_names)
553 return static_labels, non_static_labels
554
555
556 @classmethod
Xixuan Wu8c7113a2018-01-09 17:14:38 -0800557 def get_hosts_with_labels(cls, label_names, initial_query):
Xixuan Wubc3e0c82017-12-18 13:39:10 -0800558 """Get hosts by label filters.
559
Xixuan Wu8c7113a2018-01-09 17:14:38 -0800560 @param label_names: label (string) lists for fetching hosts.
Xixuan Wu9d9549f2018-01-12 16:28:31 -0800561 @param initial_query: a model_logic.QuerySet of Host object, e.g.
562
563 Host.objects.all(), Host.valid_objects.all().
564
565 This initial_query cannot be a sliced QuerySet, e.g.
566
567 Host.objects.all().filter(query_limit=10)
Xixuan Wubc3e0c82017-12-18 13:39:10 -0800568 """
Xixuan Wu8c7113a2018-01-09 17:14:38 -0800569 if not label_names:
Xixuan Wu9d9549f2018-01-12 16:28:31 -0800570 return initial_query
Xixuan Wubc3e0c82017-12-18 13:39:10 -0800571
Xixuan Wu8c7113a2018-01-09 17:14:38 -0800572 static_labels, non_static_labels = cls.classify_labels(label_names)
573 if len(static_labels) + len(non_static_labels) != len(label_names):
Xixuan Wu9d9549f2018-01-12 16:28:31 -0800574 # Some labels don't exist in afe db, which means no hosts
575 # should be matched.
576 return set()
Xixuan Wubc3e0c82017-12-18 13:39:10 -0800577
578 for l in static_labels:
579 initial_query = initial_query.filter(static_labels=l)
580
581 for l in non_static_labels:
582 initial_query = initial_query.filter(labels=l)
583
584 return initial_query
585
586
Xixuan Wu8c7113a2018-01-09 17:14:38 -0800587 @classmethod
588 def get_hosts_with_label_ids(cls, label_ids, initial_query):
589 """Get hosts by label_id filters.
590
591 @param label_ids: label id (int) lists for fetching hosts.
592 @param initial_query: a list of Host object, e.g.
593 [<Host: 100.107.151.253>, <Host: 100.107.151.251>, ...]
594 """
595 labels = Label.objects.filter(id__in=label_ids)
596 label_names = [l.name for l in labels]
597 return cls.get_hosts_with_labels(label_names, initial_query)
598
599
showardb8471e32008-07-03 19:51:08 +0000600 @staticmethod
601 def create_one_time_host(hostname):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800602 """Creates a one-time host.
603
604 @param hostname: The name for the host.
605 """
showardb8471e32008-07-03 19:51:08 +0000606 query = Host.objects.filter(hostname=hostname)
607 if query.count() == 0:
608 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000609 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000610 else:
611 host = query[0]
612 if not host.invalid:
613 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000614 'hostname' : '%s already exists in the autotest DB. '
615 'Select it rather than entering it as a one time '
616 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000617 })
showard1ab512b2008-07-30 23:39:04 +0000618 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000619 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000620 host.save()
showard2924b0a2009-06-18 23:16:15 +0000621 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000622 return host
mbligh5244cbb2008-04-24 20:39:52 +0000623
showard1ff7b2e2009-05-15 23:17:18 +0000624
Jakob Juelich59cfe542014-09-02 16:37:46 -0700625 @classmethod
Aviv Keshetbf963c12017-04-04 10:41:55 -0700626 def _assign_to_shard_nothing_helper(cls):
627 """Does nothing.
628
629 This method is called in the middle of assign_to_shard, and does
630 nothing. It exists to allow integration tests to simulate a race
631 condition."""
632
633
634 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -0700635 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -0700636 """Assigns hosts to a shard.
637
MK Ryu638a6cf2015-05-08 14:49:43 -0700638 For all labels that have been assigned to a shard, all hosts that
639 have at least one of the shard's labels are assigned to the shard.
Jakob Juelich1b525742014-09-30 13:08:07 -0700640 Hosts that are assigned to the shard but aren't already present on the
641 shard are returned.
Jakob Juelich59cfe542014-09-02 16:37:46 -0700642
Aviv Keshet4be5a162017-03-23 08:59:06 -0700643 Any boards that are in |known_ids| but that do not belong to the shard
644 are incorrect ids, which are also returned so that the shard can remove
645 them locally.
646
MK Ryu638a6cf2015-05-08 14:49:43 -0700647 Board to shard mapping is many-to-one. Many different boards can be
648 hosted in a shard. However, DUTs of a single board cannot be distributed
649 into more than one shard.
650
Jakob Juelich59cfe542014-09-02 16:37:46 -0700651 @param shard: The shard object to assign labels/hosts for.
Jakob Juelich1b525742014-09-30 13:08:07 -0700652 @param known_ids: List of all host-ids the shard already knows.
653 This is used to figure out which hosts should be sent
654 to the shard. If shard_ids were used instead, hosts
655 would only be transferred once, even if the client
656 failed persisting them.
657 The number of hosts usually lies in O(100), so the
658 overhead is acceptable.
659
Aviv Keshet4be5a162017-03-23 08:59:06 -0700660 @returns a tuple of (hosts objects that should be sent to the shard,
661 incorrect host ids that should not belong to]
662 shard)
Jakob Juelich59cfe542014-09-02 16:37:46 -0700663 """
Jakob Juelich59cfe542014-09-02 16:37:46 -0700664 # Disclaimer: concurrent heartbeats should theoretically not occur in
665 # the current setup. As they may be introduced in the near future,
666 # this comment will be left here.
667
668 # Sending stuff twice is acceptable, but forgetting something isn't.
669 # Detecting duplicates on the client is easy, but here it's harder. The
670 # following options were considered:
671 # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
672 # than select returned, as concurrently more hosts might have been
673 # inserted
674 # - UPDATE and then SELECT WHERE shard=shard: select always returns all
675 # hosts for the shard, this is overhead
676 # - SELECT and then UPDATE only selected without requerying afterwards:
677 # returns the old state of the records.
Aviv Keshetbf963c12017-04-04 10:41:55 -0700678 new_hosts = []
Aviv Keshet4be5a162017-03-23 08:59:06 -0700679
Aviv Keshetbf963c12017-04-04 10:41:55 -0700680 possible_new_host_ids = set(Host.objects.filter(
MK Ryu638a6cf2015-05-08 14:49:43 -0700681 labels__in=shard.labels.all(),
Jakob Juelich59cfe542014-09-02 16:37:46 -0700682 leased=False
Jakob Juelich1b525742014-09-30 13:08:07 -0700683 ).exclude(
684 id__in=known_ids,
Jakob Juelich59cfe542014-09-02 16:37:46 -0700685 ).values_list('pk', flat=True))
686
Aviv Keshetbf963c12017-04-04 10:41:55 -0700687 # No-op in production, used to simulate race condition in tests.
688 cls._assign_to_shard_nothing_helper()
689
690 if possible_new_host_ids:
691 Host.objects.filter(
692 pk__in=possible_new_host_ids,
693 labels__in=shard.labels.all(),
694 leased=False
695 ).update(shard=shard)
696 new_hosts = list(Host.objects.filter(
697 pk__in=possible_new_host_ids,
698 shard=shard
699 ).all())
Aviv Keshet4be5a162017-03-23 08:59:06 -0700700
701 invalid_host_ids = list(Host.objects.filter(
702 id__in=known_ids
703 ).exclude(
704 shard=shard
705 ).values_list('pk', flat=True))
706
Aviv Keshetbf963c12017-04-04 10:41:55 -0700707 return new_hosts, invalid_host_ids
Jakob Juelich59cfe542014-09-02 16:37:46 -0700708
showardafd97de2009-10-01 18:45:09 +0000709 def resurrect_object(self, old_object):
710 super(Host, self).resurrect_object(old_object)
711 # invalid hosts can be in use by the scheduler (as one-time hosts), so
712 # don't change the status
713 self.status = old_object.status
714
715
jadmanski0afbb632008-06-06 21:10:57 +0000716 def clean_object(self):
717 self.aclgroup_set.clear()
718 self.labels.clear()
Xixuan Wucc12f3e2018-01-02 15:50:14 -0800719 self.static_labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000720
721
showarda5288b42009-07-28 20:06:08 +0000722 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000723 # extra spaces in the hostname can be a sneaky source of errors
724 self.hostname = self.hostname.strip()
725 # is this a new object being saved for the first time?
726 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000727 if not first_time:
728 AclGroup.check_for_acl_violation_hosts([self])
Dan Shia0acfbc2014-10-14 15:56:23 -0700729 # If locked is changed, send its status and user made the change to
730 # metaDB. Locks are important in host history because if a device is
731 # locked then we don't really care what state it is in.
showardfb2a7fa2008-07-17 17:04:12 +0000732 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000733 self.locked_by = User.current_user()
MK Ryud53e1492015-12-15 12:09:03 -0800734 if not self.lock_time:
735 self.lock_time = datetime.now()
showard21baa452008-10-21 00:08:39 +0000736 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000737 elif not self.locked and self.locked_by:
738 self.locked_by = None
739 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000740 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000741 if first_time:
742 everyone = AclGroup.objects.get(name='Everyone')
743 everyone.hosts.add(self)
Matt Mallett77545a32018-01-31 16:09:29 -0800744 # remove attributes that may have lingered from an old host and
745 # should not be associated with a new host
746 for host_attribute in self.hostattribute_set.all():
747 self.delete_attribute(host_attribute.attribute)
showard2bab8f42008-11-12 18:15:22 +0000748 self._check_for_updated_attributes()
749
mblighe8819cd2008-02-15 16:48:40 +0000750
showardb8471e32008-07-03 19:51:08 +0000751 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000752 AclGroup.check_for_acl_violation_hosts([self])
Aviv Kesheta8dcf052018-02-13 16:50:19 -0800753 logging.info('Preconditions for deleting host %s...', self.hostname)
showardb8471e32008-07-03 19:51:08 +0000754 for queue_entry in self.hostqueueentry_set.all():
Aviv Kesheta8dcf052018-02-13 16:50:19 -0800755 logging.info(' Deleting and aborting hqe %s...', queue_entry)
showardb8471e32008-07-03 19:51:08 +0000756 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000757 queue_entry.abort()
Aviv Kesheta8dcf052018-02-13 16:50:19 -0800758 logging.info(' ... done with hqe %s.', queue_entry)
Matt Mallett77545a32018-01-31 16:09:29 -0800759 for host_attribute in self.hostattribute_set.all():
Aviv Kesheta8dcf052018-02-13 16:50:19 -0800760 logging.info(' Deleting attribute %s...', host_attribute)
Matt Mallett77545a32018-01-31 16:09:29 -0800761 self.delete_attribute(host_attribute.attribute)
Aviv Kesheta8dcf052018-02-13 16:50:19 -0800762 logging.info(' ... done with attribute %s.', host_attribute)
763 logging.info('... preconditions done for host %s.', self.hostname)
764 logging.info('Deleting host %s...', self.hostname)
showardb8471e32008-07-03 19:51:08 +0000765 super(Host, self).delete()
Aviv Kesheta8dcf052018-02-13 16:50:19 -0800766 logging.info('... done.')
showardb8471e32008-07-03 19:51:08 +0000767
mblighe8819cd2008-02-15 16:48:40 +0000768
showard2bab8f42008-11-12 18:15:22 +0000769 def on_attribute_changed(self, attribute, old_value):
770 assert attribute == 'status'
Allen Lifc284ad2018-02-14 11:31:14 -0800771 logging.info('%s -> %s', self.hostname, self.status)
showard2bab8f42008-11-12 18:15:22 +0000772
773
Allen Li204eac12017-01-30 18:31:26 -0800774 def enqueue_job(self, job, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800775 """Enqueue a job on this host.
776
777 @param job: A job to enqueue.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800778 @param is_template: Whther the status should be "Template".
779 """
showard29f7cd22009-04-29 21:16:24 +0000780 queue_entry = HostQueueEntry.create(host=self, job=job,
Allen Li204eac12017-01-30 18:31:26 -0800781 is_template=is_template)
jadmanski0afbb632008-06-06 21:10:57 +0000782 # allow recovery of dead hosts from the frontend
783 if not self.active_queue_entry() and self.is_dead():
784 self.status = Host.Status.READY
785 self.save()
786 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000787
showard08f981b2008-06-24 21:59:03 +0000788 block = IneligibleHostQueue(job=job, host=self)
789 block.save()
790
mblighe8819cd2008-02-15 16:48:40 +0000791
jadmanski0afbb632008-06-06 21:10:57 +0000792 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800793 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000794 # TODO(showard): slighly hacky?
795 platforms = self.labels.filter(platform=True)
796 if len(platforms) == 0:
797 return None
798 return platforms[0]
799 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000800
801
showardcafd16e2009-05-29 18:37:49 +0000802 @classmethod
803 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800804 """Verify the specified hosts have no associated platforms.
805
806 @param cls: Implicit class object.
807 @param hosts: The hosts to verify.
808 @raises model_logic.ValidationError if any hosts already have a
809 platform.
810 """
showardcafd16e2009-05-29 18:37:49 +0000811 Host.objects.populate_relationships(hosts, Label, 'label_list')
Xixuan Wue7706032017-12-21 15:57:41 -0800812 Host.objects.populate_relationships(hosts, StaticLabel,
813 'staticlabel_list')
showardcafd16e2009-05-29 18:37:49 +0000814 errors = []
815 for host in hosts:
816 platforms = [label.name for label in host.label_list
817 if label.platform]
Xixuan Wue7706032017-12-21 15:57:41 -0800818 if RESPECT_STATIC_LABELS:
819 platforms += [label.name for label in host.staticlabel_list
820 if label.platform]
821
showardcafd16e2009-05-29 18:37:49 +0000822 if platforms:
823 # do a join, just in case this host has multiple platforms,
824 # we'll be able to see it
825 errors.append('Host %s already has a platform: %s' % (
826 host.hostname, ', '.join(platforms)))
827 if errors:
828 raise model_logic.ValidationError({'labels': '; '.join(errors)})
829
830
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700831 @classmethod
Dan Shib5b8b4f2016-11-02 14:04:02 -0700832 def check_board_labels_allowed(cls, hosts, new_labels=[]):
833 """Verify the specified hosts have valid board labels and the given
834 new board labels can be added.
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700835
836 @param cls: Implicit class object.
837 @param hosts: The hosts to verify.
Dan Shib5b8b4f2016-11-02 14:04:02 -0700838 @param new_labels: A list of labels to be added to the hosts.
839
840 @raises model_logic.ValidationError if any host has invalid board labels
841 or the given board labels cannot be added to the hsots.
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700842 """
843 Host.objects.populate_relationships(hosts, Label, 'label_list')
Xixuan Wue7706032017-12-21 15:57:41 -0800844 Host.objects.populate_relationships(hosts, StaticLabel,
845 'staticlabel_list')
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700846 errors = []
847 for host in hosts:
848 boards = [label.name for label in host.label_list
849 if label.name.startswith('board:')]
Xixuan Wue7706032017-12-21 15:57:41 -0800850 if RESPECT_STATIC_LABELS:
851 boards += [label.name for label in host.staticlabel_list
852 if label.name.startswith('board:')]
853
Dan Shib5b8b4f2016-11-02 14:04:02 -0700854 if not server_utils.board_labels_allowed(boards + new_labels):
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700855 # do a join, just in case this host has multiple boards,
856 # we'll be able to see it
Dan Shib5b8b4f2016-11-02 14:04:02 -0700857 errors.append('Host %s already has board labels: %s' % (
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700858 host.hostname, ', '.join(boards)))
859 if errors:
860 raise model_logic.ValidationError({'labels': '; '.join(errors)})
861
862
jadmanski0afbb632008-06-06 21:10:57 +0000863 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800864 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000865 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000866
867
jadmanski0afbb632008-06-06 21:10:57 +0000868 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800869 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000870 active = list(self.hostqueueentry_set.filter(active=True))
871 if not active:
872 return None
873 assert len(active) == 1, ('More than one active entry for '
874 'host ' + self.hostname)
875 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000876
877
showardf8b19042009-05-12 17:22:49 +0000878 def _get_attribute_model_and_args(self, attribute):
879 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000880
881
Xixuan Wu903087b2018-02-20 15:36:30 -0800882 def _get_static_attribute_model_and_args(self, attribute):
883 return StaticHostAttribute, dict(host=self, attribute=attribute)
884
885
Xixuan Wuc00f6032018-02-22 12:41:41 -0800886 def _is_replaced_by_static_attribute(self, attribute):
887 if RESPECT_STATIC_ATTRIBUTES:
888 model, args = self._get_static_attribute_model_and_args(attribute)
889 try:
890 static_attr = model.objects.get(**args)
891 return True
892 except StaticHostAttribute.DoesNotExist:
893 return False
894
895 return False
896
897
Fang Dengff361592015-02-02 15:27:34 -0800898 @classmethod
899 def get_attribute_model(cls):
900 """Return the attribute model.
901
902 Override method in parent class. See ModelExtensions for details.
903 @returns: The attribute model of Host.
904 """
905 return HostAttribute
906
907
jadmanski0afbb632008-06-06 21:10:57 +0000908 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800909 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000910 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000911
Fang Dengff361592015-02-02 15:27:34 -0800912
showarda5288b42009-07-28 20:06:08 +0000913 def __unicode__(self):
914 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000915
916
MK Ryuacf35922014-10-03 14:56:49 -0700917class HostAttribute(dbmodels.Model, model_logic.ModelExtensions):
showard0957a842009-05-11 19:25:08 +0000918 """Arbitrary keyvals associated with hosts."""
Fang Deng86248502014-12-18 16:38:00 -0800919
920 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Fang Dengff361592015-02-02 15:27:34 -0800921 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
showard0957a842009-05-11 19:25:08 +0000922 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000923 attribute = dbmodels.CharField(max_length=90)
924 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000925
926 objects = model_logic.ExtendedManager()
927
928 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800929 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000930 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000931
932
Fang Dengff361592015-02-02 15:27:34 -0800933 @classmethod
934 def get_record(cls, data):
935 """Check the database for an identical record.
936
937 Use host_id and attribute to search for a existing record.
938
939 @raises: DoesNotExist, if no record found
940 @raises: MultipleObjectsReturned if multiple records found.
941 """
942 # TODO(fdeng): We should use host_id and attribute together as
943 # a primary key in the db.
944 return cls.objects.get(host_id=data['host_id'],
945 attribute=data['attribute'])
946
947
948 @classmethod
949 def deserialize(cls, data):
950 """Override deserialize in parent class.
951
952 Do not deserialize id as id is not kept consistent on master and shards.
953
954 @param data: A dictionary of data to deserialize.
955
956 @returns: A HostAttribute object.
957 """
958 if data:
959 data.pop('id')
960 return super(HostAttribute, cls).deserialize(data)
961
962
Xixuan Wu903087b2018-02-20 15:36:30 -0800963class StaticHostAttribute(dbmodels.Model, model_logic.ModelExtensions):
964 """Static arbitrary keyvals associated with hosts."""
965
966 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
967 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
968 host = dbmodels.ForeignKey(Host)
969 attribute = dbmodels.CharField(max_length=90)
970 value = dbmodels.CharField(max_length=300)
971
972 objects = model_logic.ExtendedManager()
973
974 class Meta:
975 """Metadata for the StaticHostAttribute class."""
976 db_table = 'afe_static_host_attributes'
977
978
979 @classmethod
980 def get_record(cls, data):
981 """Check the database for an identical record.
982
983 Use host_id and attribute to search for a existing record.
984
985 @raises: DoesNotExist, if no record found
986 @raises: MultipleObjectsReturned if multiple records found.
987 """
988 return cls.objects.get(host_id=data['host_id'],
989 attribute=data['attribute'])
990
991
992 @classmethod
993 def deserialize(cls, data):
994 """Override deserialize in parent class.
995
996 Do not deserialize id as id is not kept consistent on master and shards.
997
998 @param data: A dictionary of data to deserialize.
999
1000 @returns: A StaticHostAttribute object.
1001 """
1002 if data:
1003 data.pop('id')
1004 return super(StaticHostAttribute, cls).deserialize(data)
1005
1006
showard7c785282008-05-29 19:45:12 +00001007class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001008 """\
1009 Required:
showard909c7a62008-07-15 21:52:38 +00001010 author: author name
1011 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +00001012 name: test name
showard909c7a62008-07-15 21:52:38 +00001013 time: short, medium, long
1014 test_class: This describes the class for your the test belongs in.
1015 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +00001016 test_type: Client or Server
1017 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +00001018 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
1019 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +00001020 i.e. if sync_count = 2 it is a sync job that requires two
1021 machines.
jadmanski0afbb632008-06-06 21:10:57 +00001022 Optional:
showard909c7a62008-07-15 21:52:38 +00001023 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +00001024 dependency_labels: many-to-many relationship with labels corresponding to
1025 test dependencies.
showard909c7a62008-07-15 21:52:38 +00001026 experimental: If this is set to True production servers will ignore the test
1027 run_verify: Whether or not the scheduler should run the verify stage
Dan Shi07e09af2013-04-12 09:31:29 -07001028 run_reset: Whether or not the scheduler should run the reset stage
Aviv Keshet9af96d32013-03-05 12:56:24 -08001029 test_retry: Number of times to retry test if the test did not complete
1030 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +00001031 """
showard909c7a62008-07-15 21:52:38 +00001032 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +00001033
showarda5288b42009-07-28 20:06:08 +00001034 name = dbmodels.CharField(max_length=255, unique=True)
1035 author = dbmodels.CharField(max_length=255)
1036 test_class = dbmodels.CharField(max_length=255)
1037 test_category = dbmodels.CharField(max_length=255)
1038 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +00001039 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +00001040 experimental = dbmodels.BooleanField(default=True)
Dan Shi07e09af2013-04-12 09:31:29 -07001041 run_verify = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +00001042 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
1043 default=TestTime.MEDIUM)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001044 test_type = dbmodels.SmallIntegerField(
1045 choices=control_data.CONTROL_TYPE.choices())
showard909c7a62008-07-15 21:52:38 +00001046 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +00001047 path = dbmodels.CharField(max_length=255, unique=True)
Aviv Keshet9af96d32013-03-05 12:56:24 -08001048 test_retry = dbmodels.IntegerField(blank=True, default=0)
Dan Shi07e09af2013-04-12 09:31:29 -07001049 run_reset = dbmodels.BooleanField(default=True)
mblighe8819cd2008-02-15 16:48:40 +00001050
showardeab66ce2009-12-23 00:03:56 +00001051 dependency_labels = (
1052 dbmodels.ManyToManyField(Label, blank=True,
1053 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +00001054 name_field = 'name'
1055 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +00001056
1057
jamesren35a70222010-02-16 19:30:46 +00001058 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001059 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +00001060 escaped_description = saxutils.escape(self.description)
1061 return '<span style="white-space:pre">%s</span>' % escaped_description
1062 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +00001063 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +00001064
1065
jadmanski0afbb632008-06-06 21:10:57 +00001066 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001067 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +00001068 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +00001069
showarda5288b42009-07-28 20:06:08 +00001070 def __unicode__(self):
1071 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +00001072
1073
jamesren4a41e012010-07-16 22:33:48 +00001074class TestParameter(dbmodels.Model):
1075 """
1076 A declared parameter of a test
1077 """
1078 test = dbmodels.ForeignKey(Test)
1079 name = dbmodels.CharField(max_length=255)
1080
1081 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001082 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001083 db_table = 'afe_test_parameters'
1084 unique_together = ('test', 'name')
1085
1086 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -07001087 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +00001088
1089
showard2b9a88b2008-06-13 20:55:03 +00001090class Profiler(dbmodels.Model, model_logic.ModelExtensions):
1091 """\
1092 Required:
1093 name: profiler name
1094 test_type: Client or Server
1095
1096 Optional:
1097 description: arbirary text description
1098 """
showarda5288b42009-07-28 20:06:08 +00001099 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +00001100 description = dbmodels.TextField(blank=True)
1101
1102 name_field = 'name'
1103 objects = model_logic.ExtendedManager()
1104
1105
1106 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001107 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +00001108 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +00001109
showarda5288b42009-07-28 20:06:08 +00001110 def __unicode__(self):
1111 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +00001112
1113
showard7c785282008-05-29 19:45:12 +00001114class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001115 """\
1116 Required:
1117 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +00001118
jadmanski0afbb632008-06-06 21:10:57 +00001119 Optional:
1120 description: arbitrary description of group
1121 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001122
1123 SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
1124
showarda5288b42009-07-28 20:06:08 +00001125 name = dbmodels.CharField(max_length=255, unique=True)
1126 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001127 users = dbmodels.ManyToManyField(User, blank=False,
1128 db_table='afe_acl_groups_users')
1129 hosts = dbmodels.ManyToManyField(Host, blank=True,
1130 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +00001131
jadmanski0afbb632008-06-06 21:10:57 +00001132 name_field = 'name'
1133 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001134
showard08f981b2008-06-24 21:59:03 +00001135 @staticmethod
showard3dd47c22008-07-10 00:41:36 +00001136 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001137 """Verify the current user has access to the specified hosts.
1138
1139 @param hosts: The hosts to verify against.
1140 @raises AclAccessViolation if the current user doesn't have access
1141 to a host.
1142 """
showard64a95952010-01-13 21:27:16 +00001143 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +00001144 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +00001145 return
showard3dd47c22008-07-10 00:41:36 +00001146 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +00001147 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +00001148 for host in hosts:
1149 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001150 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +00001151 no_access = (isinstance(host, Host)
1152 and not host.invalid
1153 and int(host.id) not in accessible_host_ids)
1154 if no_access:
showardeaa408e2009-09-11 18:45:31 +00001155 raise AclAccessViolation("%s does not have access to %s" %
1156 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +00001157
showard9dbdcda2008-10-14 17:34:36 +00001158
1159 @staticmethod
showarddc817512008-11-12 18:16:41 +00001160 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001161 """Look for queue entries that aren't abortable by the current user.
1162
1163 An entry is not abortable if:
1164 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +00001165 * the machine isn't ACL-accessible, or
1166 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001167
1168 @param queue_entries: The queue entries to check.
1169 @raises AclAccessViolation if a queue entry is not abortable by the
1170 current user.
showarddc817512008-11-12 18:16:41 +00001171 """
showard64a95952010-01-13 21:27:16 +00001172 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +00001173 if user.is_superuser():
1174 return
showarddc817512008-11-12 18:16:41 +00001175 not_owned = queue_entries.exclude(job__owner=user.login)
1176 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +00001177 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
1178 # 1.0.
1179 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +00001180 accessible_ids = set(
1181 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +00001182 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +00001183 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +00001184 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +00001185 cannot_abort = [entry for entry in not_owned.select_related()
1186 if entry.id not in accessible_ids
1187 or entry.id in public_ids]
1188 if len(cannot_abort) == 0:
1189 return
1190 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +00001191 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +00001192 for entry in cannot_abort)
1193 raise AclAccessViolation('You cannot abort the following job entries: '
1194 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +00001195
1196
showard3dd47c22008-07-10 00:41:36 +00001197 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001198 """Verifies the current user has acces to this ACL group.
1199
1200 @raises AclAccessViolation if the current user doesn't have access to
1201 this ACL group.
1202 """
showard64a95952010-01-13 21:27:16 +00001203 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +00001204 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +00001205 return
1206 if self.name == 'Everyone':
1207 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +00001208 if not user in self.users.all():
1209 raise AclAccessViolation("You do not have access to %s"
1210 % self.name)
1211
1212 @staticmethod
showard08f981b2008-06-24 21:59:03 +00001213 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001214 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +00001215 everyone = AclGroup.objects.get(name='Everyone')
1216
showard3dd47c22008-07-10 00:41:36 +00001217 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +00001218 # TODO(showard): this is a bit of a hack, since the fact that this query
1219 # works is kind of a coincidence of Django internals. This trick
1220 # doesn't work in general (on all foreign key relationships). I'll
1221 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +00001222 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +00001223 everyone.hosts.add(*orphaned_hosts.distinct())
1224
1225 # find hosts in both Everyone and another ACL group, and remove them
1226 # from Everyone
showarda5288b42009-07-28 20:06:08 +00001227 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
1228 acled_hosts = set()
1229 for host in hosts_in_everyone:
1230 # Has an ACL group other than Everyone
1231 if host.aclgroup_set.count() > 1:
1232 acled_hosts.add(host)
1233 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +00001234
1235
1236 def delete(self):
showard3dd47c22008-07-10 00:41:36 +00001237 if (self.name == 'Everyone'):
1238 raise AclAccessViolation("You cannot delete 'Everyone'!")
1239 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +00001240 super(AclGroup, self).delete()
1241 self.on_host_membership_change()
1242
1243
showard04f2cd82008-07-25 20:53:31 +00001244 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001245 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +00001246 if not self.users.count():
showard64a95952010-01-13 21:27:16 +00001247 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +00001248
1249
showard8cbaf1e2009-09-08 16:27:04 +00001250 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001251 """Called after a save.
1252
1253 @param change: Whether there was a change.
1254 """
showard8cbaf1e2009-09-08 16:27:04 +00001255 if not change:
showard64a95952010-01-13 21:27:16 +00001256 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +00001257 self.add_current_user_if_empty()
1258 self.on_host_membership_change()
1259
1260
1261 def save(self, *args, **kwargs):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001262 change = bool(self.id)
1263 if change:
showard8cbaf1e2009-09-08 16:27:04 +00001264 # Check the original object for an ACL violation
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001265 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
showard8cbaf1e2009-09-08 16:27:04 +00001266 super(AclGroup, self).save(*args, **kwargs)
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001267 self.perform_after_save(change)
showard8cbaf1e2009-09-08 16:27:04 +00001268
showardeb3be4d2008-04-21 20:59:26 +00001269
jadmanski0afbb632008-06-06 21:10:57 +00001270 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001271 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +00001272 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +00001273
showarda5288b42009-07-28 20:06:08 +00001274 def __unicode__(self):
1275 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +00001276
mblighe8819cd2008-02-15 16:48:40 +00001277
jamesren4a41e012010-07-16 22:33:48 +00001278class ParameterizedJob(dbmodels.Model):
1279 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001280 Auxiliary configuration for a parameterized job.
Richard Barnettefddab4a2017-02-02 13:23:30 -08001281
1282 This class is obsolete, and ought to be dead. Due to a series of
1283 unfortunate events, it can't be deleted:
1284 * In `class Job` we're required to keep a reference to this class
1285 for the sake of the scheduler unit tests.
1286 * The existence of the reference in `Job` means that certain
1287 methods here will get called from the `get_jobs` RPC.
1288 So, the definitions below seem to be the minimum stub we can support
1289 unless/until we change the database schema.
jamesren4a41e012010-07-16 22:33:48 +00001290 """
Richard Barnettef616e8f2017-03-06 09:36:15 -08001291
1292 @classmethod
1293 def smart_get(cls, id_or_name, *args, **kwargs):
1294 """For compatibility with Job.add_object.
1295
1296 @param cls: Implicit class object.
1297 @param id_or_name: The ID or name to get.
1298 @param args: Non-keyword arguments.
1299 @param kwargs: Keyword arguments.
1300 """
1301 return cls.objects.get(pk=id_or_name)
1302
1303
1304 def job(self):
1305 """Returns the job if it exists, or else None."""
1306 jobs = self.job_set.all()
1307 assert jobs.count() <= 1
1308 return jobs and jobs[0] or None
1309
1310
1311 class Meta:
1312 """Metadata for class ParameterizedJob."""
1313 db_table = 'afe_parameterized_jobs'
1314
1315 def __unicode__(self):
1316 return u'%s (parameterized) - %s' % (self.test.name, self.job())
1317
1318
showard7c785282008-05-29 19:45:12 +00001319class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +00001320 'Custom manager to provide efficient status counts querying.'
1321 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001322 """Returns a dict mapping the given job IDs to their status count dicts.
1323
1324 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +00001325 """
1326 if not job_ids:
1327 return {}
1328 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1329 cursor = connection.cursor()
1330 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +00001331 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +00001332 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +00001333 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +00001334 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +00001335 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +00001336 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +00001337 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +00001338 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +00001339 full_status = HostQueueEntry.compute_full_status(status, aborted,
1340 complete)
showardb6d16622009-05-26 19:35:29 +00001341 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +00001342 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +00001343 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +00001344
1345
showard7c785282008-05-29 19:45:12 +00001346class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001347 """\
1348 owner: username of job owner
1349 name: job name (does not have to be unique)
Alex Miller7d658cf2013-09-04 16:00:35 -07001350 priority: Integer priority value. Higher is more important.
jadmanski0afbb632008-06-06 21:10:57 +00001351 control_file: contents of control file
1352 control_type: Client or Server
1353 created_on: date of job creation
1354 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +00001355 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +00001356 run_verify: Whether or not to run the verify phase
Dan Shi07e09af2013-04-12 09:31:29 -07001357 run_reset: Whether or not to run the reset phase
Simran Basi94dc0032013-11-12 14:09:46 -08001358 timeout: DEPRECATED - hours from queuing time until job times out
1359 timeout_mins: minutes from job queuing time until the job times out
Simran Basi34217022012-11-06 13:43:15 -08001360 max_runtime_hrs: DEPRECATED - hours from job starting time until job
1361 times out
1362 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +00001363 email_list: list of people to email on completion delimited by any of:
1364 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +00001365 dependency_labels: many-to-many relationship with labels corresponding to
1366 job dependencies
showard21baa452008-10-21 00:08:39 +00001367 reboot_before: Never, If dirty, or Always
1368 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001369 parse_failed_repair: if True, a failed repair launched by this job will have
1370 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001371 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001372 parent_job: Parent job (optional)
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001373 test_retry: Number of times to retry test if the test did not complete
1374 successfully. (optional, default: 0)
Dan Shic9e17142015-02-19 11:50:55 -08001375 require_ssp: Require server-side packaging unless require_ssp is set to
1376 False. (optional, default: None)
jadmanski0afbb632008-06-06 21:10:57 +00001377 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001378
1379 # TODO: Investigate, if jobkeyval_set is really needed.
1380 # dynamic_suite will write them into an attached file for the drone, but
1381 # it doesn't seem like they are actually used. If they aren't used, remove
1382 # jobkeyval_set here.
1383 SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1384 'hostqueueentry_set',
1385 'jobkeyval_set',
1386 'shard'])
1387
Fang Deng2b79fd72015-05-20 19:11:36 -07001388 # SQL for selecting jobs that should be sent to shard.
1389 # We use raw sql as django filters were not optimized.
1390 # The following jobs are excluded by the SQL.
1391 # - Non-aborted jobs known to shard as specified in |known_ids|.
1392 # Note for jobs aborted on master, even if already known to shard,
1393 # will be sent to shard again so that shard can abort them.
1394 # - Completed jobs
1395 # - Active jobs
1396 # - Jobs without host_queue_entries
1397 NON_ABORTED_KNOWN_JOBS = '(t2.aborted = 0 AND t1.id IN (%(known_ids)s))'
1398
1399 SQL_SHARD_JOBS = (
MK Ryu5c7ef302015-07-10 17:23:08 -07001400 'SELECT DISTINCT(t1.id) FROM afe_jobs t1 '
Fang Deng2b79fd72015-05-20 19:11:36 -07001401 'INNER JOIN afe_host_queue_entries t2 ON '
MK Ryu5c7ef302015-07-10 17:23:08 -07001402 ' (t1.id = t2.job_id AND t2.complete != 1 AND t2.active != 1 '
Fang Deng2b79fd72015-05-20 19:11:36 -07001403 ' %(check_known_jobs)s) '
MK Ryu5c7ef302015-07-10 17:23:08 -07001404 'LEFT OUTER JOIN afe_jobs_dependency_labels t3 ON (t1.id = t3.job_id) '
Dan Shi6b89f1c2016-09-17 22:25:53 -07001405 'JOIN afe_shards_labels t4 '
1406 ' ON (t4.label_id = t3.label_id OR t4.label_id = t2.meta_host) '
1407 'WHERE t4.shard_id = %(shard_id)s'
Fang Deng2b79fd72015-05-20 19:11:36 -07001408 )
1409
MK Ryu5c7ef302015-07-10 17:23:08 -07001410 # Jobs can be created with assigned hosts and have no dependency
1411 # labels nor meta_host.
Fang Deng2b79fd72015-05-20 19:11:36 -07001412 # We are looking for:
MK Ryu5c7ef302015-07-10 17:23:08 -07001413 # - a job whose hqe's meta_host is null
Fang Deng2b79fd72015-05-20 19:11:36 -07001414 # - a job whose hqe has a host
1415 # - one of the host's labels matches the shard's label.
1416 # Non-aborted known jobs, completed jobs, active jobs, jobs
Prathmesh Prabhu2e315cf2018-03-02 20:32:37 +00001417 # without hqe are exluded as we do with SQL_SHARD_JOBS.
MK Ryu5c7ef302015-07-10 17:23:08 -07001418 SQL_SHARD_JOBS_WITH_HOSTS = (
1419 'SELECT DISTINCT(t1.id) FROM afe_jobs t1 '
Fang Deng2b79fd72015-05-20 19:11:36 -07001420 'INNER JOIN afe_host_queue_entries t2 ON '
1421 ' (t1.id = t2.job_id AND t2.complete != 1 AND t2.active != 1 '
1422 ' AND t2.meta_host IS NULL AND t2.host_id IS NOT NULL '
1423 ' %(check_known_jobs)s) '
Xixuan Wu4379e932018-01-17 10:29:50 -08001424 'LEFT OUTER JOIN %(host_label_table)s t3 ON (t2.host_id = t3.host_id) '
1425 'WHERE (t3.%(host_label_column)s IN %(label_ids)s)'
Fang Deng2b79fd72015-05-20 19:11:36 -07001426 )
MK Ryu5c7ef302015-07-10 17:23:08 -07001427
Fang Deng2b79fd72015-05-20 19:11:36 -07001428 # Even if we had filters about complete, active and aborted
MK Ryu5c7ef302015-07-10 17:23:08 -07001429 # bits in the above two SQLs, there is a chance that
Fang Deng2b79fd72015-05-20 19:11:36 -07001430 # the result may still contain a job with an hqe with 'complete=1'
1431 # or 'active=1' or 'aborted=0 and afe_job.id in known jobs.'
1432 # This happens when a job has two (or more) hqes and at least
1433 # one hqe has different bits than others.
1434 # We use a second sql to ensure we exclude all un-desired jobs.
1435 SQL_JOBS_TO_EXCLUDE =(
1436 'SELECT t1.id FROM afe_jobs t1 '
1437 'INNER JOIN afe_host_queue_entries t2 ON '
1438 ' (t1.id = t2.job_id) '
1439 'WHERE (t1.id in (%(candidates)s) '
1440 ' AND (t2.complete=1 OR t2.active=1 '
1441 ' %(check_known_jobs)s))'
1442 )
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001443
Jakob Juelichf88fa932014-09-03 17:58:04 -07001444 def _deserialize_relation(self, link, data):
1445 if link in ['hostqueueentry_set', 'jobkeyval_set']:
1446 for obj in data:
1447 obj['job_id'] = self.id
1448
1449 super(Job, self)._deserialize_relation(link, data)
1450
1451
1452 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001453 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -07001454 self.shard = Shard.deserialize(data)
1455
1456
Jakob Juelicha94efe62014-09-18 16:02:49 -07001457 def sanity_check_update_from_shard(self, shard, updated_serialized):
Jakob Juelich02e61292014-10-17 12:36:55 -07001458 # If the job got aborted on the master after the client fetched it
1459 # no shard_id will be set. The shard might still push updates though,
1460 # as the job might complete before the abort bit syncs to the shard.
1461 # Alternative considered: The master scheduler could be changed to not
1462 # set aborted jobs to completed that are sharded out. But that would
1463 # require database queries and seemed more complicated to implement.
1464 # This seems safe to do, as there won't be updates pushed from the wrong
1465 # shards should be powered off and wiped hen they are removed from the
1466 # master.
1467 if self.shard_id and self.shard_id != shard.id:
Aviv Keshet6dec0e12017-04-17 15:23:33 -07001468 raise error.IgnorableUnallowedRecordsSentToMaster(
Jakob Juelicha94efe62014-09-18 16:02:49 -07001469 'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1470 'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1471 shard.id))
1472
1473
Allen Lib8b2e592017-12-14 17:41:40 -08001474 RebootBefore = model_attributes.RebootBefore
1475 RebootAfter = model_attributes.RebootAfter
Simran Basi94dc0032013-11-12 14:09:46 -08001476 # TIMEOUT is deprecated.
showardb1e51872008-10-07 11:08:18 +00001477 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
Simran Basi94dc0032013-11-12 14:09:46 -08001478 'AUTOTEST_WEB', 'job_timeout_default', default=24)
1479 DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1480 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
Simran Basi34217022012-11-06 13:43:15 -08001481 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1482 # completed.
showard12f3e322009-05-13 21:27:42 +00001483 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1484 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001485 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1486 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001487 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1488 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1489 default=False)
Jacob Kopczynskid6a5e912018-03-01 09:52:34 -08001490 FETCH_READONLY_JOBS = global_config.global_config.get_config_value(
1491 'AUTOTEST_WEB','shard_heartbeat_use_readonly_slave', default=False)
showardb1e51872008-10-07 11:08:18 +00001492
showarda5288b42009-07-28 20:06:08 +00001493 owner = dbmodels.CharField(max_length=255)
1494 name = dbmodels.CharField(max_length=255)
Alex Miller7d658cf2013-09-04 16:00:35 -07001495 priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
jamesren4a41e012010-07-16 22:33:48 +00001496 control_file = dbmodels.TextField(null=True, blank=True)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001497 control_type = dbmodels.SmallIntegerField(
1498 choices=control_data.CONTROL_TYPE.choices(),
1499 blank=True, # to allow 0
1500 default=control_data.CONTROL_TYPE.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001501 created_on = dbmodels.DateTimeField()
Simran Basi8d89b642014-05-02 17:33:20 -07001502 synch_count = dbmodels.IntegerField(blank=True, default=0)
showardb1e51872008-10-07 11:08:18 +00001503 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
Dan Shi07e09af2013-04-12 09:31:29 -07001504 run_verify = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001505 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001506 dependency_labels = (
1507 dbmodels.ManyToManyField(Label, blank=True,
1508 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001509 reboot_before = dbmodels.SmallIntegerField(
1510 choices=model_attributes.RebootBefore.choices(), blank=True,
1511 default=DEFAULT_REBOOT_BEFORE)
1512 reboot_after = dbmodels.SmallIntegerField(
1513 choices=model_attributes.RebootAfter.choices(), blank=True,
1514 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001515 parse_failed_repair = dbmodels.BooleanField(
1516 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001517 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1518 # completed.
showard12f3e322009-05-13 21:27:42 +00001519 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001520 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001521 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001522
Richard Barnettefddab4a2017-02-02 13:23:30 -08001523 # TODO(jrbarnette) We have to keep `parameterized_job` around or it
1524 # breaks the scheduler_models unit tests (and fixing the unit tests
1525 # will break the scheduler, so don't do that).
1526 #
1527 # The ultimate fix is to delete the column from the database table
1528 # at which point, you _must_ delete this. Until you're ready to do
1529 # that, DON'T MUCK WITH IT.
jamesren4a41e012010-07-16 22:33:48 +00001530 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1531 blank=True)
1532
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001533 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001534
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001535 test_retry = dbmodels.IntegerField(blank=True, default=0)
1536
Dan Shi07e09af2013-04-12 09:31:29 -07001537 run_reset = dbmodels.BooleanField(default=True)
1538
Simran Basi94dc0032013-11-12 14:09:46 -08001539 timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1540
Jakob Juelich8421d592014-09-17 15:27:06 -07001541 # If this is None on the master, a slave should be found.
1542 # If this is None on a slave, it should be synced back to the master
Jakob Jülich92c06332014-08-25 19:06:57 +00001543 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1544
Dan Shic9e17142015-02-19 11:50:55 -08001545 # If this is None, server-side packaging will be used for server side test,
1546 # unless it's disabled in global config AUTOSERV/enable_ssp_container.
1547 require_ssp = dbmodels.NullBooleanField(default=None, blank=True, null=True)
1548
jadmanski0afbb632008-06-06 21:10:57 +00001549 # custom manager
1550 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001551
1552
Alex Millerec212252014-02-28 16:48:34 -08001553 @decorators.cached_property
1554 def labels(self):
1555 """All the labels of this job"""
1556 # We need to convert dependency_labels to a list, because all() gives us
1557 # back an iterator, and storing/caching an iterator means we'd only be
1558 # able to read from it once.
1559 return list(self.dependency_labels.all())
1560
1561
jadmanski0afbb632008-06-06 21:10:57 +00001562 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001563 """Returns whether this job is of type server."""
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001564 return self.control_type == control_data.CONTROL_TYPE.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001565
1566
jadmanski0afbb632008-06-06 21:10:57 +00001567 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001568 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001569 """Creates a job.
1570
1571 The job is created by taking some information (the listed args) and
1572 filling in the rest of the necessary information.
1573
1574 @param cls: Implicit class object.
1575 @param owner: The owner for the job.
1576 @param options: An options object.
1577 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001578 """
showard3dd47c22008-07-10 00:41:36 +00001579 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001580
jamesren4a41e012010-07-16 22:33:48 +00001581 control_file = options.get('control_file')
jamesren4a41e012010-07-16 22:33:48 +00001582
showard4d233752010-01-20 19:06:40 +00001583 user = User.current_user()
1584 if options.get('reboot_before') is None:
1585 options['reboot_before'] = user.get_reboot_before_display()
1586 if options.get('reboot_after') is None:
1587 options['reboot_after'] = user.get_reboot_after_display()
1588
jamesren76fcf192010-04-21 20:39:50 +00001589 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1590
Simran Basi94dc0032013-11-12 14:09:46 -08001591 if options.get('timeout_mins') is None and options.get('timeout'):
1592 options['timeout_mins'] = options['timeout'] * 60
1593
jadmanski0afbb632008-06-06 21:10:57 +00001594 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001595 owner=owner,
1596 name=options['name'],
1597 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001598 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001599 control_type=options['control_type'],
1600 synch_count=options.get('synch_count'),
Simran Basi94dc0032013-11-12 14:09:46 -08001601 # timeout needs to be deleted in the future.
showarda1e74b32009-05-12 17:32:04 +00001602 timeout=options.get('timeout'),
Simran Basi94dc0032013-11-12 14:09:46 -08001603 timeout_mins=options.get('timeout_mins'),
Simran Basi34217022012-11-06 13:43:15 -08001604 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001605 run_verify=options.get('run_verify'),
1606 email_list=options.get('email_list'),
1607 reboot_before=options.get('reboot_before'),
1608 reboot_after=options.get('reboot_after'),
1609 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001610 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001611 drone_set=drone_set,
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001612 parent_job=options.get('parent_job_id'),
Dan Shi07e09af2013-04-12 09:31:29 -07001613 test_retry=options.get('test_retry'),
Dan Shic9e17142015-02-19 11:50:55 -08001614 run_reset=options.get('run_reset'),
1615 require_ssp=options.get('require_ssp'))
mblighe8819cd2008-02-15 16:48:40 +00001616
showarda1e74b32009-05-12 17:32:04 +00001617 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001618
jamesrend8b6e172010-04-16 23:45:00 +00001619 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001620 for key, value in options['keyvals'].iteritems():
1621 JobKeyval.objects.create(job=job, key=key, value=value)
1622
jadmanski0afbb632008-06-06 21:10:57 +00001623 return job
mblighe8819cd2008-02-15 16:48:40 +00001624
1625
Jakob Juelich59cfe542014-09-02 16:37:46 -07001626 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -07001627 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -07001628 """Assigns unassigned jobs to a shard.
1629
Jakob Juelich1b525742014-09-30 13:08:07 -07001630 For all labels that have been assigned to this shard, all jobs that
1631 have this label, are assigned to this shard.
1632
1633 Jobs that are assigned to the shard but aren't already present on the
1634 shard are returned.
1635
Jakob Juelich59cfe542014-09-02 16:37:46 -07001636 @param shard: The shard to assign jobs to.
Jakob Juelich1b525742014-09-30 13:08:07 -07001637 @param known_ids: List of all ids of incomplete jobs, the shard already
1638 knows about.
1639 This is used to figure out which jobs should be sent
1640 to the shard. If shard_ids were used instead, jobs
1641 would only be transferred once, even if the client
1642 failed persisting them.
1643 The number of unfinished jobs usually lies in O(1000).
1644 Assuming one id takes 8 chars in the json, this means
1645 overhead that lies in the lower kilobyte range.
1646 A not in query with 5000 id's takes about 30ms.
1647
Jakob Juelich59cfe542014-09-02 16:37:46 -07001648 @returns The job objects that should be sent to the shard.
1649 """
1650 # Disclaimer: Concurrent heartbeats should not occur in today's setup.
1651 # If this changes or they are triggered manually, this applies:
1652 # Jobs may be returned more than once by concurrent calls of this
1653 # function, as there is a race condition between SELECT and UPDATE.
Fang Deng2b79fd72015-05-20 19:11:36 -07001654 job_ids = set([])
1655 check_known_jobs_exclude = ''
1656 check_known_jobs_include = ''
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001657
Fang Deng2b79fd72015-05-20 19:11:36 -07001658 if known_ids:
1659 check_known_jobs = (
1660 cls.NON_ABORTED_KNOWN_JOBS %
1661 {'known_ids': ','.join([str(i) for i in known_ids])})
1662 check_known_jobs_exclude = 'AND NOT ' + check_known_jobs
1663 check_known_jobs_include = 'OR ' + check_known_jobs
1664
Jacob Kopczynskid6a5e912018-03-01 09:52:34 -08001665 raw_sql = cls.SQL_SHARD_JOBS % {
1666 'check_known_jobs': check_known_jobs_exclude,
1667 'shard_id': shard.id
1668 }
1669 if cls.FETCH_READONLY_JOBS:
1670 #TODO(jkop): Get rid of this kludge when we update Django to >=1.7
1671 #correct usage would be .raw(..., using='readonly')
1672 old_db = Job.objects._db
1673 try:
1674 Job.objects._db = 'readonly'
1675 job_ids |= set([j.id for j in Job.objects.raw(raw_sql)])
1676 finally:
1677 Job.objects._db = old_db
1678 if not job_ids:
1679 #If the replica is down or we're in a test, fetch from master.
1680 job_ids |= set([j.id for j in Job.objects.raw(raw_sql)])
Xixuan Wu4379e932018-01-17 10:29:50 -08001681
1682 static_labels, non_static_labels = Host.classify_label_objects(
1683 shard.labels.all())
1684 if static_labels:
1685 label_ids = [str(l.id) for l in static_labels]
1686 query = Job.objects.raw(cls.SQL_SHARD_JOBS_WITH_HOSTS % {
1687 'check_known_jobs': check_known_jobs_exclude,
1688 'host_label_table': 'afe_static_hosts_labels',
1689 'host_label_column': 'staticlabel_id',
1690 'label_ids': '(%s)' % ','.join(label_ids)})
1691 job_ids |= set([j.id for j in query])
1692
1693 if non_static_labels:
1694 label_ids = [str(l.id) for l in non_static_labels]
1695 query = Job.objects.raw(cls.SQL_SHARD_JOBS_WITH_HOSTS % {
1696 'check_known_jobs': check_known_jobs_exclude,
1697 'host_label_table': 'afe_hosts_labels',
1698 'host_label_column': 'label_id',
1699 'label_ids': '(%s)' % ','.join(label_ids)})
Fang Deng2b79fd72015-05-20 19:11:36 -07001700 job_ids |= set([j.id for j in query])
1701
1702 if job_ids:
1703 query = Job.objects.raw(
1704 cls.SQL_JOBS_TO_EXCLUDE %
1705 {'check_known_jobs': check_known_jobs_include,
1706 'candidates': ','.join([str(i) for i in job_ids])})
1707 job_ids -= set([j.id for j in query])
1708
Jakob Juelich59cfe542014-09-02 16:37:46 -07001709 if job_ids:
1710 Job.objects.filter(pk__in=job_ids).update(shard=shard)
1711 return list(Job.objects.filter(pk__in=job_ids).all())
1712 return []
1713
1714
Allen Li204eac12017-01-30 18:31:26 -08001715 def queue(self, hosts, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001716 """Enqueue a job on the given hosts.
1717
1718 @param hosts: The hosts to use.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001719 @param is_template: Whether the status should be "Template".
1720 """
showarda9545c02009-12-18 22:44:26 +00001721 if not hosts:
Allen Li204eac12017-01-30 18:31:26 -08001722 # hostless job
1723 entry = HostQueueEntry.create(job=self, is_template=is_template)
1724 entry.save()
showarda9545c02009-12-18 22:44:26 +00001725 return
1726
jadmanski0afbb632008-06-06 21:10:57 +00001727 for host in hosts:
Allen Li204eac12017-01-30 18:31:26 -08001728 host.enqueue_job(self, is_template=is_template)
showard29f7cd22009-04-29 21:16:24 +00001729
1730
jadmanski0afbb632008-06-06 21:10:57 +00001731 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001732 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001733 try:
1734 return User.objects.get(login=self.owner)
1735 except self.DoesNotExist:
1736 return None
mblighe8819cd2008-02-15 16:48:40 +00001737
1738
showard64a95952010-01-13 21:27:16 +00001739 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001740 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001741 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001742 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001743
1744
showardd1195652009-12-08 22:21:02 +00001745 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001746 """Returns a string tag for this job."""
MK Ryu0c1a37d2015-04-30 12:00:55 -07001747 return server_utils.get_job_tag(self.id, self.owner)
showardd1195652009-12-08 22:21:02 +00001748
1749
showardc1a98d12010-01-15 00:22:22 +00001750 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001751 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001752 return dict((keyval.key, keyval.value)
1753 for keyval in self.jobkeyval_set.all())
1754
1755
Fang Dengff361592015-02-02 15:27:34 -08001756 @classmethod
1757 def get_attribute_model(cls):
1758 """Return the attribute model.
1759
1760 Override method in parent class. This class is called when
1761 deserializing the one-to-many relationship betwen Job and JobKeyval.
1762 On deserialization, we will try to clear any existing job keyvals
1763 associated with a job to avoid any inconsistency.
1764 Though Job doesn't implement ModelWithAttribute, we still treat
1765 it as an attribute model for this purpose.
1766
1767 @returns: The attribute model of Job.
1768 """
1769 return JobKeyval
1770
1771
jadmanski0afbb632008-06-06 21:10:57 +00001772 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001773 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001774 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001775
showarda5288b42009-07-28 20:06:08 +00001776 def __unicode__(self):
1777 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001778
1779
Allen Lic1f0a912017-10-19 17:19:02 -07001780class JobHandoff(dbmodels.Model, model_logic.ModelExtensions):
1781 """Jobs that have been handed off to lucifer."""
1782
Allen Lifef16ae2017-11-20 15:23:02 -08001783 job = dbmodels.OneToOneField(Job, on_delete=dbmodels.CASCADE,
1784 primary_key=True)
1785 created = dbmodels.DateTimeField(auto_now_add=True)
1786 completed = dbmodels.BooleanField(default=False)
Allen Li1fc27d32018-01-26 17:51:16 -08001787 drone = dbmodels.CharField(
1788 max_length=128, null=True,
1789 help_text='''
1790The hostname of the drone the job is running on and whose job_aborter
1791should be responsible for aborting the job if the job process dies.
1792NULL means any drone's job_aborter has free reign to abort the job.
1793''')
Allen Lic1f0a912017-10-19 17:19:02 -07001794
1795 class Meta:
1796 """Metadata for class Job."""
1797 db_table = 'afe_job_handoffs'
1798
1799
showardc1a98d12010-01-15 00:22:22 +00001800class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1801 """Keyvals associated with jobs"""
Fang Dengff361592015-02-02 15:27:34 -08001802
1803 SERIALIZATION_LINKS_TO_KEEP = set(['job'])
1804 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
1805
showardc1a98d12010-01-15 00:22:22 +00001806 job = dbmodels.ForeignKey(Job)
1807 key = dbmodels.CharField(max_length=90)
1808 value = dbmodels.CharField(max_length=300)
1809
1810 objects = model_logic.ExtendedManager()
1811
Fang Dengff361592015-02-02 15:27:34 -08001812
1813 @classmethod
1814 def get_record(cls, data):
1815 """Check the database for an identical record.
1816
1817 Use job_id and key to search for a existing record.
1818
1819 @raises: DoesNotExist, if no record found
1820 @raises: MultipleObjectsReturned if multiple records found.
1821 """
1822 # TODO(fdeng): We should use job_id and key together as
1823 # a primary key in the db.
1824 return cls.objects.get(job_id=data['job_id'], key=data['key'])
1825
1826
1827 @classmethod
1828 def deserialize(cls, data):
1829 """Override deserialize in parent class.
1830
1831 Do not deserialize id as id is not kept consistent on master and shards.
1832
1833 @param data: A dictionary of data to deserialize.
1834
1835 @returns: A JobKeyval object.
1836 """
1837 if data:
1838 data.pop('id')
1839 return super(JobKeyval, cls).deserialize(data)
1840
1841
showardc1a98d12010-01-15 00:22:22 +00001842 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001843 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001844 db_table = 'afe_job_keyvals'
1845
1846
showard7c785282008-05-29 19:45:12 +00001847class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001848 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001849 job = dbmodels.ForeignKey(Job)
1850 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001851
jadmanski0afbb632008-06-06 21:10:57 +00001852 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001853
jadmanski0afbb632008-06-06 21:10:57 +00001854 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001855 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001856 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001857
mblighe8819cd2008-02-15 16:48:40 +00001858
showard7c785282008-05-29 19:45:12 +00001859class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001860 """Represents a host queue entry."""
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001861
1862 SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001863 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Jakob Juelichf865d332014-09-29 10:47:49 -07001864 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001865
Jakob Juelichf88fa932014-09-03 17:58:04 -07001866
1867 def custom_deserialize_relation(self, link, data):
1868 assert link == 'meta_host'
1869 self.meta_host = Label.deserialize(data)
1870
1871
Jakob Juelicha94efe62014-09-18 16:02:49 -07001872 def sanity_check_update_from_shard(self, shard, updated_serialized,
1873 job_ids_sent):
1874 if self.job_id not in job_ids_sent:
Aviv Keshet6dec0e12017-04-17 15:23:33 -07001875 raise error.IgnorableUnallowedRecordsSentToMaster(
Jakob Juelicha94efe62014-09-18 16:02:49 -07001876 'Sent HostQueueEntry without corresponding '
1877 'job entry: %s' % updated_serialized)
1878
1879
showardeaa408e2009-09-11 18:45:31 +00001880 Status = host_queue_entry_states.Status
1881 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001882 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
Laurence Goodby303d2662016-07-01 15:57:04 -07001883 PRE_JOB_STATUSES = host_queue_entry_states.PRE_JOB_STATUSES
1884 IDLE_PRE_JOB_STATUSES = host_queue_entry_states.IDLE_PRE_JOB_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001885
jadmanski0afbb632008-06-06 21:10:57 +00001886 job = dbmodels.ForeignKey(Job)
1887 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001888 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001889 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1890 db_column='meta_host')
1891 active = dbmodels.BooleanField(default=False)
1892 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001893 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001894 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1895 default='')
showard89f84db2009-03-12 20:39:13 +00001896 # If atomic_group is set, this is a virtual HostQueueEntry that will
1897 # be expanded into many actual hosts within the group at schedule time.
1898 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001899 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001900 started_on = dbmodels.DateTimeField(null=True, blank=True)
Fang Deng51599032014-06-23 17:24:27 -07001901 finished_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001902
jadmanski0afbb632008-06-06 21:10:57 +00001903 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001904
mblighe8819cd2008-02-15 16:48:40 +00001905
showard2bab8f42008-11-12 18:15:22 +00001906 def __init__(self, *args, **kwargs):
1907 super(HostQueueEntry, self).__init__(*args, **kwargs)
1908 self._record_attributes(['status'])
1909
1910
showard29f7cd22009-04-29 21:16:24 +00001911 @classmethod
Allen Li204eac12017-01-30 18:31:26 -08001912 def create(cls, job, host=None, meta_host=None,
showard29f7cd22009-04-29 21:16:24 +00001913 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001914 """Creates a new host queue entry.
1915
1916 @param cls: Implicit class object.
1917 @param job: The associated job.
1918 @param host: The associated host.
1919 @param meta_host: The associated meta host.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001920 @param is_template: Whether the status should be "Template".
1921 """
showard29f7cd22009-04-29 21:16:24 +00001922 if is_template:
1923 status = cls.Status.TEMPLATE
1924 else:
1925 status = cls.Status.QUEUED
1926
Allen Li204eac12017-01-30 18:31:26 -08001927 return cls(job=job, host=host, meta_host=meta_host, status=status)
showard29f7cd22009-04-29 21:16:24 +00001928
1929
showarda5288b42009-07-28 20:06:08 +00001930 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001931 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001932 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001933 self._check_for_updated_attributes()
1934
1935
showardc0ac3a72009-07-08 21:14:45 +00001936 def execution_path(self):
1937 """
1938 Path to this entry's results (relative to the base results directory).
1939 """
MK Ryu0c1a37d2015-04-30 12:00:55 -07001940 return server_utils.get_hqe_exec_path(self.job.tag(),
1941 self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001942
1943
showard3f15eed2008-11-14 22:40:48 +00001944 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001945 """Returns the first non-None name found in priority order.
1946
Allen Li204eac12017-01-30 18:31:26 -08001947 The priority order checked is: (1) host name; (2) meta host name
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001948 """
showard3f15eed2008-11-14 22:40:48 +00001949 if self.host:
1950 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001951 else:
Allen Li204eac12017-01-30 18:31:26 -08001952 assert self.meta_host
1953 return self.meta_host.name
showard3f15eed2008-11-14 22:40:48 +00001954
1955
showard2bab8f42008-11-12 18:15:22 +00001956 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001957 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001958 self.active, self.complete = True, False
1959 elif self.status in self.COMPLETE_STATUSES:
1960 self.active, self.complete = False, True
1961 else:
1962 self.active, self.complete = False, False
1963
1964
1965 def on_attribute_changed(self, attribute, old_value):
1966 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001967 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1968 self.status)
showard2bab8f42008-11-12 18:15:22 +00001969
1970
jadmanski0afbb632008-06-06 21:10:57 +00001971 def is_meta_host_entry(self):
1972 'True if this is a entry has a meta_host instead of a host.'
1973 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001974
showarda3ab0d52008-11-03 19:03:47 +00001975
Simran Basic1b26762013-06-26 14:23:21 -07001976 # This code is shared between rpc_interface and models.HostQueueEntry.
1977 # Sadly due to circular imports between the 2 (crbug.com/230100) making it
1978 # a class method was the best way to refactor it. Attempting to put it in
1979 # rpc_utils or a new utils module failed as that would require us to import
1980 # models.py but to call it from here we would have to import the utils.py
1981 # thus creating a cycle.
1982 @classmethod
1983 def abort_host_queue_entries(cls, host_queue_entries):
1984 """Aborts a collection of host_queue_entries.
1985
1986 Abort these host queue entry and all host queue entries of jobs created
1987 by them.
1988
1989 @param host_queue_entries: List of host queue entries we want to abort.
1990 """
1991 # This isn't completely immune to race conditions since it's not atomic,
1992 # but it should be safe given the scheduler's behavior.
1993
1994 # TODO(milleral): crbug.com/230100
1995 # The |abort_host_queue_entries| rpc does nearly exactly this,
1996 # however, trying to re-use the code generates some horrible
1997 # circular import error. I'd be nice to refactor things around
1998 # sometime so the code could be reused.
1999
2000 # Fixpoint algorithm to find the whole tree of HQEs to abort to
2001 # minimize the total number of database queries:
2002 children = set()
2003 new_children = set(host_queue_entries)
2004 while new_children:
2005 children.update(new_children)
2006 new_child_ids = [hqe.job_id for hqe in new_children]
2007 new_children = HostQueueEntry.objects.filter(
2008 job__parent_job__in=new_child_ids,
2009 complete=False, aborted=False).all()
2010 # To handle circular parental relationships
2011 new_children = set(new_children) - children
2012
2013 # Associate a user with the host queue entries that we're about
2014 # to abort so that we can look up who to blame for the aborts.
2015 now = datetime.now()
2016 user = User.current_user()
2017 aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
2018 aborted_by=user, aborted_on=now) for hqe in children]
2019 AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
2020 # Bulk update all of the HQEs to set the abort bit.
2021 child_ids = [hqe.id for hqe in children]
2022 HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
2023
2024
Scott Zawalski23041432013-04-17 07:39:09 -07002025 def abort(self):
Alex Millerdea67042013-04-22 17:23:34 -07002026 """ Aborts this host queue entry.
Simran Basic1b26762013-06-26 14:23:21 -07002027
Alex Millerdea67042013-04-22 17:23:34 -07002028 Abort this host queue entry and all host queue entries of jobs created by
2029 this one.
2030
2031 """
showardd3dc1992009-04-22 21:01:40 +00002032 if not self.complete and not self.aborted:
Simran Basi97582a22013-06-27 12:03:21 -07002033 HostQueueEntry.abort_host_queue_entries([self])
mblighe8819cd2008-02-15 16:48:40 +00002034
showardd3dc1992009-04-22 21:01:40 +00002035
2036 @classmethod
2037 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002038 """Returns a modified status msg if the host queue entry was aborted.
2039
2040 @param cls: Implicit class object.
2041 @param status: The original status message.
2042 @param aborted: Whether the host queue entry was aborted.
2043 @param complete: Whether the host queue entry was completed.
2044 """
showardd3dc1992009-04-22 21:01:40 +00002045 if aborted and not complete:
2046 return 'Aborted (%s)' % status
2047 return status
2048
2049
2050 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002051 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00002052 return self.compute_full_status(self.status, self.aborted,
2053 self.complete)
2054
2055
2056 def _postprocess_object_dict(self, object_dict):
2057 object_dict['full_status'] = self.full_status()
2058
2059
jadmanski0afbb632008-06-06 21:10:57 +00002060 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002061 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00002062 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00002063
showard12f3e322009-05-13 21:27:42 +00002064
showard4c119042008-09-29 19:16:18 +00002065
showarda5288b42009-07-28 20:06:08 +00002066 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00002067 hostname = None
2068 if self.host:
2069 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00002070 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00002071
2072
Paul Hobbs4621bf12017-05-11 17:42:02 -07002073class HostQueueEntryStartTimes(dbmodels.Model):
2074 """An auxilary table to HostQueueEntry to index by start time."""
Paul Hobbs69de2fa2017-06-07 14:59:33 -07002075 insert_time = dbmodels.DateTimeField()
Paul Hobbs4621bf12017-05-11 17:42:02 -07002076 highest_hqe_id = dbmodels.IntegerField()
2077
2078 class Meta:
2079 """Metadata for class HostQueueEntryStartTimes."""
Paul Hobbs69de2fa2017-06-07 14:59:33 -07002080 db_table = 'afe_host_queue_entry_start_times'
Paul Hobbs4621bf12017-05-11 17:42:02 -07002081
2082
showard4c119042008-09-29 19:16:18 +00002083class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002084 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00002085 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
2086 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00002087 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00002088
2089 objects = model_logic.ExtendedManager()
2090
showard68c7aa02008-10-09 16:49:11 +00002091
showarda5288b42009-07-28 20:06:08 +00002092 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00002093 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00002094 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00002095
showard4c119042008-09-29 19:16:18 +00002096 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002097 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00002098 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00002099
2100
showard6d7b2ff2009-06-10 00:16:47 +00002101class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
2102 """\
2103 Tasks to run on hosts at the next time they are in the Ready state. Use this
2104 for high-priority tasks, such as forced repair or forced reinstall.
2105
2106 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00002107 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00002108 time_requested: date and time the request for this task was made
2109 is_active: task is currently running
2110 is_complete: task has finished running
beeps8bb1f7d2013-08-05 01:30:09 -07002111 is_aborted: task was aborted
showard2fe3f1d2009-07-06 20:19:11 +00002112 time_started: date and time the task started
Dan Shid0725542014-06-23 15:34:27 -07002113 time_finished: date and time the task finished
showard2fe3f1d2009-07-06 20:19:11 +00002114 queue_entry: Host queue entry waiting on this task (or None, if task was not
2115 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00002116 """
Alex Millerdfff2fd2013-05-28 13:05:06 -07002117 Task = enum.Enum('Verify', 'Cleanup', 'Repair', 'Reset', 'Provision',
Dan Shi07e09af2013-04-12 09:31:29 -07002118 string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00002119
2120 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00002121 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00002122 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00002123 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00002124 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
2125 null=False)
2126 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
2127 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
beeps8bb1f7d2013-08-05 01:30:09 -07002128 is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00002129 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00002130 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00002131 success = dbmodels.BooleanField(default=False, blank=False, null=False)
Dan Shid0725542014-06-23 15:34:27 -07002132 time_finished = dbmodels.DateTimeField(null=True, blank=True)
showard6d7b2ff2009-06-10 00:16:47 +00002133
2134 objects = model_logic.ExtendedManager()
2135
2136
showard9bb960b2009-11-19 01:02:11 +00002137 def save(self, **kwargs):
2138 if self.queue_entry:
2139 self.requested_by = User.objects.get(
2140 login=self.queue_entry.job.owner)
2141 super(SpecialTask, self).save(**kwargs)
2142
2143
showarded2afea2009-07-07 20:54:07 +00002144 def execution_path(self):
MK Ryu0c1a37d2015-04-30 12:00:55 -07002145 """Returns the execution path for a special task."""
2146 return server_utils.get_special_task_exec_path(
2147 self.host.hostname, self.id, self.task, self.time_requested)
showarded2afea2009-07-07 20:54:07 +00002148
2149
showardc0ac3a72009-07-08 21:14:45 +00002150 # property to emulate HostQueueEntry.status
2151 @property
2152 def status(self):
MK Ryu0c1a37d2015-04-30 12:00:55 -07002153 """Returns a host queue entry status appropriate for a speical task."""
2154 return server_utils.get_special_task_status(
2155 self.is_complete, self.success, self.is_active)
showardc0ac3a72009-07-08 21:14:45 +00002156
2157
2158 # property to emulate HostQueueEntry.started_on
2159 @property
2160 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002161 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00002162 return self.time_started
2163
2164
showard6d7b2ff2009-06-10 00:16:47 +00002165 @classmethod
showardc5103442010-01-15 00:20:26 +00002166 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002167 """Schedules a special task on a host if not already scheduled.
2168
2169 @param cls: Implicit class object.
2170 @param host: The host to use.
2171 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00002172 """
showardc5103442010-01-15 00:20:26 +00002173 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
2174 is_active=False,
2175 is_complete=False)
2176 if existing_tasks:
2177 return existing_tasks[0]
2178
2179 special_task = SpecialTask(host=host, task=task,
2180 requested_by=User.current_user())
2181 special_task.save()
2182 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00002183
2184
beeps8bb1f7d2013-08-05 01:30:09 -07002185 def abort(self):
2186 """ Abort this special task."""
2187 self.is_aborted = True
2188 self.save()
2189
2190
showarded2afea2009-07-07 20:54:07 +00002191 def activate(self):
showard474d1362009-08-20 23:32:01 +00002192 """
2193 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00002194 """
showard97446882009-07-20 22:37:28 +00002195 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00002196 self.is_active = True
2197 self.time_started = datetime.now()
2198 self.save()
2199
2200
showarde60e44e2009-11-13 20:45:38 +00002201 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002202 """Sets a task as completed.
2203
2204 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00002205 """
showard97446882009-07-20 22:37:28 +00002206 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00002207 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00002208 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00002209 self.success = success
Dan Shid85d6112014-07-14 10:32:55 -07002210 if self.time_started:
2211 self.time_finished = datetime.now()
showard2fe3f1d2009-07-06 20:19:11 +00002212 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00002213
2214
2215 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002216 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00002217 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00002218
showard474d1362009-08-20 23:32:01 +00002219
showarda5288b42009-07-28 20:06:08 +00002220 def __unicode__(self):
2221 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00002222 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00002223 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00002224 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00002225 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00002226 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00002227
2228 return result
Dan Shi6964fa52014-12-18 11:04:27 -08002229
2230
2231class StableVersion(dbmodels.Model, model_logic.ModelExtensions):
2232
2233 board = dbmodels.CharField(max_length=255, unique=True)
2234 version = dbmodels.CharField(max_length=255)
2235
2236 class Meta:
2237 """Metadata for class StableVersion."""
Fang Deng86248502014-12-18 16:38:00 -08002238 db_table = 'afe_stable_versions'