blob: 418e9850410edc055c244834016b1c7933f78223 [file] [log] [blame]
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001# pylint: disable-msg=C0111
2
showardd1195652009-12-08 22:21:02 +00003import logging, os
showardfb2a7fa2008-07-17 17:04:12 +00004from datetime import datetime
Aviv Keshetfa199002013-05-09 13:31:46 -07005import django.core
6try:
7 from django.db import models as dbmodels, connection
8except django.core.exceptions.ImproperlyConfigured:
9 raise ImportError('Django database not yet configured. Import either '
10 'setup_django_environment or '
11 'setup_django_lite_environment from '
12 'autotest_lib.frontend before any imports that '
13 'depend on django models.')
jamesren35a70222010-02-16 19:30:46 +000014from xml.sax import saxutils
showardcafd16e2009-05-29 18:37:49 +000015import common
jamesrendd855242010-03-02 22:23:44 +000016from autotest_lib.frontend.afe import model_logic, model_attributes
Prashanth B489b91d2014-03-15 12:17:16 -070017from autotest_lib.frontend.afe import rdb_model_extensions
showardcafd16e2009-05-29 18:37:49 +000018from autotest_lib.frontend import settings, thread_local
Jakob Juelicha94efe62014-09-18 16:02:49 -070019from autotest_lib.client.common_lib import enum, error, host_protections
20from autotest_lib.client.common_lib import global_config
showardeaa408e2009-09-11 18:45:31 +000021from autotest_lib.client.common_lib import host_queue_entry_states
Jakob Juelicha94efe62014-09-18 16:02:49 -070022from autotest_lib.client.common_lib import control_data, priorities, decorators
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -080023from autotest_lib.client.common_lib import site_utils
Gabe Blackb72f4fb2015-01-20 16:47:13 -080024from autotest_lib.client.common_lib.cros.graphite import autotest_es
MK Ryu0c1a37d2015-04-30 12:00:55 -070025from autotest_lib.server import utils as server_utils
mblighe8819cd2008-02-15 16:48:40 +000026
showard0fc38302008-10-23 00:44:07 +000027# job options and user preferences
jamesrendd855242010-03-02 22:23:44 +000028DEFAULT_REBOOT_BEFORE = model_attributes.RebootBefore.IF_DIRTY
Dan Shi07e09af2013-04-12 09:31:29 -070029DEFAULT_REBOOT_AFTER = model_attributes.RebootBefore.NEVER
mblighe8819cd2008-02-15 16:48:40 +000030
showard89f84db2009-03-12 20:39:13 +000031
mblighe8819cd2008-02-15 16:48:40 +000032class AclAccessViolation(Exception):
jadmanski0afbb632008-06-06 21:10:57 +000033 """\
34 Raised when an operation is attempted with proper permissions as
35 dictated by ACLs.
36 """
mblighe8819cd2008-02-15 16:48:40 +000037
38
showard205fd602009-03-21 00:17:35 +000039class AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model):
showard89f84db2009-03-12 20:39:13 +000040 """\
41 An atomic group defines a collection of hosts which must only be scheduled
42 all at once. Any host with a label having an atomic group will only be
43 scheduled for a job at the same time as other hosts sharing that label.
44
45 Required:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080046 name: A name for this atomic group, e.g. 'rack23' or 'funky_net'.
showard89f84db2009-03-12 20:39:13 +000047 max_number_of_machines: The maximum number of machines that will be
48 scheduled at once when scheduling jobs to this atomic group.
49 The job.synch_count is considered the minimum.
50
51 Optional:
52 description: Arbitrary text description of this group's purpose.
53 """
showarda5288b42009-07-28 20:06:08 +000054 name = dbmodels.CharField(max_length=255, unique=True)
showard89f84db2009-03-12 20:39:13 +000055 description = dbmodels.TextField(blank=True)
showarde9450c92009-06-30 01:58:52 +000056 # This magic value is the default to simplify the scheduler logic.
57 # It must be "large". The common use of atomic groups is to want all
58 # machines in the group to be used, limits on which subset used are
59 # often chosen via dependency labels.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080060 # TODO(dennisjeffrey): Revisit this so we don't have to assume that
61 # "infinity" is around 3.3 million.
showarde9450c92009-06-30 01:58:52 +000062 INFINITE_MACHINES = 333333333
63 max_number_of_machines = dbmodels.IntegerField(default=INFINITE_MACHINES)
showard205fd602009-03-21 00:17:35 +000064 invalid = dbmodels.BooleanField(default=False,
showarda5288b42009-07-28 20:06:08 +000065 editable=settings.FULL_ADMIN)
showard89f84db2009-03-12 20:39:13 +000066
showard89f84db2009-03-12 20:39:13 +000067 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +000068 objects = model_logic.ModelWithInvalidManager()
showard205fd602009-03-21 00:17:35 +000069 valid_objects = model_logic.ValidObjectsManager()
70
71
showard29f7cd22009-04-29 21:16:24 +000072 def enqueue_job(self, job, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080073 """Enqueue a job on an associated atomic group of hosts.
74
75 @param job: A job to enqueue.
76 @param is_template: Whether the status should be "Template".
77 """
showard29f7cd22009-04-29 21:16:24 +000078 queue_entry = HostQueueEntry.create(atomic_group=self, job=job,
79 is_template=is_template)
showardc92da832009-04-07 18:14:34 +000080 queue_entry.save()
81
82
showard205fd602009-03-21 00:17:35 +000083 def clean_object(self):
84 self.label_set.clear()
showard89f84db2009-03-12 20:39:13 +000085
86
87 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080088 """Metadata for class AtomicGroup."""
showardeab66ce2009-12-23 00:03:56 +000089 db_table = 'afe_atomic_groups'
showard89f84db2009-03-12 20:39:13 +000090
showard205fd602009-03-21 00:17:35 +000091
showarda5288b42009-07-28 20:06:08 +000092 def __unicode__(self):
93 return unicode(self.name)
showard89f84db2009-03-12 20:39:13 +000094
95
showard7c785282008-05-29 19:45:12 +000096class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000097 """\
98 Required:
showard89f84db2009-03-12 20:39:13 +000099 name: label name
mblighe8819cd2008-02-15 16:48:40 +0000100
jadmanski0afbb632008-06-06 21:10:57 +0000101 Optional:
showard89f84db2009-03-12 20:39:13 +0000102 kernel_config: URL/path to kernel config for jobs run on this label.
103 platform: If True, this is a platform label (defaults to False).
104 only_if_needed: If True, a Host with this label can only be used if that
105 label is requested by the job/test (either as the meta_host or
106 in the job_dependencies).
107 atomic_group: The atomic group associated with this label.
jadmanski0afbb632008-06-06 21:10:57 +0000108 """
showarda5288b42009-07-28 20:06:08 +0000109 name = dbmodels.CharField(max_length=255, unique=True)
110 kernel_config = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000111 platform = dbmodels.BooleanField(default=False)
112 invalid = dbmodels.BooleanField(default=False,
113 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +0000114 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +0000115
jadmanski0afbb632008-06-06 21:10:57 +0000116 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +0000117 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000118 valid_objects = model_logic.ValidObjectsManager()
showard89f84db2009-03-12 20:39:13 +0000119 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
120
mbligh5244cbb2008-04-24 20:39:52 +0000121
jadmanski0afbb632008-06-06 21:10:57 +0000122 def clean_object(self):
123 self.host_set.clear()
showard01a51672009-05-29 18:42:37 +0000124 self.test_set.clear()
mblighe8819cd2008-02-15 16:48:40 +0000125
126
showard29f7cd22009-04-29 21:16:24 +0000127 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800128 """Enqueue a job on any host of this label.
129
130 @param job: A job to enqueue.
131 @param atomic_group: The associated atomic group.
132 @param is_template: Whether the status should be "Template".
133 """
showard29f7cd22009-04-29 21:16:24 +0000134 queue_entry = HostQueueEntry.create(meta_host=self, job=job,
135 is_template=is_template,
136 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000137 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000138
139
Fang Dengff361592015-02-02 15:27:34 -0800140
jadmanski0afbb632008-06-06 21:10:57 +0000141 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800142 """Metadata for class Label."""
showardeab66ce2009-12-23 00:03:56 +0000143 db_table = 'afe_labels'
mblighe8819cd2008-02-15 16:48:40 +0000144
Fang Dengff361592015-02-02 15:27:34 -0800145
showarda5288b42009-07-28 20:06:08 +0000146 def __unicode__(self):
147 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000148
149
Jakob Jülich92c06332014-08-25 19:06:57 +0000150class Shard(dbmodels.Model, model_logic.ModelExtensions):
151
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700152 hostname = dbmodels.CharField(max_length=255, unique=True)
153
154 name_field = 'hostname'
155
Jakob Jülich92c06332014-08-25 19:06:57 +0000156 labels = dbmodels.ManyToManyField(Label, blank=True,
157 db_table='afe_shards_labels')
158
159 class Meta:
160 """Metadata for class ParameterizedJob."""
161 db_table = 'afe_shards'
162
163
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -0800164 def rpc_hostname(self):
165 """Get the rpc hostname of the shard.
166
167 @return: Just the shard hostname for all non-testing environments.
168 The address of the default gateway for vm testing environments.
169 """
170 # TODO: Figure out a better solution for testing. Since no 2 shards
171 # can run on the same host, if the shard hostname is localhost we
172 # conclude that it must be a vm in a test cluster. In such situations
173 # a name of localhost:<port> is necessary to achieve the correct
174 # afe links/redirection from the frontend (this happens through the
175 # host), but for rpcs that are performed *on* the shard, they need to
176 # use the address of the gateway.
MK Ryu8f8cdb42015-05-11 17:41:14 -0700177 # In the virtual machine testing environment (i.e., puppylab), each
178 # shard VM has a hostname like localhost:<port>. In the real cluster
179 # environment, a shard node does not have 'localhost' for its hostname.
180 # The following hostname substitution is needed only for the VM
181 # in puppylab.
182 # The 'hostname' should not be replaced in the case of real cluster.
183 if site_utils.is_puppylab_vm(self.hostname):
184 hostname = self.hostname.split(':')[0]
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -0800185 return self.hostname.replace(
186 hostname, site_utils.DEFAULT_VM_GATEWAY)
187 return self.hostname
188
189
jamesren76fcf192010-04-21 20:39:50 +0000190class Drone(dbmodels.Model, model_logic.ModelExtensions):
191 """
192 A scheduler drone
193
194 hostname: the drone's hostname
195 """
196 hostname = dbmodels.CharField(max_length=255, unique=True)
197
198 name_field = 'hostname'
199 objects = model_logic.ExtendedManager()
200
201
202 def save(self, *args, **kwargs):
203 if not User.current_user().is_superuser():
204 raise Exception('Only superusers may edit drones')
205 super(Drone, self).save(*args, **kwargs)
206
207
208 def delete(self):
209 if not User.current_user().is_superuser():
210 raise Exception('Only superusers may delete drones')
211 super(Drone, self).delete()
212
213
214 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800215 """Metadata for class Drone."""
jamesren76fcf192010-04-21 20:39:50 +0000216 db_table = 'afe_drones'
217
218 def __unicode__(self):
219 return unicode(self.hostname)
220
221
222class DroneSet(dbmodels.Model, model_logic.ModelExtensions):
223 """
224 A set of scheduler drones
225
226 These will be used by the scheduler to decide what drones a job is allowed
227 to run on.
228
229 name: the drone set's name
230 drones: the drones that are part of the set
231 """
232 DRONE_SETS_ENABLED = global_config.global_config.get_config_value(
233 'SCHEDULER', 'drone_sets_enabled', type=bool, default=False)
234 DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value(
235 'SCHEDULER', 'default_drone_set_name', default=None)
236
237 name = dbmodels.CharField(max_length=255, unique=True)
238 drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones')
239
240 name_field = 'name'
241 objects = model_logic.ExtendedManager()
242
243
244 def save(self, *args, **kwargs):
245 if not User.current_user().is_superuser():
246 raise Exception('Only superusers may edit drone sets')
247 super(DroneSet, self).save(*args, **kwargs)
248
249
250 def delete(self):
251 if not User.current_user().is_superuser():
252 raise Exception('Only superusers may delete drone sets')
253 super(DroneSet, self).delete()
254
255
256 @classmethod
257 def drone_sets_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800258 """Returns whether drone sets are enabled.
259
260 @param cls: Implicit class object.
261 """
jamesren76fcf192010-04-21 20:39:50 +0000262 return cls.DRONE_SETS_ENABLED
263
264
265 @classmethod
266 def default_drone_set_name(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800267 """Returns the default drone set name.
268
269 @param cls: Implicit class object.
270 """
jamesren76fcf192010-04-21 20:39:50 +0000271 return cls.DEFAULT_DRONE_SET_NAME
272
273
274 @classmethod
275 def get_default(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800276 """Gets the default drone set name, compatible with Job.add_object.
277
278 @param cls: Implicit class object.
279 """
jamesren76fcf192010-04-21 20:39:50 +0000280 return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME)
281
282
283 @classmethod
284 def resolve_name(cls, drone_set_name):
285 """
286 Returns the name of one of these, if not None, in order of preference:
287 1) the drone set given,
288 2) the current user's default drone set, or
289 3) the global default drone set
290
291 or returns None if drone sets are disabled
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800292
293 @param cls: Implicit class object.
294 @param drone_set_name: A drone set name.
jamesren76fcf192010-04-21 20:39:50 +0000295 """
296 if not cls.drone_sets_enabled():
297 return None
298
299 user = User.current_user()
300 user_drone_set_name = user.drone_set and user.drone_set.name
301
302 return drone_set_name or user_drone_set_name or cls.get_default().name
303
304
305 def get_drone_hostnames(self):
306 """
307 Gets the hostnames of all drones in this drone set
308 """
309 return set(self.drones.all().values_list('hostname', flat=True))
310
311
312 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800313 """Metadata for class DroneSet."""
jamesren76fcf192010-04-21 20:39:50 +0000314 db_table = 'afe_drone_sets'
315
316 def __unicode__(self):
317 return unicode(self.name)
318
319
showardfb2a7fa2008-07-17 17:04:12 +0000320class User(dbmodels.Model, model_logic.ModelExtensions):
321 """\
322 Required:
323 login :user login name
324
325 Optional:
326 access_level: 0=User (default), 1=Admin, 100=Root
327 """
328 ACCESS_ROOT = 100
329 ACCESS_ADMIN = 1
330 ACCESS_USER = 0
331
showard64a95952010-01-13 21:27:16 +0000332 AUTOTEST_SYSTEM = 'autotest_system'
333
showarda5288b42009-07-28 20:06:08 +0000334 login = dbmodels.CharField(max_length=255, unique=True)
showardfb2a7fa2008-07-17 17:04:12 +0000335 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
336
showard0fc38302008-10-23 00:44:07 +0000337 # user preferences
jamesrendd855242010-03-02 22:23:44 +0000338 reboot_before = dbmodels.SmallIntegerField(
339 choices=model_attributes.RebootBefore.choices(), blank=True,
340 default=DEFAULT_REBOOT_BEFORE)
341 reboot_after = dbmodels.SmallIntegerField(
342 choices=model_attributes.RebootAfter.choices(), blank=True,
343 default=DEFAULT_REBOOT_AFTER)
jamesren76fcf192010-04-21 20:39:50 +0000344 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
showard97db5ba2008-11-12 18:18:02 +0000345 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000346
showardfb2a7fa2008-07-17 17:04:12 +0000347 name_field = 'login'
348 objects = model_logic.ExtendedManager()
349
350
showarda5288b42009-07-28 20:06:08 +0000351 def save(self, *args, **kwargs):
showardfb2a7fa2008-07-17 17:04:12 +0000352 # is this a new object being saved for the first time?
353 first_time = (self.id is None)
354 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000355 if user and not user.is_superuser() and user.login != self.login:
356 raise AclAccessViolation("You cannot modify user " + self.login)
showarda5288b42009-07-28 20:06:08 +0000357 super(User, self).save(*args, **kwargs)
showardfb2a7fa2008-07-17 17:04:12 +0000358 if first_time:
359 everyone = AclGroup.objects.get(name='Everyone')
360 everyone.users.add(self)
361
362
363 def is_superuser(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800364 """Returns whether the user has superuser access."""
showardfb2a7fa2008-07-17 17:04:12 +0000365 return self.access_level >= self.ACCESS_ROOT
366
367
showard64a95952010-01-13 21:27:16 +0000368 @classmethod
369 def current_user(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800370 """Returns the current user.
371
372 @param cls: Implicit class object.
373 """
showard64a95952010-01-13 21:27:16 +0000374 user = thread_local.get_user()
375 if user is None:
showardcfcdd802010-01-15 00:16:33 +0000376 user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM)
showard64a95952010-01-13 21:27:16 +0000377 user.access_level = cls.ACCESS_ROOT
378 user.save()
379 return user
380
381
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -0800382 @classmethod
383 def get_record(cls, data):
384 """Check the database for an identical record.
385
386 Check for a record with matching id and login. If one exists,
387 return it. If one does not exist there is a possibility that
388 the following cases have happened:
389 1. Same id, different login
390 We received: "1 chromeos-test"
391 And we have: "1 debug-user"
392 In this case we need to delete "1 debug_user" and insert
393 "1 chromeos-test".
394
395 2. Same login, different id:
396 We received: "1 chromeos-test"
397 And we have: "2 chromeos-test"
398 In this case we need to delete "2 chromeos-test" and insert
399 "1 chromeos-test".
400
401 As long as this method deletes bad records and raises the
402 DoesNotExist exception the caller will handle creating the
403 new record.
404
405 @raises: DoesNotExist, if a record with the matching login and id
406 does not exist.
407 """
408
409 # Both the id and login should be uniqe but there are cases when
410 # we might already have a user with the same login/id because
411 # current_user will proactively create a user record if it doesn't
412 # exist. Since we want to avoid conflict between the master and
413 # shard, just delete any existing user records that don't match
414 # what we're about to deserialize from the master.
415 try:
416 return cls.objects.get(login=data['login'], id=data['id'])
417 except cls.DoesNotExist:
418 cls.delete_matching_record(login=data['login'])
419 cls.delete_matching_record(id=data['id'])
420 raise
421
422
showardfb2a7fa2008-07-17 17:04:12 +0000423 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800424 """Metadata for class User."""
showardeab66ce2009-12-23 00:03:56 +0000425 db_table = 'afe_users'
showardfb2a7fa2008-07-17 17:04:12 +0000426
showarda5288b42009-07-28 20:06:08 +0000427 def __unicode__(self):
428 return unicode(self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000429
430
Prashanth B489b91d2014-03-15 12:17:16 -0700431class Host(model_logic.ModelWithInvalid, rdb_model_extensions.AbstractHostModel,
showardf8b19042009-05-12 17:22:49 +0000432 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000433 """\
434 Required:
435 hostname
mblighe8819cd2008-02-15 16:48:40 +0000436
jadmanski0afbb632008-06-06 21:10:57 +0000437 optional:
showard21baa452008-10-21 00:08:39 +0000438 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000439
jadmanski0afbb632008-06-06 21:10:57 +0000440 Internal:
Prashanth B489b91d2014-03-15 12:17:16 -0700441 From AbstractHostModel:
442 synch_id: currently unused
443 status: string describing status of host
444 invalid: true if the host has been deleted
445 protection: indicates what can be done to this host during repair
446 lock_time: DateTime at which the host was locked
447 dirty: true if the host has been used without being rebooted
448 Local:
449 locked_by: user that locked the host, or null if the host is unlocked
jadmanski0afbb632008-06-06 21:10:57 +0000450 """
mblighe8819cd2008-02-15 16:48:40 +0000451
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700452 SERIALIZATION_LINKS_TO_FOLLOW = set(['aclgroup_set',
453 'hostattribute_set',
454 'labels',
455 'shard'])
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800456 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['invalid'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700457
Jakob Juelichf88fa932014-09-03 17:58:04 -0700458
459 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700460 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -0700461 self.shard = Shard.deserialize(data)
462
463
Prashanth B489b91d2014-03-15 12:17:16 -0700464 # Note: Only specify foreign keys here, specify all native host columns in
465 # rdb_model_extensions instead.
466 Protection = host_protections.Protection
showardeab66ce2009-12-23 00:03:56 +0000467 labels = dbmodels.ManyToManyField(Label, blank=True,
468 db_table='afe_hosts_labels')
showardfb2a7fa2008-07-17 17:04:12 +0000469 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
jadmanski0afbb632008-06-06 21:10:57 +0000470 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000471 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000472 valid_objects = model_logic.ValidObjectsManager()
beepscc9fc702013-12-02 12:45:38 -0800473 leased_objects = model_logic.LeasedHostManager()
mbligh5244cbb2008-04-24 20:39:52 +0000474
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700475 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
showard2bab8f42008-11-12 18:15:22 +0000476
477 def __init__(self, *args, **kwargs):
478 super(Host, self).__init__(*args, **kwargs)
479 self._record_attributes(['status'])
480
481
showardb8471e32008-07-03 19:51:08 +0000482 @staticmethod
483 def create_one_time_host(hostname):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800484 """Creates a one-time host.
485
486 @param hostname: The name for the host.
487 """
showardb8471e32008-07-03 19:51:08 +0000488 query = Host.objects.filter(hostname=hostname)
489 if query.count() == 0:
490 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000491 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000492 else:
493 host = query[0]
494 if not host.invalid:
495 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000496 'hostname' : '%s already exists in the autotest DB. '
497 'Select it rather than entering it as a one time '
498 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000499 })
showard1ab512b2008-07-30 23:39:04 +0000500 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000501 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000502 host.save()
showard2924b0a2009-06-18 23:16:15 +0000503 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000504 return host
mbligh5244cbb2008-04-24 20:39:52 +0000505
showard1ff7b2e2009-05-15 23:17:18 +0000506
Jakob Juelich59cfe542014-09-02 16:37:46 -0700507 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -0700508 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -0700509 """Assigns hosts to a shard.
510
MK Ryu638a6cf2015-05-08 14:49:43 -0700511 For all labels that have been assigned to a shard, all hosts that
512 have at least one of the shard's labels are assigned to the shard.
Jakob Juelich1b525742014-09-30 13:08:07 -0700513 Hosts that are assigned to the shard but aren't already present on the
514 shard are returned.
Jakob Juelich59cfe542014-09-02 16:37:46 -0700515
MK Ryu638a6cf2015-05-08 14:49:43 -0700516 Board to shard mapping is many-to-one. Many different boards can be
517 hosted in a shard. However, DUTs of a single board cannot be distributed
518 into more than one shard.
519
Jakob Juelich59cfe542014-09-02 16:37:46 -0700520 @param shard: The shard object to assign labels/hosts for.
Jakob Juelich1b525742014-09-30 13:08:07 -0700521 @param known_ids: List of all host-ids the shard already knows.
522 This is used to figure out which hosts should be sent
523 to the shard. If shard_ids were used instead, hosts
524 would only be transferred once, even if the client
525 failed persisting them.
526 The number of hosts usually lies in O(100), so the
527 overhead is acceptable.
528
Jakob Juelich59cfe542014-09-02 16:37:46 -0700529 @returns the hosts objects that should be sent to the shard.
530 """
531
532 # Disclaimer: concurrent heartbeats should theoretically not occur in
533 # the current setup. As they may be introduced in the near future,
534 # this comment will be left here.
535
536 # Sending stuff twice is acceptable, but forgetting something isn't.
537 # Detecting duplicates on the client is easy, but here it's harder. The
538 # following options were considered:
539 # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
540 # than select returned, as concurrently more hosts might have been
541 # inserted
542 # - UPDATE and then SELECT WHERE shard=shard: select always returns all
543 # hosts for the shard, this is overhead
544 # - SELECT and then UPDATE only selected without requerying afterwards:
545 # returns the old state of the records.
MK Ryu638a6cf2015-05-08 14:49:43 -0700546 host_ids = set(Host.objects.filter(
547 labels__in=shard.labels.all(),
Jakob Juelich59cfe542014-09-02 16:37:46 -0700548 leased=False
Jakob Juelich1b525742014-09-30 13:08:07 -0700549 ).exclude(
550 id__in=known_ids,
Jakob Juelich59cfe542014-09-02 16:37:46 -0700551 ).values_list('pk', flat=True))
552
553 if host_ids:
554 Host.objects.filter(pk__in=host_ids).update(shard=shard)
555 return list(Host.objects.filter(pk__in=host_ids).all())
556 return []
557
showardafd97de2009-10-01 18:45:09 +0000558 def resurrect_object(self, old_object):
559 super(Host, self).resurrect_object(old_object)
560 # invalid hosts can be in use by the scheduler (as one-time hosts), so
561 # don't change the status
562 self.status = old_object.status
563
564
jadmanski0afbb632008-06-06 21:10:57 +0000565 def clean_object(self):
566 self.aclgroup_set.clear()
567 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000568
569
Dan Shia0acfbc2014-10-14 15:56:23 -0700570 def record_state(self, type_str, state, value, other_metadata=None):
571 """Record metadata in elasticsearch.
572
573 @param type_str: sets the _type field in elasticsearch db.
574 @param state: string representing what state we are recording,
575 e.g. 'locked'
576 @param value: value of the state, e.g. True
577 @param other_metadata: Other metadata to store in metaDB.
578 """
579 metadata = {
580 state: value,
581 'hostname': self.hostname,
582 }
583 if other_metadata:
584 metadata = dict(metadata.items() + other_metadata.items())
Matthew Sartori68186332015-04-27 17:19:53 -0700585 autotest_es.post(use_http=True, type_str=type_str, metadata=metadata)
Dan Shia0acfbc2014-10-14 15:56:23 -0700586
587
showarda5288b42009-07-28 20:06:08 +0000588 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000589 # extra spaces in the hostname can be a sneaky source of errors
590 self.hostname = self.hostname.strip()
591 # is this a new object being saved for the first time?
592 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000593 if not first_time:
594 AclGroup.check_for_acl_violation_hosts([self])
Dan Shia0acfbc2014-10-14 15:56:23 -0700595 # If locked is changed, send its status and user made the change to
596 # metaDB. Locks are important in host history because if a device is
597 # locked then we don't really care what state it is in.
showardfb2a7fa2008-07-17 17:04:12 +0000598 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000599 self.locked_by = User.current_user()
showardfb2a7fa2008-07-17 17:04:12 +0000600 self.lock_time = datetime.now()
Dan Shia0acfbc2014-10-14 15:56:23 -0700601 self.record_state('lock_history', 'locked', self.locked,
Matthew Sartori68186332015-04-27 17:19:53 -0700602 {'changed_by': self.locked_by.login,
603 'lock_reason': self.lock_reason})
showard21baa452008-10-21 00:08:39 +0000604 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000605 elif not self.locked and self.locked_by:
Dan Shia0acfbc2014-10-14 15:56:23 -0700606 self.record_state('lock_history', 'locked', self.locked,
607 {'changed_by': self.locked_by.login})
showardfb2a7fa2008-07-17 17:04:12 +0000608 self.locked_by = None
609 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000610 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000611 if first_time:
612 everyone = AclGroup.objects.get(name='Everyone')
613 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000614 self._check_for_updated_attributes()
615
mblighe8819cd2008-02-15 16:48:40 +0000616
showardb8471e32008-07-03 19:51:08 +0000617 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000618 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000619 for queue_entry in self.hostqueueentry_set.all():
620 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000621 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000622 super(Host, self).delete()
623
mblighe8819cd2008-02-15 16:48:40 +0000624
showard2bab8f42008-11-12 18:15:22 +0000625 def on_attribute_changed(self, attribute, old_value):
626 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000627 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000628
629
showard29f7cd22009-04-29 21:16:24 +0000630 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800631 """Enqueue a job on this host.
632
633 @param job: A job to enqueue.
634 @param atomic_group: The associated atomic group.
635 @param is_template: Whther the status should be "Template".
636 """
showard29f7cd22009-04-29 21:16:24 +0000637 queue_entry = HostQueueEntry.create(host=self, job=job,
638 is_template=is_template,
639 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000640 # allow recovery of dead hosts from the frontend
641 if not self.active_queue_entry() and self.is_dead():
642 self.status = Host.Status.READY
643 self.save()
644 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000645
showard08f981b2008-06-24 21:59:03 +0000646 block = IneligibleHostQueue(job=job, host=self)
647 block.save()
648
mblighe8819cd2008-02-15 16:48:40 +0000649
jadmanski0afbb632008-06-06 21:10:57 +0000650 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800651 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000652 # TODO(showard): slighly hacky?
653 platforms = self.labels.filter(platform=True)
654 if len(platforms) == 0:
655 return None
656 return platforms[0]
657 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000658
659
showardcafd16e2009-05-29 18:37:49 +0000660 @classmethod
661 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800662 """Verify the specified hosts have no associated platforms.
663
664 @param cls: Implicit class object.
665 @param hosts: The hosts to verify.
666 @raises model_logic.ValidationError if any hosts already have a
667 platform.
668 """
showardcafd16e2009-05-29 18:37:49 +0000669 Host.objects.populate_relationships(hosts, Label, 'label_list')
670 errors = []
671 for host in hosts:
672 platforms = [label.name for label in host.label_list
673 if label.platform]
674 if platforms:
675 # do a join, just in case this host has multiple platforms,
676 # we'll be able to see it
677 errors.append('Host %s already has a platform: %s' % (
678 host.hostname, ', '.join(platforms)))
679 if errors:
680 raise model_logic.ValidationError({'labels': '; '.join(errors)})
681
682
jadmanski0afbb632008-06-06 21:10:57 +0000683 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800684 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000685 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000686
687
jadmanski0afbb632008-06-06 21:10:57 +0000688 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800689 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000690 active = list(self.hostqueueentry_set.filter(active=True))
691 if not active:
692 return None
693 assert len(active) == 1, ('More than one active entry for '
694 'host ' + self.hostname)
695 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000696
697
showardf8b19042009-05-12 17:22:49 +0000698 def _get_attribute_model_and_args(self, attribute):
699 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000700
701
Fang Dengff361592015-02-02 15:27:34 -0800702 @classmethod
703 def get_attribute_model(cls):
704 """Return the attribute model.
705
706 Override method in parent class. See ModelExtensions for details.
707 @returns: The attribute model of Host.
708 """
709 return HostAttribute
710
711
jadmanski0afbb632008-06-06 21:10:57 +0000712 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800713 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000714 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000715
Fang Dengff361592015-02-02 15:27:34 -0800716
showarda5288b42009-07-28 20:06:08 +0000717 def __unicode__(self):
718 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000719
720
MK Ryuacf35922014-10-03 14:56:49 -0700721class HostAttribute(dbmodels.Model, model_logic.ModelExtensions):
showard0957a842009-05-11 19:25:08 +0000722 """Arbitrary keyvals associated with hosts."""
Fang Deng86248502014-12-18 16:38:00 -0800723
724 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Fang Dengff361592015-02-02 15:27:34 -0800725 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
showard0957a842009-05-11 19:25:08 +0000726 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000727 attribute = dbmodels.CharField(max_length=90)
728 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000729
730 objects = model_logic.ExtendedManager()
731
732 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800733 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000734 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000735
736
Fang Dengff361592015-02-02 15:27:34 -0800737 @classmethod
738 def get_record(cls, data):
739 """Check the database for an identical record.
740
741 Use host_id and attribute to search for a existing record.
742
743 @raises: DoesNotExist, if no record found
744 @raises: MultipleObjectsReturned if multiple records found.
745 """
746 # TODO(fdeng): We should use host_id and attribute together as
747 # a primary key in the db.
748 return cls.objects.get(host_id=data['host_id'],
749 attribute=data['attribute'])
750
751
752 @classmethod
753 def deserialize(cls, data):
754 """Override deserialize in parent class.
755
756 Do not deserialize id as id is not kept consistent on master and shards.
757
758 @param data: A dictionary of data to deserialize.
759
760 @returns: A HostAttribute object.
761 """
762 if data:
763 data.pop('id')
764 return super(HostAttribute, cls).deserialize(data)
765
766
showard7c785282008-05-29 19:45:12 +0000767class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000768 """\
769 Required:
showard909c7a62008-07-15 21:52:38 +0000770 author: author name
771 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000772 name: test name
showard909c7a62008-07-15 21:52:38 +0000773 time: short, medium, long
774 test_class: This describes the class for your the test belongs in.
775 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000776 test_type: Client or Server
777 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000778 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
779 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000780 i.e. if sync_count = 2 it is a sync job that requires two
781 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000782 Optional:
showard909c7a62008-07-15 21:52:38 +0000783 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000784 dependency_labels: many-to-many relationship with labels corresponding to
785 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000786 experimental: If this is set to True production servers will ignore the test
787 run_verify: Whether or not the scheduler should run the verify stage
Dan Shi07e09af2013-04-12 09:31:29 -0700788 run_reset: Whether or not the scheduler should run the reset stage
Aviv Keshet9af96d32013-03-05 12:56:24 -0800789 test_retry: Number of times to retry test if the test did not complete
790 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +0000791 """
showard909c7a62008-07-15 21:52:38 +0000792 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000793
showarda5288b42009-07-28 20:06:08 +0000794 name = dbmodels.CharField(max_length=255, unique=True)
795 author = dbmodels.CharField(max_length=255)
796 test_class = dbmodels.CharField(max_length=255)
797 test_category = dbmodels.CharField(max_length=255)
798 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000799 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000800 experimental = dbmodels.BooleanField(default=True)
Dan Shi07e09af2013-04-12 09:31:29 -0700801 run_verify = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +0000802 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
803 default=TestTime.MEDIUM)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700804 test_type = dbmodels.SmallIntegerField(
805 choices=control_data.CONTROL_TYPE.choices())
showard909c7a62008-07-15 21:52:38 +0000806 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000807 path = dbmodels.CharField(max_length=255, unique=True)
Aviv Keshet9af96d32013-03-05 12:56:24 -0800808 test_retry = dbmodels.IntegerField(blank=True, default=0)
Dan Shi07e09af2013-04-12 09:31:29 -0700809 run_reset = dbmodels.BooleanField(default=True)
mblighe8819cd2008-02-15 16:48:40 +0000810
showardeab66ce2009-12-23 00:03:56 +0000811 dependency_labels = (
812 dbmodels.ManyToManyField(Label, blank=True,
813 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000814 name_field = 'name'
815 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000816
817
jamesren35a70222010-02-16 19:30:46 +0000818 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800819 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +0000820 escaped_description = saxutils.escape(self.description)
821 return '<span style="white-space:pre">%s</span>' % escaped_description
822 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000823 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000824
825
jadmanski0afbb632008-06-06 21:10:57 +0000826 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800827 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +0000828 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000829
showarda5288b42009-07-28 20:06:08 +0000830 def __unicode__(self):
831 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000832
833
jamesren4a41e012010-07-16 22:33:48 +0000834class TestParameter(dbmodels.Model):
835 """
836 A declared parameter of a test
837 """
838 test = dbmodels.ForeignKey(Test)
839 name = dbmodels.CharField(max_length=255)
840
841 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800842 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000843 db_table = 'afe_test_parameters'
844 unique_together = ('test', 'name')
845
846 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -0700847 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +0000848
849
showard2b9a88b2008-06-13 20:55:03 +0000850class Profiler(dbmodels.Model, model_logic.ModelExtensions):
851 """\
852 Required:
853 name: profiler name
854 test_type: Client or Server
855
856 Optional:
857 description: arbirary text description
858 """
showarda5288b42009-07-28 20:06:08 +0000859 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000860 description = dbmodels.TextField(blank=True)
861
862 name_field = 'name'
863 objects = model_logic.ExtendedManager()
864
865
866 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800867 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +0000868 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000869
showarda5288b42009-07-28 20:06:08 +0000870 def __unicode__(self):
871 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000872
873
showard7c785282008-05-29 19:45:12 +0000874class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000875 """\
876 Required:
877 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000878
jadmanski0afbb632008-06-06 21:10:57 +0000879 Optional:
880 description: arbitrary description of group
881 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700882
883 SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
884
showarda5288b42009-07-28 20:06:08 +0000885 name = dbmodels.CharField(max_length=255, unique=True)
886 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000887 users = dbmodels.ManyToManyField(User, blank=False,
888 db_table='afe_acl_groups_users')
889 hosts = dbmodels.ManyToManyField(Host, blank=True,
890 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000891
jadmanski0afbb632008-06-06 21:10:57 +0000892 name_field = 'name'
893 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000894
showard08f981b2008-06-24 21:59:03 +0000895 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000896 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800897 """Verify the current user has access to the specified hosts.
898
899 @param hosts: The hosts to verify against.
900 @raises AclAccessViolation if the current user doesn't have access
901 to a host.
902 """
showard64a95952010-01-13 21:27:16 +0000903 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000904 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000905 return
showard3dd47c22008-07-10 00:41:36 +0000906 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000907 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000908 for host in hosts:
909 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800910 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +0000911 no_access = (isinstance(host, Host)
912 and not host.invalid
913 and int(host.id) not in accessible_host_ids)
914 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000915 raise AclAccessViolation("%s does not have access to %s" %
916 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000917
showard9dbdcda2008-10-14 17:34:36 +0000918
919 @staticmethod
showarddc817512008-11-12 18:16:41 +0000920 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800921 """Look for queue entries that aren't abortable by the current user.
922
923 An entry is not abortable if:
924 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +0000925 * the machine isn't ACL-accessible, or
926 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800927
928 @param queue_entries: The queue entries to check.
929 @raises AclAccessViolation if a queue entry is not abortable by the
930 current user.
showarddc817512008-11-12 18:16:41 +0000931 """
showard64a95952010-01-13 21:27:16 +0000932 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000933 if user.is_superuser():
934 return
showarddc817512008-11-12 18:16:41 +0000935 not_owned = queue_entries.exclude(job__owner=user.login)
936 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000937 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
938 # 1.0.
939 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000940 accessible_ids = set(
941 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000942 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000943 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000944 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000945 cannot_abort = [entry for entry in not_owned.select_related()
946 if entry.id not in accessible_ids
947 or entry.id in public_ids]
948 if len(cannot_abort) == 0:
949 return
950 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000951 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000952 for entry in cannot_abort)
953 raise AclAccessViolation('You cannot abort the following job entries: '
954 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000955
956
showard3dd47c22008-07-10 00:41:36 +0000957 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800958 """Verifies the current user has acces to this ACL group.
959
960 @raises AclAccessViolation if the current user doesn't have access to
961 this ACL group.
962 """
showard64a95952010-01-13 21:27:16 +0000963 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000964 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000965 return
966 if self.name == 'Everyone':
967 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000968 if not user in self.users.all():
969 raise AclAccessViolation("You do not have access to %s"
970 % self.name)
971
972 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000973 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800974 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +0000975 everyone = AclGroup.objects.get(name='Everyone')
976
showard3dd47c22008-07-10 00:41:36 +0000977 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000978 # TODO(showard): this is a bit of a hack, since the fact that this query
979 # works is kind of a coincidence of Django internals. This trick
980 # doesn't work in general (on all foreign key relationships). I'll
981 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000982 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000983 everyone.hosts.add(*orphaned_hosts.distinct())
984
985 # find hosts in both Everyone and another ACL group, and remove them
986 # from Everyone
showarda5288b42009-07-28 20:06:08 +0000987 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
988 acled_hosts = set()
989 for host in hosts_in_everyone:
990 # Has an ACL group other than Everyone
991 if host.aclgroup_set.count() > 1:
992 acled_hosts.add(host)
993 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +0000994
995
996 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000997 if (self.name == 'Everyone'):
998 raise AclAccessViolation("You cannot delete 'Everyone'!")
999 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +00001000 super(AclGroup, self).delete()
1001 self.on_host_membership_change()
1002
1003
showard04f2cd82008-07-25 20:53:31 +00001004 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001005 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +00001006 if not self.users.count():
showard64a95952010-01-13 21:27:16 +00001007 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +00001008
1009
showard8cbaf1e2009-09-08 16:27:04 +00001010 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001011 """Called after a save.
1012
1013 @param change: Whether there was a change.
1014 """
showard8cbaf1e2009-09-08 16:27:04 +00001015 if not change:
showard64a95952010-01-13 21:27:16 +00001016 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +00001017 self.add_current_user_if_empty()
1018 self.on_host_membership_change()
1019
1020
1021 def save(self, *args, **kwargs):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001022 change = bool(self.id)
1023 if change:
showard8cbaf1e2009-09-08 16:27:04 +00001024 # Check the original object for an ACL violation
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001025 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
showard8cbaf1e2009-09-08 16:27:04 +00001026 super(AclGroup, self).save(*args, **kwargs)
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001027 self.perform_after_save(change)
showard8cbaf1e2009-09-08 16:27:04 +00001028
showardeb3be4d2008-04-21 20:59:26 +00001029
jadmanski0afbb632008-06-06 21:10:57 +00001030 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001031 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +00001032 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +00001033
showarda5288b42009-07-28 20:06:08 +00001034 def __unicode__(self):
1035 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +00001036
mblighe8819cd2008-02-15 16:48:40 +00001037
jamesren4a41e012010-07-16 22:33:48 +00001038class Kernel(dbmodels.Model):
1039 """
1040 A kernel configuration for a parameterized job
1041 """
1042 version = dbmodels.CharField(max_length=255)
1043 cmdline = dbmodels.CharField(max_length=255, blank=True)
1044
1045 @classmethod
1046 def create_kernels(cls, kernel_list):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001047 """Creates all kernels in the kernel list.
jamesren4a41e012010-07-16 22:33:48 +00001048
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001049 @param cls: Implicit class object.
1050 @param kernel_list: A list of dictionaries that describe the kernels,
1051 in the same format as the 'kernel' argument to
1052 rpc_interface.generate_control_file.
1053 @return A list of the created kernels.
jamesren4a41e012010-07-16 22:33:48 +00001054 """
1055 if not kernel_list:
1056 return None
1057 return [cls._create(kernel) for kernel in kernel_list]
1058
1059
1060 @classmethod
1061 def _create(cls, kernel_dict):
1062 version = kernel_dict.pop('version')
1063 cmdline = kernel_dict.pop('cmdline', '')
1064
1065 if kernel_dict:
1066 raise Exception('Extraneous kernel arguments remain: %r'
1067 % kernel_dict)
1068
1069 kernel, _ = cls.objects.get_or_create(version=version,
1070 cmdline=cmdline)
1071 return kernel
1072
1073
1074 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001075 """Metadata for class Kernel."""
jamesren4a41e012010-07-16 22:33:48 +00001076 db_table = 'afe_kernels'
1077 unique_together = ('version', 'cmdline')
1078
1079 def __unicode__(self):
1080 return u'%s %s' % (self.version, self.cmdline)
1081
1082
1083class ParameterizedJob(dbmodels.Model):
1084 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001085 Auxiliary configuration for a parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001086 """
1087 test = dbmodels.ForeignKey(Test)
1088 label = dbmodels.ForeignKey(Label, null=True)
1089 use_container = dbmodels.BooleanField(default=False)
1090 profile_only = dbmodels.BooleanField(default=False)
1091 upload_kernel_config = dbmodels.BooleanField(default=False)
1092
1093 kernels = dbmodels.ManyToManyField(
1094 Kernel, db_table='afe_parameterized_job_kernels')
1095 profilers = dbmodels.ManyToManyField(
1096 Profiler, through='ParameterizedJobProfiler')
1097
1098
1099 @classmethod
1100 def smart_get(cls, id_or_name, *args, **kwargs):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001101 """For compatibility with Job.add_object.
1102
1103 @param cls: Implicit class object.
1104 @param id_or_name: The ID or name to get.
1105 @param args: Non-keyword arguments.
1106 @param kwargs: Keyword arguments.
1107 """
jamesren4a41e012010-07-16 22:33:48 +00001108 return cls.objects.get(pk=id_or_name)
1109
1110
1111 def job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001112 """Returns the job if it exists, or else None."""
jamesren4a41e012010-07-16 22:33:48 +00001113 jobs = self.job_set.all()
1114 assert jobs.count() <= 1
1115 return jobs and jobs[0] or None
1116
1117
1118 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001119 """Metadata for class ParameterizedJob."""
jamesren4a41e012010-07-16 22:33:48 +00001120 db_table = 'afe_parameterized_jobs'
1121
1122 def __unicode__(self):
1123 return u'%s (parameterized) - %s' % (self.test.name, self.job())
1124
1125
1126class ParameterizedJobProfiler(dbmodels.Model):
1127 """
1128 A profiler to run on a parameterized job
1129 """
1130 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1131 profiler = dbmodels.ForeignKey(Profiler)
1132
1133 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001134 """Metedata for class ParameterizedJobProfiler."""
jamesren4a41e012010-07-16 22:33:48 +00001135 db_table = 'afe_parameterized_jobs_profilers'
1136 unique_together = ('parameterized_job', 'profiler')
1137
1138
1139class ParameterizedJobProfilerParameter(dbmodels.Model):
1140 """
1141 A parameter for a profiler in a parameterized job
1142 """
1143 parameterized_job_profiler = dbmodels.ForeignKey(ParameterizedJobProfiler)
1144 parameter_name = dbmodels.CharField(max_length=255)
1145 parameter_value = dbmodels.TextField()
1146 parameter_type = dbmodels.CharField(
1147 max_length=8, choices=model_attributes.ParameterTypes.choices())
1148
1149 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001150 """Metadata for class ParameterizedJobProfilerParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001151 db_table = 'afe_parameterized_job_profiler_parameters'
1152 unique_together = ('parameterized_job_profiler', 'parameter_name')
1153
1154 def __unicode__(self):
1155 return u'%s - %s' % (self.parameterized_job_profiler.profiler.name,
1156 self.parameter_name)
1157
1158
1159class ParameterizedJobParameter(dbmodels.Model):
1160 """
1161 Parameters for a parameterized job
1162 """
1163 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1164 test_parameter = dbmodels.ForeignKey(TestParameter)
1165 parameter_value = dbmodels.TextField()
1166 parameter_type = dbmodels.CharField(
1167 max_length=8, choices=model_attributes.ParameterTypes.choices())
1168
1169 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001170 """Metadata for class ParameterizedJobParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001171 db_table = 'afe_parameterized_job_parameters'
1172 unique_together = ('parameterized_job', 'test_parameter')
1173
1174 def __unicode__(self):
1175 return u'%s - %s' % (self.parameterized_job.job().name,
1176 self.test_parameter.name)
1177
1178
showard7c785282008-05-29 19:45:12 +00001179class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +00001180 'Custom manager to provide efficient status counts querying.'
1181 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001182 """Returns a dict mapping the given job IDs to their status count dicts.
1183
1184 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +00001185 """
1186 if not job_ids:
1187 return {}
1188 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1189 cursor = connection.cursor()
1190 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +00001191 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +00001192 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +00001193 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +00001194 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +00001195 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +00001196 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +00001197 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +00001198 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +00001199 full_status = HostQueueEntry.compute_full_status(status, aborted,
1200 complete)
showardb6d16622009-05-26 19:35:29 +00001201 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +00001202 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +00001203 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +00001204
1205
showard7c785282008-05-29 19:45:12 +00001206class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001207 """\
1208 owner: username of job owner
1209 name: job name (does not have to be unique)
Alex Miller7d658cf2013-09-04 16:00:35 -07001210 priority: Integer priority value. Higher is more important.
jadmanski0afbb632008-06-06 21:10:57 +00001211 control_file: contents of control file
1212 control_type: Client or Server
1213 created_on: date of job creation
1214 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +00001215 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +00001216 run_verify: Whether or not to run the verify phase
Dan Shi07e09af2013-04-12 09:31:29 -07001217 run_reset: Whether or not to run the reset phase
Simran Basi94dc0032013-11-12 14:09:46 -08001218 timeout: DEPRECATED - hours from queuing time until job times out
1219 timeout_mins: minutes from job queuing time until the job times out
Simran Basi34217022012-11-06 13:43:15 -08001220 max_runtime_hrs: DEPRECATED - hours from job starting time until job
1221 times out
1222 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +00001223 email_list: list of people to email on completion delimited by any of:
1224 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +00001225 dependency_labels: many-to-many relationship with labels corresponding to
1226 job dependencies
showard21baa452008-10-21 00:08:39 +00001227 reboot_before: Never, If dirty, or Always
1228 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001229 parse_failed_repair: if True, a failed repair launched by this job will have
1230 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001231 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001232 parent_job: Parent job (optional)
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001233 test_retry: Number of times to retry test if the test did not complete
1234 successfully. (optional, default: 0)
Dan Shic9e17142015-02-19 11:50:55 -08001235 require_ssp: Require server-side packaging unless require_ssp is set to
1236 False. (optional, default: None)
jadmanski0afbb632008-06-06 21:10:57 +00001237 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001238
1239 # TODO: Investigate, if jobkeyval_set is really needed.
1240 # dynamic_suite will write them into an attached file for the drone, but
1241 # it doesn't seem like they are actually used. If they aren't used, remove
1242 # jobkeyval_set here.
1243 SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1244 'hostqueueentry_set',
1245 'jobkeyval_set',
1246 'shard'])
1247
1248
Jakob Juelichf88fa932014-09-03 17:58:04 -07001249 def _deserialize_relation(self, link, data):
1250 if link in ['hostqueueentry_set', 'jobkeyval_set']:
1251 for obj in data:
1252 obj['job_id'] = self.id
1253
1254 super(Job, self)._deserialize_relation(link, data)
1255
1256
1257 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001258 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -07001259 self.shard = Shard.deserialize(data)
1260
1261
Jakob Juelicha94efe62014-09-18 16:02:49 -07001262 def sanity_check_update_from_shard(self, shard, updated_serialized):
Jakob Juelich02e61292014-10-17 12:36:55 -07001263 # If the job got aborted on the master after the client fetched it
1264 # no shard_id will be set. The shard might still push updates though,
1265 # as the job might complete before the abort bit syncs to the shard.
1266 # Alternative considered: The master scheduler could be changed to not
1267 # set aborted jobs to completed that are sharded out. But that would
1268 # require database queries and seemed more complicated to implement.
1269 # This seems safe to do, as there won't be updates pushed from the wrong
1270 # shards should be powered off and wiped hen they are removed from the
1271 # master.
1272 if self.shard_id and self.shard_id != shard.id:
Jakob Juelicha94efe62014-09-18 16:02:49 -07001273 raise error.UnallowedRecordsSentToMaster(
1274 'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1275 'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1276 shard.id))
1277
1278
Simran Basi94dc0032013-11-12 14:09:46 -08001279 # TIMEOUT is deprecated.
showardb1e51872008-10-07 11:08:18 +00001280 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
Simran Basi94dc0032013-11-12 14:09:46 -08001281 'AUTOTEST_WEB', 'job_timeout_default', default=24)
1282 DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1283 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
Simran Basi34217022012-11-06 13:43:15 -08001284 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1285 # completed.
showard12f3e322009-05-13 21:27:42 +00001286 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1287 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001288 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1289 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001290 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1291 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1292 default=False)
showardb1e51872008-10-07 11:08:18 +00001293
showarda5288b42009-07-28 20:06:08 +00001294 owner = dbmodels.CharField(max_length=255)
1295 name = dbmodels.CharField(max_length=255)
Alex Miller7d658cf2013-09-04 16:00:35 -07001296 priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
jamesren4a41e012010-07-16 22:33:48 +00001297 control_file = dbmodels.TextField(null=True, blank=True)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001298 control_type = dbmodels.SmallIntegerField(
1299 choices=control_data.CONTROL_TYPE.choices(),
1300 blank=True, # to allow 0
1301 default=control_data.CONTROL_TYPE.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001302 created_on = dbmodels.DateTimeField()
Simran Basi8d89b642014-05-02 17:33:20 -07001303 synch_count = dbmodels.IntegerField(blank=True, default=0)
showardb1e51872008-10-07 11:08:18 +00001304 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
Dan Shi07e09af2013-04-12 09:31:29 -07001305 run_verify = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001306 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001307 dependency_labels = (
1308 dbmodels.ManyToManyField(Label, blank=True,
1309 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001310 reboot_before = dbmodels.SmallIntegerField(
1311 choices=model_attributes.RebootBefore.choices(), blank=True,
1312 default=DEFAULT_REBOOT_BEFORE)
1313 reboot_after = dbmodels.SmallIntegerField(
1314 choices=model_attributes.RebootAfter.choices(), blank=True,
1315 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001316 parse_failed_repair = dbmodels.BooleanField(
1317 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001318 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1319 # completed.
showard12f3e322009-05-13 21:27:42 +00001320 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001321 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001322 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001323
jamesren4a41e012010-07-16 22:33:48 +00001324 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1325 blank=True)
1326
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001327 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001328
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001329 test_retry = dbmodels.IntegerField(blank=True, default=0)
1330
Dan Shi07e09af2013-04-12 09:31:29 -07001331 run_reset = dbmodels.BooleanField(default=True)
1332
Simran Basi94dc0032013-11-12 14:09:46 -08001333 timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1334
Jakob Juelich8421d592014-09-17 15:27:06 -07001335 # If this is None on the master, a slave should be found.
1336 # If this is None on a slave, it should be synced back to the master
Jakob Jülich92c06332014-08-25 19:06:57 +00001337 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1338
Dan Shic9e17142015-02-19 11:50:55 -08001339 # If this is None, server-side packaging will be used for server side test,
1340 # unless it's disabled in global config AUTOSERV/enable_ssp_container.
1341 require_ssp = dbmodels.NullBooleanField(default=None, blank=True, null=True)
1342
jadmanski0afbb632008-06-06 21:10:57 +00001343 # custom manager
1344 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001345
1346
Alex Millerec212252014-02-28 16:48:34 -08001347 @decorators.cached_property
1348 def labels(self):
1349 """All the labels of this job"""
1350 # We need to convert dependency_labels to a list, because all() gives us
1351 # back an iterator, and storing/caching an iterator means we'd only be
1352 # able to read from it once.
1353 return list(self.dependency_labels.all())
1354
1355
jadmanski0afbb632008-06-06 21:10:57 +00001356 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001357 """Returns whether this job is of type server."""
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001358 return self.control_type == control_data.CONTROL_TYPE.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001359
1360
jadmanski0afbb632008-06-06 21:10:57 +00001361 @classmethod
jamesren4a41e012010-07-16 22:33:48 +00001362 def parameterized_jobs_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001363 """Returns whether parameterized jobs are enabled.
1364
1365 @param cls: Implicit class object.
1366 """
jamesren4a41e012010-07-16 22:33:48 +00001367 return global_config.global_config.get_config_value(
1368 'AUTOTEST_WEB', 'parameterized_jobs', type=bool)
1369
1370
1371 @classmethod
1372 def check_parameterized_job(cls, control_file, parameterized_job):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001373 """Checks that the job is valid given the global config settings.
jamesren4a41e012010-07-16 22:33:48 +00001374
1375 First, either control_file must be set, or parameterized_job must be
1376 set, but not both. Second, parameterized_job must be set if and only if
1377 the parameterized_jobs option in the global config is set to True.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001378
1379 @param cls: Implict class object.
1380 @param control_file: A control file.
1381 @param parameterized_job: A parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001382 """
1383 if not (bool(control_file) ^ bool(parameterized_job)):
1384 raise Exception('Job must have either control file or '
1385 'parameterization, but not both')
1386
1387 parameterized_jobs_enabled = cls.parameterized_jobs_enabled()
1388 if control_file and parameterized_jobs_enabled:
1389 raise Exception('Control file specified, but parameterized jobs '
1390 'are enabled')
1391 if parameterized_job and not parameterized_jobs_enabled:
1392 raise Exception('Parameterized job specified, but parameterized '
1393 'jobs are not enabled')
1394
1395
1396 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001397 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001398 """Creates a job.
1399
1400 The job is created by taking some information (the listed args) and
1401 filling in the rest of the necessary information.
1402
1403 @param cls: Implicit class object.
1404 @param owner: The owner for the job.
1405 @param options: An options object.
1406 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001407 """
showard3dd47c22008-07-10 00:41:36 +00001408 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001409
jamesren4a41e012010-07-16 22:33:48 +00001410 control_file = options.get('control_file')
1411 parameterized_job = options.get('parameterized_job')
jamesren4a41e012010-07-16 22:33:48 +00001412
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001413 # The current implementation of parameterized jobs requires that only
1414 # control files or parameterized jobs are used. Using the image
1415 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1416 # parameterized jobs and control files jobs, it does muck enough with
1417 # normal jobs by adding a parameterized id to them that this check will
1418 # fail. So for now we just skip this check.
1419 # cls.check_parameterized_job(control_file=control_file,
1420 # parameterized_job=parameterized_job)
showard4d233752010-01-20 19:06:40 +00001421 user = User.current_user()
1422 if options.get('reboot_before') is None:
1423 options['reboot_before'] = user.get_reboot_before_display()
1424 if options.get('reboot_after') is None:
1425 options['reboot_after'] = user.get_reboot_after_display()
1426
jamesren76fcf192010-04-21 20:39:50 +00001427 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1428
Simran Basi94dc0032013-11-12 14:09:46 -08001429 if options.get('timeout_mins') is None and options.get('timeout'):
1430 options['timeout_mins'] = options['timeout'] * 60
1431
jadmanski0afbb632008-06-06 21:10:57 +00001432 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001433 owner=owner,
1434 name=options['name'],
1435 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001436 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001437 control_type=options['control_type'],
1438 synch_count=options.get('synch_count'),
Simran Basi94dc0032013-11-12 14:09:46 -08001439 # timeout needs to be deleted in the future.
showarda1e74b32009-05-12 17:32:04 +00001440 timeout=options.get('timeout'),
Simran Basi94dc0032013-11-12 14:09:46 -08001441 timeout_mins=options.get('timeout_mins'),
Simran Basi34217022012-11-06 13:43:15 -08001442 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001443 run_verify=options.get('run_verify'),
1444 email_list=options.get('email_list'),
1445 reboot_before=options.get('reboot_before'),
1446 reboot_after=options.get('reboot_after'),
1447 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001448 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001449 drone_set=drone_set,
Aviv Keshet18308922013-02-19 17:49:49 -08001450 parameterized_job=parameterized_job,
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001451 parent_job=options.get('parent_job_id'),
Dan Shi07e09af2013-04-12 09:31:29 -07001452 test_retry=options.get('test_retry'),
Dan Shic9e17142015-02-19 11:50:55 -08001453 run_reset=options.get('run_reset'),
1454 require_ssp=options.get('require_ssp'))
mblighe8819cd2008-02-15 16:48:40 +00001455
showarda1e74b32009-05-12 17:32:04 +00001456 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001457
jamesrend8b6e172010-04-16 23:45:00 +00001458 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001459 for key, value in options['keyvals'].iteritems():
1460 JobKeyval.objects.create(job=job, key=key, value=value)
1461
jadmanski0afbb632008-06-06 21:10:57 +00001462 return job
mblighe8819cd2008-02-15 16:48:40 +00001463
1464
Jakob Juelich59cfe542014-09-02 16:37:46 -07001465 @classmethod
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001466 def _add_filters_for_shard_assignment(cls, query, known_ids):
1467 """Exclude jobs that should be not sent to shard.
1468
1469 This is a helper that filters out the following jobs:
1470 - Non-aborted jobs known to shard as specified in |known_ids|.
1471 Note for jobs aborted on master, even if already known to shard,
1472 will be sent to shard again so that shard can abort them.
1473 - Completed jobs
1474 - Active jobs
1475 @param query: A query that finds jobs for shards, to which the 'exclude'
1476 filters will be applied.
1477 @param known_ids: List of all ids of incomplete jobs, the shard already
1478 knows about.
1479
1480 @returns: A django QuerySet after filtering out unnecessary jobs.
1481
1482 """
1483 return query.exclude(
1484 id__in=known_ids,
1485 hostqueueentry__aborted=False
1486 ).exclude(
1487 hostqueueentry__complete=True
1488 ).exclude(
1489 hostqueueentry__active=True)
1490
1491
1492 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -07001493 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -07001494 """Assigns unassigned jobs to a shard.
1495
Jakob Juelich1b525742014-09-30 13:08:07 -07001496 For all labels that have been assigned to this shard, all jobs that
1497 have this label, are assigned to this shard.
1498
1499 Jobs that are assigned to the shard but aren't already present on the
1500 shard are returned.
1501
Jakob Juelich59cfe542014-09-02 16:37:46 -07001502 @param shard: The shard to assign jobs to.
Jakob Juelich1b525742014-09-30 13:08:07 -07001503 @param known_ids: List of all ids of incomplete jobs, the shard already
1504 knows about.
1505 This is used to figure out which jobs should be sent
1506 to the shard. If shard_ids were used instead, jobs
1507 would only be transferred once, even if the client
1508 failed persisting them.
1509 The number of unfinished jobs usually lies in O(1000).
1510 Assuming one id takes 8 chars in the json, this means
1511 overhead that lies in the lower kilobyte range.
1512 A not in query with 5000 id's takes about 30ms.
1513
Jakob Juelich59cfe542014-09-02 16:37:46 -07001514 @returns The job objects that should be sent to the shard.
1515 """
1516 # Disclaimer: Concurrent heartbeats should not occur in today's setup.
1517 # If this changes or they are triggered manually, this applies:
1518 # Jobs may be returned more than once by concurrent calls of this
1519 # function, as there is a race condition between SELECT and UPDATE.
MK Ryu06a4b522015-04-24 15:06:10 -07001520 query = Job.objects.filter(
MK Ryu638a6cf2015-05-08 14:49:43 -07001521 dependency_labels__in=shard.labels.all(),
MK Ryu06a4b522015-04-24 15:06:10 -07001522 # If an HQE associated with a job is removed in some reasons,
1523 # such jobs should be excluded. Refer crbug.com/479766
1524 hostqueueentry__isnull=False
1525 )
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001526 query = cls._add_filters_for_shard_assignment(query, known_ids)
1527 job_ids = set(query.distinct().values_list('pk', flat=True))
1528
1529 # Combine frontend jobs in the heartbeat.
1530 query = Job.objects.filter(
1531 hostqueueentry__meta_host__isnull=True,
1532 hostqueueentry__host__isnull=False,
MK Ryu638a6cf2015-05-08 14:49:43 -07001533 hostqueueentry__host__labels__in=shard.labels.all()
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001534 )
1535 query = cls._add_filters_for_shard_assignment(query, known_ids)
1536 job_ids |= set(query.distinct().values_list('pk', flat=True))
Jakob Juelich59cfe542014-09-02 16:37:46 -07001537 if job_ids:
1538 Job.objects.filter(pk__in=job_ids).update(shard=shard)
1539 return list(Job.objects.filter(pk__in=job_ids).all())
1540 return []
1541
1542
jamesren4a41e012010-07-16 22:33:48 +00001543 def save(self, *args, **kwargs):
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001544 # The current implementation of parameterized jobs requires that only
1545 # control files or parameterized jobs are used. Using the image
1546 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1547 # parameterized jobs and control files jobs, it does muck enough with
1548 # normal jobs by adding a parameterized id to them that this check will
1549 # fail. So for now we just skip this check.
1550 # cls.check_parameterized_job(control_file=self.control_file,
1551 # parameterized_job=self.parameterized_job)
jamesren4a41e012010-07-16 22:33:48 +00001552 super(Job, self).save(*args, **kwargs)
1553
1554
showard29f7cd22009-04-29 21:16:24 +00001555 def queue(self, hosts, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001556 """Enqueue a job on the given hosts.
1557
1558 @param hosts: The hosts to use.
1559 @param atomic_group: The associated atomic group.
1560 @param is_template: Whether the status should be "Template".
1561 """
showarda9545c02009-12-18 22:44:26 +00001562 if not hosts:
1563 if atomic_group:
1564 # No hosts or labels are required to queue an atomic group
1565 # Job. However, if they are given, we respect them below.
1566 atomic_group.enqueue_job(self, is_template=is_template)
1567 else:
1568 # hostless job
1569 entry = HostQueueEntry.create(job=self, is_template=is_template)
1570 entry.save()
1571 return
1572
jadmanski0afbb632008-06-06 21:10:57 +00001573 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +00001574 host.enqueue_job(self, atomic_group=atomic_group,
1575 is_template=is_template)
1576
1577
1578 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001579 """Creates a recurring job.
1580
1581 @param start_date: The starting date of the job.
1582 @param loop_period: How often to re-run the job, in seconds.
1583 @param loop_count: The re-run count.
1584 @param owner: The owner of the job.
1585 """
showard29f7cd22009-04-29 21:16:24 +00001586 rec = RecurringRun(job=self, start_date=start_date,
1587 loop_period=loop_period,
1588 loop_count=loop_count,
1589 owner=User.objects.get(login=owner))
1590 rec.save()
1591 return rec.id
mblighe8819cd2008-02-15 16:48:40 +00001592
1593
jadmanski0afbb632008-06-06 21:10:57 +00001594 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001595 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001596 try:
1597 return User.objects.get(login=self.owner)
1598 except self.DoesNotExist:
1599 return None
mblighe8819cd2008-02-15 16:48:40 +00001600
1601
showard64a95952010-01-13 21:27:16 +00001602 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001603 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001604 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001605 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001606
1607
showardd1195652009-12-08 22:21:02 +00001608 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001609 """Returns a string tag for this job."""
MK Ryu0c1a37d2015-04-30 12:00:55 -07001610 return server_utils.get_job_tag(self.id, self.owner)
showardd1195652009-12-08 22:21:02 +00001611
1612
showardc1a98d12010-01-15 00:22:22 +00001613 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001614 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001615 return dict((keyval.key, keyval.value)
1616 for keyval in self.jobkeyval_set.all())
1617
1618
Fang Dengff361592015-02-02 15:27:34 -08001619 @classmethod
1620 def get_attribute_model(cls):
1621 """Return the attribute model.
1622
1623 Override method in parent class. This class is called when
1624 deserializing the one-to-many relationship betwen Job and JobKeyval.
1625 On deserialization, we will try to clear any existing job keyvals
1626 associated with a job to avoid any inconsistency.
1627 Though Job doesn't implement ModelWithAttribute, we still treat
1628 it as an attribute model for this purpose.
1629
1630 @returns: The attribute model of Job.
1631 """
1632 return JobKeyval
1633
1634
jadmanski0afbb632008-06-06 21:10:57 +00001635 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001636 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001637 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001638
showarda5288b42009-07-28 20:06:08 +00001639 def __unicode__(self):
1640 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001641
1642
showardc1a98d12010-01-15 00:22:22 +00001643class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1644 """Keyvals associated with jobs"""
Fang Dengff361592015-02-02 15:27:34 -08001645
1646 SERIALIZATION_LINKS_TO_KEEP = set(['job'])
1647 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
1648
showardc1a98d12010-01-15 00:22:22 +00001649 job = dbmodels.ForeignKey(Job)
1650 key = dbmodels.CharField(max_length=90)
1651 value = dbmodels.CharField(max_length=300)
1652
1653 objects = model_logic.ExtendedManager()
1654
Fang Dengff361592015-02-02 15:27:34 -08001655
1656 @classmethod
1657 def get_record(cls, data):
1658 """Check the database for an identical record.
1659
1660 Use job_id and key to search for a existing record.
1661
1662 @raises: DoesNotExist, if no record found
1663 @raises: MultipleObjectsReturned if multiple records found.
1664 """
1665 # TODO(fdeng): We should use job_id and key together as
1666 # a primary key in the db.
1667 return cls.objects.get(job_id=data['job_id'], key=data['key'])
1668
1669
1670 @classmethod
1671 def deserialize(cls, data):
1672 """Override deserialize in parent class.
1673
1674 Do not deserialize id as id is not kept consistent on master and shards.
1675
1676 @param data: A dictionary of data to deserialize.
1677
1678 @returns: A JobKeyval object.
1679 """
1680 if data:
1681 data.pop('id')
1682 return super(JobKeyval, cls).deserialize(data)
1683
1684
showardc1a98d12010-01-15 00:22:22 +00001685 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001686 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001687 db_table = 'afe_job_keyvals'
1688
1689
showard7c785282008-05-29 19:45:12 +00001690class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001691 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001692 job = dbmodels.ForeignKey(Job)
1693 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001694
jadmanski0afbb632008-06-06 21:10:57 +00001695 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001696
jadmanski0afbb632008-06-06 21:10:57 +00001697 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001698 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001699 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001700
mblighe8819cd2008-02-15 16:48:40 +00001701
showard7c785282008-05-29 19:45:12 +00001702class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001703 """Represents a host queue entry."""
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001704
1705 SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001706 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Jakob Juelichf865d332014-09-29 10:47:49 -07001707 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001708
Jakob Juelichf88fa932014-09-03 17:58:04 -07001709
1710 def custom_deserialize_relation(self, link, data):
1711 assert link == 'meta_host'
1712 self.meta_host = Label.deserialize(data)
1713
1714
Jakob Juelicha94efe62014-09-18 16:02:49 -07001715 def sanity_check_update_from_shard(self, shard, updated_serialized,
1716 job_ids_sent):
1717 if self.job_id not in job_ids_sent:
1718 raise error.UnallowedRecordsSentToMaster(
1719 'Sent HostQueueEntry without corresponding '
1720 'job entry: %s' % updated_serialized)
1721
1722
showardeaa408e2009-09-11 18:45:31 +00001723 Status = host_queue_entry_states.Status
1724 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001725 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001726
jadmanski0afbb632008-06-06 21:10:57 +00001727 job = dbmodels.ForeignKey(Job)
1728 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001729 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001730 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1731 db_column='meta_host')
1732 active = dbmodels.BooleanField(default=False)
1733 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001734 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001735 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1736 default='')
showard89f84db2009-03-12 20:39:13 +00001737 # If atomic_group is set, this is a virtual HostQueueEntry that will
1738 # be expanded into many actual hosts within the group at schedule time.
1739 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001740 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001741 started_on = dbmodels.DateTimeField(null=True, blank=True)
Fang Deng51599032014-06-23 17:24:27 -07001742 finished_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001743
jadmanski0afbb632008-06-06 21:10:57 +00001744 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001745
mblighe8819cd2008-02-15 16:48:40 +00001746
showard2bab8f42008-11-12 18:15:22 +00001747 def __init__(self, *args, **kwargs):
1748 super(HostQueueEntry, self).__init__(*args, **kwargs)
1749 self._record_attributes(['status'])
1750
1751
showard29f7cd22009-04-29 21:16:24 +00001752 @classmethod
1753 def create(cls, job, host=None, meta_host=None, atomic_group=None,
1754 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001755 """Creates a new host queue entry.
1756
1757 @param cls: Implicit class object.
1758 @param job: The associated job.
1759 @param host: The associated host.
1760 @param meta_host: The associated meta host.
1761 @param atomic_group: The associated atomic group.
1762 @param is_template: Whether the status should be "Template".
1763 """
showard29f7cd22009-04-29 21:16:24 +00001764 if is_template:
1765 status = cls.Status.TEMPLATE
1766 else:
1767 status = cls.Status.QUEUED
1768
1769 return cls(job=job, host=host, meta_host=meta_host,
1770 atomic_group=atomic_group, status=status)
1771
1772
showarda5288b42009-07-28 20:06:08 +00001773 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001774 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001775 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001776 self._check_for_updated_attributes()
1777
1778
showardc0ac3a72009-07-08 21:14:45 +00001779 def execution_path(self):
1780 """
1781 Path to this entry's results (relative to the base results directory).
1782 """
MK Ryu0c1a37d2015-04-30 12:00:55 -07001783 return server_utils.get_hqe_exec_path(self.job.tag(),
1784 self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001785
1786
showard3f15eed2008-11-14 22:40:48 +00001787 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001788 """Returns the first non-None name found in priority order.
1789
1790 The priority order checked is: (1) host name; (2) meta host name; and
1791 (3) atomic group name.
1792 """
showard3f15eed2008-11-14 22:40:48 +00001793 if self.host:
1794 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001795 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +00001796 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +00001797 else:
1798 assert self.atomic_group, "no host, meta_host or atomic group!"
1799 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +00001800
1801
showard2bab8f42008-11-12 18:15:22 +00001802 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001803 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001804 self.active, self.complete = True, False
1805 elif self.status in self.COMPLETE_STATUSES:
1806 self.active, self.complete = False, True
1807 else:
1808 self.active, self.complete = False, False
1809
1810
1811 def on_attribute_changed(self, attribute, old_value):
1812 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001813 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1814 self.status)
showard2bab8f42008-11-12 18:15:22 +00001815
1816
jadmanski0afbb632008-06-06 21:10:57 +00001817 def is_meta_host_entry(self):
1818 'True if this is a entry has a meta_host instead of a host.'
1819 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001820
showarda3ab0d52008-11-03 19:03:47 +00001821
Simran Basic1b26762013-06-26 14:23:21 -07001822 # This code is shared between rpc_interface and models.HostQueueEntry.
1823 # Sadly due to circular imports between the 2 (crbug.com/230100) making it
1824 # a class method was the best way to refactor it. Attempting to put it in
1825 # rpc_utils or a new utils module failed as that would require us to import
1826 # models.py but to call it from here we would have to import the utils.py
1827 # thus creating a cycle.
1828 @classmethod
1829 def abort_host_queue_entries(cls, host_queue_entries):
1830 """Aborts a collection of host_queue_entries.
1831
1832 Abort these host queue entry and all host queue entries of jobs created
1833 by them.
1834
1835 @param host_queue_entries: List of host queue entries we want to abort.
1836 """
1837 # This isn't completely immune to race conditions since it's not atomic,
1838 # but it should be safe given the scheduler's behavior.
1839
1840 # TODO(milleral): crbug.com/230100
1841 # The |abort_host_queue_entries| rpc does nearly exactly this,
1842 # however, trying to re-use the code generates some horrible
1843 # circular import error. I'd be nice to refactor things around
1844 # sometime so the code could be reused.
1845
1846 # Fixpoint algorithm to find the whole tree of HQEs to abort to
1847 # minimize the total number of database queries:
1848 children = set()
1849 new_children = set(host_queue_entries)
1850 while new_children:
1851 children.update(new_children)
1852 new_child_ids = [hqe.job_id for hqe in new_children]
1853 new_children = HostQueueEntry.objects.filter(
1854 job__parent_job__in=new_child_ids,
1855 complete=False, aborted=False).all()
1856 # To handle circular parental relationships
1857 new_children = set(new_children) - children
1858
1859 # Associate a user with the host queue entries that we're about
1860 # to abort so that we can look up who to blame for the aborts.
1861 now = datetime.now()
1862 user = User.current_user()
1863 aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
1864 aborted_by=user, aborted_on=now) for hqe in children]
1865 AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
1866 # Bulk update all of the HQEs to set the abort bit.
1867 child_ids = [hqe.id for hqe in children]
1868 HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
1869
1870
Scott Zawalski23041432013-04-17 07:39:09 -07001871 def abort(self):
Alex Millerdea67042013-04-22 17:23:34 -07001872 """ Aborts this host queue entry.
Simran Basic1b26762013-06-26 14:23:21 -07001873
Alex Millerdea67042013-04-22 17:23:34 -07001874 Abort this host queue entry and all host queue entries of jobs created by
1875 this one.
1876
1877 """
showardd3dc1992009-04-22 21:01:40 +00001878 if not self.complete and not self.aborted:
Simran Basi97582a22013-06-27 12:03:21 -07001879 HostQueueEntry.abort_host_queue_entries([self])
mblighe8819cd2008-02-15 16:48:40 +00001880
showardd3dc1992009-04-22 21:01:40 +00001881
1882 @classmethod
1883 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001884 """Returns a modified status msg if the host queue entry was aborted.
1885
1886 @param cls: Implicit class object.
1887 @param status: The original status message.
1888 @param aborted: Whether the host queue entry was aborted.
1889 @param complete: Whether the host queue entry was completed.
1890 """
showardd3dc1992009-04-22 21:01:40 +00001891 if aborted and not complete:
1892 return 'Aborted (%s)' % status
1893 return status
1894
1895
1896 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001897 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00001898 return self.compute_full_status(self.status, self.aborted,
1899 self.complete)
1900
1901
1902 def _postprocess_object_dict(self, object_dict):
1903 object_dict['full_status'] = self.full_status()
1904
1905
jadmanski0afbb632008-06-06 21:10:57 +00001906 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001907 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001908 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00001909
showard12f3e322009-05-13 21:27:42 +00001910
showard4c119042008-09-29 19:16:18 +00001911
showarda5288b42009-07-28 20:06:08 +00001912 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00001913 hostname = None
1914 if self.host:
1915 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00001916 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00001917
1918
showard4c119042008-09-29 19:16:18 +00001919class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001920 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00001921 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
1922 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00001923 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00001924
1925 objects = model_logic.ExtendedManager()
1926
showard68c7aa02008-10-09 16:49:11 +00001927
showarda5288b42009-07-28 20:06:08 +00001928 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00001929 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00001930 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00001931
showard4c119042008-09-29 19:16:18 +00001932 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001933 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001934 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00001935
1936
1937class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
1938 """\
1939 job: job to use as a template
1940 owner: owner of the instantiated template
1941 start_date: Run the job at scheduled date
1942 loop_period: Re-run (loop) the job periodically
1943 (in every loop_period seconds)
1944 loop_count: Re-run (loop) count
1945 """
1946
1947 job = dbmodels.ForeignKey(Job)
1948 owner = dbmodels.ForeignKey(User)
1949 start_date = dbmodels.DateTimeField()
1950 loop_period = dbmodels.IntegerField(blank=True)
1951 loop_count = dbmodels.IntegerField(blank=True)
1952
1953 objects = model_logic.ExtendedManager()
1954
1955 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001956 """Metadata for class RecurringRun."""
showardeab66ce2009-12-23 00:03:56 +00001957 db_table = 'afe_recurring_run'
showard29f7cd22009-04-29 21:16:24 +00001958
showarda5288b42009-07-28 20:06:08 +00001959 def __unicode__(self):
1960 return u'RecurringRun(job %s, start %s, period %s, count %s)' % (
showard29f7cd22009-04-29 21:16:24 +00001961 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +00001962
1963
1964class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
1965 """\
1966 Tasks to run on hosts at the next time they are in the Ready state. Use this
1967 for high-priority tasks, such as forced repair or forced reinstall.
1968
1969 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00001970 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00001971 time_requested: date and time the request for this task was made
1972 is_active: task is currently running
1973 is_complete: task has finished running
beeps8bb1f7d2013-08-05 01:30:09 -07001974 is_aborted: task was aborted
showard2fe3f1d2009-07-06 20:19:11 +00001975 time_started: date and time the task started
Dan Shid0725542014-06-23 15:34:27 -07001976 time_finished: date and time the task finished
showard2fe3f1d2009-07-06 20:19:11 +00001977 queue_entry: Host queue entry waiting on this task (or None, if task was not
1978 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00001979 """
Alex Millerdfff2fd2013-05-28 13:05:06 -07001980 Task = enum.Enum('Verify', 'Cleanup', 'Repair', 'Reset', 'Provision',
Dan Shi07e09af2013-04-12 09:31:29 -07001981 string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00001982
1983 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00001984 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00001985 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00001986 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00001987 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
1988 null=False)
1989 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
1990 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
beeps8bb1f7d2013-08-05 01:30:09 -07001991 is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00001992 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00001993 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00001994 success = dbmodels.BooleanField(default=False, blank=False, null=False)
Dan Shid0725542014-06-23 15:34:27 -07001995 time_finished = dbmodels.DateTimeField(null=True, blank=True)
showard6d7b2ff2009-06-10 00:16:47 +00001996
1997 objects = model_logic.ExtendedManager()
1998
1999
showard9bb960b2009-11-19 01:02:11 +00002000 def save(self, **kwargs):
2001 if self.queue_entry:
2002 self.requested_by = User.objects.get(
2003 login=self.queue_entry.job.owner)
2004 super(SpecialTask, self).save(**kwargs)
2005
2006
showarded2afea2009-07-07 20:54:07 +00002007 def execution_path(self):
MK Ryu0c1a37d2015-04-30 12:00:55 -07002008 """Returns the execution path for a special task."""
2009 return server_utils.get_special_task_exec_path(
2010 self.host.hostname, self.id, self.task, self.time_requested)
showarded2afea2009-07-07 20:54:07 +00002011
2012
showardc0ac3a72009-07-08 21:14:45 +00002013 # property to emulate HostQueueEntry.status
2014 @property
2015 def status(self):
MK Ryu0c1a37d2015-04-30 12:00:55 -07002016 """Returns a host queue entry status appropriate for a speical task."""
2017 return server_utils.get_special_task_status(
2018 self.is_complete, self.success, self.is_active)
showardc0ac3a72009-07-08 21:14:45 +00002019
2020
2021 # property to emulate HostQueueEntry.started_on
2022 @property
2023 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002024 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00002025 return self.time_started
2026
2027
showard6d7b2ff2009-06-10 00:16:47 +00002028 @classmethod
showardc5103442010-01-15 00:20:26 +00002029 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002030 """Schedules a special task on a host if not already scheduled.
2031
2032 @param cls: Implicit class object.
2033 @param host: The host to use.
2034 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00002035 """
showardc5103442010-01-15 00:20:26 +00002036 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
2037 is_active=False,
2038 is_complete=False)
2039 if existing_tasks:
2040 return existing_tasks[0]
2041
2042 special_task = SpecialTask(host=host, task=task,
2043 requested_by=User.current_user())
2044 special_task.save()
2045 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00002046
2047
beeps8bb1f7d2013-08-05 01:30:09 -07002048 def abort(self):
2049 """ Abort this special task."""
2050 self.is_aborted = True
2051 self.save()
2052
2053
showarded2afea2009-07-07 20:54:07 +00002054 def activate(self):
showard474d1362009-08-20 23:32:01 +00002055 """
2056 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00002057 """
showard97446882009-07-20 22:37:28 +00002058 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00002059 self.is_active = True
2060 self.time_started = datetime.now()
2061 self.save()
2062
2063
showarde60e44e2009-11-13 20:45:38 +00002064 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002065 """Sets a task as completed.
2066
2067 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00002068 """
showard97446882009-07-20 22:37:28 +00002069 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00002070 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00002071 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00002072 self.success = success
Dan Shid85d6112014-07-14 10:32:55 -07002073 if self.time_started:
2074 self.time_finished = datetime.now()
showard2fe3f1d2009-07-06 20:19:11 +00002075 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00002076
2077
2078 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002079 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00002080 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00002081
showard474d1362009-08-20 23:32:01 +00002082
showarda5288b42009-07-28 20:06:08 +00002083 def __unicode__(self):
2084 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00002085 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00002086 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00002087 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00002088 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00002089 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00002090
2091 return result
Dan Shi6964fa52014-12-18 11:04:27 -08002092
2093
2094class StableVersion(dbmodels.Model, model_logic.ModelExtensions):
2095
2096 board = dbmodels.CharField(max_length=255, unique=True)
2097 version = dbmodels.CharField(max_length=255)
2098
2099 class Meta:
2100 """Metadata for class StableVersion."""
Fang Deng86248502014-12-18 16:38:00 -08002101 db_table = 'afe_stable_versions'