blob: e2f0b1f37aec96068207cd28fcfafbf5754b4730 [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.
177 hostname = self.hostname.split(':')[0]
178 if site_utils.is_localhost(hostname):
179 return self.hostname.replace(
180 hostname, site_utils.DEFAULT_VM_GATEWAY)
181 return self.hostname
182
183
jamesren76fcf192010-04-21 20:39:50 +0000184class Drone(dbmodels.Model, model_logic.ModelExtensions):
185 """
186 A scheduler drone
187
188 hostname: the drone's hostname
189 """
190 hostname = dbmodels.CharField(max_length=255, unique=True)
191
192 name_field = 'hostname'
193 objects = model_logic.ExtendedManager()
194
195
196 def save(self, *args, **kwargs):
197 if not User.current_user().is_superuser():
198 raise Exception('Only superusers may edit drones')
199 super(Drone, self).save(*args, **kwargs)
200
201
202 def delete(self):
203 if not User.current_user().is_superuser():
204 raise Exception('Only superusers may delete drones')
205 super(Drone, self).delete()
206
207
208 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800209 """Metadata for class Drone."""
jamesren76fcf192010-04-21 20:39:50 +0000210 db_table = 'afe_drones'
211
212 def __unicode__(self):
213 return unicode(self.hostname)
214
215
216class DroneSet(dbmodels.Model, model_logic.ModelExtensions):
217 """
218 A set of scheduler drones
219
220 These will be used by the scheduler to decide what drones a job is allowed
221 to run on.
222
223 name: the drone set's name
224 drones: the drones that are part of the set
225 """
226 DRONE_SETS_ENABLED = global_config.global_config.get_config_value(
227 'SCHEDULER', 'drone_sets_enabled', type=bool, default=False)
228 DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value(
229 'SCHEDULER', 'default_drone_set_name', default=None)
230
231 name = dbmodels.CharField(max_length=255, unique=True)
232 drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones')
233
234 name_field = 'name'
235 objects = model_logic.ExtendedManager()
236
237
238 def save(self, *args, **kwargs):
239 if not User.current_user().is_superuser():
240 raise Exception('Only superusers may edit drone sets')
241 super(DroneSet, self).save(*args, **kwargs)
242
243
244 def delete(self):
245 if not User.current_user().is_superuser():
246 raise Exception('Only superusers may delete drone sets')
247 super(DroneSet, self).delete()
248
249
250 @classmethod
251 def drone_sets_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800252 """Returns whether drone sets are enabled.
253
254 @param cls: Implicit class object.
255 """
jamesren76fcf192010-04-21 20:39:50 +0000256 return cls.DRONE_SETS_ENABLED
257
258
259 @classmethod
260 def default_drone_set_name(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800261 """Returns the default drone set name.
262
263 @param cls: Implicit class object.
264 """
jamesren76fcf192010-04-21 20:39:50 +0000265 return cls.DEFAULT_DRONE_SET_NAME
266
267
268 @classmethod
269 def get_default(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800270 """Gets the default drone set name, compatible with Job.add_object.
271
272 @param cls: Implicit class object.
273 """
jamesren76fcf192010-04-21 20:39:50 +0000274 return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME)
275
276
277 @classmethod
278 def resolve_name(cls, drone_set_name):
279 """
280 Returns the name of one of these, if not None, in order of preference:
281 1) the drone set given,
282 2) the current user's default drone set, or
283 3) the global default drone set
284
285 or returns None if drone sets are disabled
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800286
287 @param cls: Implicit class object.
288 @param drone_set_name: A drone set name.
jamesren76fcf192010-04-21 20:39:50 +0000289 """
290 if not cls.drone_sets_enabled():
291 return None
292
293 user = User.current_user()
294 user_drone_set_name = user.drone_set and user.drone_set.name
295
296 return drone_set_name or user_drone_set_name or cls.get_default().name
297
298
299 def get_drone_hostnames(self):
300 """
301 Gets the hostnames of all drones in this drone set
302 """
303 return set(self.drones.all().values_list('hostname', flat=True))
304
305
306 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800307 """Metadata for class DroneSet."""
jamesren76fcf192010-04-21 20:39:50 +0000308 db_table = 'afe_drone_sets'
309
310 def __unicode__(self):
311 return unicode(self.name)
312
313
showardfb2a7fa2008-07-17 17:04:12 +0000314class User(dbmodels.Model, model_logic.ModelExtensions):
315 """\
316 Required:
317 login :user login name
318
319 Optional:
320 access_level: 0=User (default), 1=Admin, 100=Root
321 """
322 ACCESS_ROOT = 100
323 ACCESS_ADMIN = 1
324 ACCESS_USER = 0
325
showard64a95952010-01-13 21:27:16 +0000326 AUTOTEST_SYSTEM = 'autotest_system'
327
showarda5288b42009-07-28 20:06:08 +0000328 login = dbmodels.CharField(max_length=255, unique=True)
showardfb2a7fa2008-07-17 17:04:12 +0000329 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
330
showard0fc38302008-10-23 00:44:07 +0000331 # user preferences
jamesrendd855242010-03-02 22:23:44 +0000332 reboot_before = dbmodels.SmallIntegerField(
333 choices=model_attributes.RebootBefore.choices(), blank=True,
334 default=DEFAULT_REBOOT_BEFORE)
335 reboot_after = dbmodels.SmallIntegerField(
336 choices=model_attributes.RebootAfter.choices(), blank=True,
337 default=DEFAULT_REBOOT_AFTER)
jamesren76fcf192010-04-21 20:39:50 +0000338 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
showard97db5ba2008-11-12 18:18:02 +0000339 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000340
showardfb2a7fa2008-07-17 17:04:12 +0000341 name_field = 'login'
342 objects = model_logic.ExtendedManager()
343
344
showarda5288b42009-07-28 20:06:08 +0000345 def save(self, *args, **kwargs):
showardfb2a7fa2008-07-17 17:04:12 +0000346 # is this a new object being saved for the first time?
347 first_time = (self.id is None)
348 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000349 if user and not user.is_superuser() and user.login != self.login:
350 raise AclAccessViolation("You cannot modify user " + self.login)
showarda5288b42009-07-28 20:06:08 +0000351 super(User, self).save(*args, **kwargs)
showardfb2a7fa2008-07-17 17:04:12 +0000352 if first_time:
353 everyone = AclGroup.objects.get(name='Everyone')
354 everyone.users.add(self)
355
356
357 def is_superuser(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800358 """Returns whether the user has superuser access."""
showardfb2a7fa2008-07-17 17:04:12 +0000359 return self.access_level >= self.ACCESS_ROOT
360
361
showard64a95952010-01-13 21:27:16 +0000362 @classmethod
363 def current_user(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800364 """Returns the current user.
365
366 @param cls: Implicit class object.
367 """
showard64a95952010-01-13 21:27:16 +0000368 user = thread_local.get_user()
369 if user is None:
showardcfcdd802010-01-15 00:16:33 +0000370 user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM)
showard64a95952010-01-13 21:27:16 +0000371 user.access_level = cls.ACCESS_ROOT
372 user.save()
373 return user
374
375
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -0800376 @classmethod
377 def get_record(cls, data):
378 """Check the database for an identical record.
379
380 Check for a record with matching id and login. If one exists,
381 return it. If one does not exist there is a possibility that
382 the following cases have happened:
383 1. Same id, different login
384 We received: "1 chromeos-test"
385 And we have: "1 debug-user"
386 In this case we need to delete "1 debug_user" and insert
387 "1 chromeos-test".
388
389 2. Same login, different id:
390 We received: "1 chromeos-test"
391 And we have: "2 chromeos-test"
392 In this case we need to delete "2 chromeos-test" and insert
393 "1 chromeos-test".
394
395 As long as this method deletes bad records and raises the
396 DoesNotExist exception the caller will handle creating the
397 new record.
398
399 @raises: DoesNotExist, if a record with the matching login and id
400 does not exist.
401 """
402
403 # Both the id and login should be uniqe but there are cases when
404 # we might already have a user with the same login/id because
405 # current_user will proactively create a user record if it doesn't
406 # exist. Since we want to avoid conflict between the master and
407 # shard, just delete any existing user records that don't match
408 # what we're about to deserialize from the master.
409 try:
410 return cls.objects.get(login=data['login'], id=data['id'])
411 except cls.DoesNotExist:
412 cls.delete_matching_record(login=data['login'])
413 cls.delete_matching_record(id=data['id'])
414 raise
415
416
showardfb2a7fa2008-07-17 17:04:12 +0000417 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800418 """Metadata for class User."""
showardeab66ce2009-12-23 00:03:56 +0000419 db_table = 'afe_users'
showardfb2a7fa2008-07-17 17:04:12 +0000420
showarda5288b42009-07-28 20:06:08 +0000421 def __unicode__(self):
422 return unicode(self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000423
424
Prashanth B489b91d2014-03-15 12:17:16 -0700425class Host(model_logic.ModelWithInvalid, rdb_model_extensions.AbstractHostModel,
showardf8b19042009-05-12 17:22:49 +0000426 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000427 """\
428 Required:
429 hostname
mblighe8819cd2008-02-15 16:48:40 +0000430
jadmanski0afbb632008-06-06 21:10:57 +0000431 optional:
showard21baa452008-10-21 00:08:39 +0000432 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000433
jadmanski0afbb632008-06-06 21:10:57 +0000434 Internal:
Prashanth B489b91d2014-03-15 12:17:16 -0700435 From AbstractHostModel:
436 synch_id: currently unused
437 status: string describing status of host
438 invalid: true if the host has been deleted
439 protection: indicates what can be done to this host during repair
440 lock_time: DateTime at which the host was locked
441 dirty: true if the host has been used without being rebooted
442 Local:
443 locked_by: user that locked the host, or null if the host is unlocked
jadmanski0afbb632008-06-06 21:10:57 +0000444 """
mblighe8819cd2008-02-15 16:48:40 +0000445
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700446 SERIALIZATION_LINKS_TO_FOLLOW = set(['aclgroup_set',
447 'hostattribute_set',
448 'labels',
449 'shard'])
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800450 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['invalid'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700451
Jakob Juelichf88fa932014-09-03 17:58:04 -0700452
453 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700454 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -0700455 self.shard = Shard.deserialize(data)
456
457
Prashanth B489b91d2014-03-15 12:17:16 -0700458 # Note: Only specify foreign keys here, specify all native host columns in
459 # rdb_model_extensions instead.
460 Protection = host_protections.Protection
showardeab66ce2009-12-23 00:03:56 +0000461 labels = dbmodels.ManyToManyField(Label, blank=True,
462 db_table='afe_hosts_labels')
showardfb2a7fa2008-07-17 17:04:12 +0000463 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
jadmanski0afbb632008-06-06 21:10:57 +0000464 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000465 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000466 valid_objects = model_logic.ValidObjectsManager()
beepscc9fc702013-12-02 12:45:38 -0800467 leased_objects = model_logic.LeasedHostManager()
mbligh5244cbb2008-04-24 20:39:52 +0000468
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700469 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
showard2bab8f42008-11-12 18:15:22 +0000470
471 def __init__(self, *args, **kwargs):
472 super(Host, self).__init__(*args, **kwargs)
473 self._record_attributes(['status'])
474
475
showardb8471e32008-07-03 19:51:08 +0000476 @staticmethod
477 def create_one_time_host(hostname):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800478 """Creates a one-time host.
479
480 @param hostname: The name for the host.
481 """
showardb8471e32008-07-03 19:51:08 +0000482 query = Host.objects.filter(hostname=hostname)
483 if query.count() == 0:
484 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000485 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000486 else:
487 host = query[0]
488 if not host.invalid:
489 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000490 'hostname' : '%s already exists in the autotest DB. '
491 'Select it rather than entering it as a one time '
492 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000493 })
showard1ab512b2008-07-30 23:39:04 +0000494 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000495 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000496 host.save()
showard2924b0a2009-06-18 23:16:15 +0000497 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000498 return host
mbligh5244cbb2008-04-24 20:39:52 +0000499
showard1ff7b2e2009-05-15 23:17:18 +0000500
Jakob Juelich59cfe542014-09-02 16:37:46 -0700501 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -0700502 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -0700503 """Assigns hosts to a shard.
504
MK Ryu638a6cf2015-05-08 14:49:43 -0700505 For all labels that have been assigned to a shard, all hosts that
506 have at least one of the shard's labels are assigned to the shard.
Jakob Juelich1b525742014-09-30 13:08:07 -0700507 Hosts that are assigned to the shard but aren't already present on the
508 shard are returned.
Jakob Juelich59cfe542014-09-02 16:37:46 -0700509
MK Ryu638a6cf2015-05-08 14:49:43 -0700510 Board to shard mapping is many-to-one. Many different boards can be
511 hosted in a shard. However, DUTs of a single board cannot be distributed
512 into more than one shard.
513
Jakob Juelich59cfe542014-09-02 16:37:46 -0700514 @param shard: The shard object to assign labels/hosts for.
Jakob Juelich1b525742014-09-30 13:08:07 -0700515 @param known_ids: List of all host-ids the shard already knows.
516 This is used to figure out which hosts should be sent
517 to the shard. If shard_ids were used instead, hosts
518 would only be transferred once, even if the client
519 failed persisting them.
520 The number of hosts usually lies in O(100), so the
521 overhead is acceptable.
522
Jakob Juelich59cfe542014-09-02 16:37:46 -0700523 @returns the hosts objects that should be sent to the shard.
524 """
525
526 # Disclaimer: concurrent heartbeats should theoretically not occur in
527 # the current setup. As they may be introduced in the near future,
528 # this comment will be left here.
529
530 # Sending stuff twice is acceptable, but forgetting something isn't.
531 # Detecting duplicates on the client is easy, but here it's harder. The
532 # following options were considered:
533 # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
534 # than select returned, as concurrently more hosts might have been
535 # inserted
536 # - UPDATE and then SELECT WHERE shard=shard: select always returns all
537 # hosts for the shard, this is overhead
538 # - SELECT and then UPDATE only selected without requerying afterwards:
539 # returns the old state of the records.
MK Ryu638a6cf2015-05-08 14:49:43 -0700540 host_ids = set(Host.objects.filter(
541 labels__in=shard.labels.all(),
Jakob Juelich59cfe542014-09-02 16:37:46 -0700542 leased=False
Jakob Juelich1b525742014-09-30 13:08:07 -0700543 ).exclude(
544 id__in=known_ids,
Jakob Juelich59cfe542014-09-02 16:37:46 -0700545 ).values_list('pk', flat=True))
546
547 if host_ids:
548 Host.objects.filter(pk__in=host_ids).update(shard=shard)
549 return list(Host.objects.filter(pk__in=host_ids).all())
550 return []
551
showardafd97de2009-10-01 18:45:09 +0000552 def resurrect_object(self, old_object):
553 super(Host, self).resurrect_object(old_object)
554 # invalid hosts can be in use by the scheduler (as one-time hosts), so
555 # don't change the status
556 self.status = old_object.status
557
558
jadmanski0afbb632008-06-06 21:10:57 +0000559 def clean_object(self):
560 self.aclgroup_set.clear()
561 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000562
563
Dan Shia0acfbc2014-10-14 15:56:23 -0700564 def record_state(self, type_str, state, value, other_metadata=None):
565 """Record metadata in elasticsearch.
566
567 @param type_str: sets the _type field in elasticsearch db.
568 @param state: string representing what state we are recording,
569 e.g. 'locked'
570 @param value: value of the state, e.g. True
571 @param other_metadata: Other metadata to store in metaDB.
572 """
573 metadata = {
574 state: value,
575 'hostname': self.hostname,
576 }
577 if other_metadata:
578 metadata = dict(metadata.items() + other_metadata.items())
Matthew Sartori68186332015-04-27 17:19:53 -0700579 autotest_es.post(use_http=True, type_str=type_str, metadata=metadata)
Dan Shia0acfbc2014-10-14 15:56:23 -0700580
581
showarda5288b42009-07-28 20:06:08 +0000582 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000583 # extra spaces in the hostname can be a sneaky source of errors
584 self.hostname = self.hostname.strip()
585 # is this a new object being saved for the first time?
586 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000587 if not first_time:
588 AclGroup.check_for_acl_violation_hosts([self])
Dan Shia0acfbc2014-10-14 15:56:23 -0700589 # If locked is changed, send its status and user made the change to
590 # metaDB. Locks are important in host history because if a device is
591 # locked then we don't really care what state it is in.
showardfb2a7fa2008-07-17 17:04:12 +0000592 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000593 self.locked_by = User.current_user()
showardfb2a7fa2008-07-17 17:04:12 +0000594 self.lock_time = datetime.now()
Dan Shia0acfbc2014-10-14 15:56:23 -0700595 self.record_state('lock_history', 'locked', self.locked,
Matthew Sartori68186332015-04-27 17:19:53 -0700596 {'changed_by': self.locked_by.login,
597 'lock_reason': self.lock_reason})
showard21baa452008-10-21 00:08:39 +0000598 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000599 elif not self.locked and self.locked_by:
Dan Shia0acfbc2014-10-14 15:56:23 -0700600 self.record_state('lock_history', 'locked', self.locked,
601 {'changed_by': self.locked_by.login})
showardfb2a7fa2008-07-17 17:04:12 +0000602 self.locked_by = None
603 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000604 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000605 if first_time:
606 everyone = AclGroup.objects.get(name='Everyone')
607 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000608 self._check_for_updated_attributes()
609
mblighe8819cd2008-02-15 16:48:40 +0000610
showardb8471e32008-07-03 19:51:08 +0000611 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000612 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000613 for queue_entry in self.hostqueueentry_set.all():
614 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000615 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000616 super(Host, self).delete()
617
mblighe8819cd2008-02-15 16:48:40 +0000618
showard2bab8f42008-11-12 18:15:22 +0000619 def on_attribute_changed(self, attribute, old_value):
620 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000621 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000622
623
showard29f7cd22009-04-29 21:16:24 +0000624 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800625 """Enqueue a job on this host.
626
627 @param job: A job to enqueue.
628 @param atomic_group: The associated atomic group.
629 @param is_template: Whther the status should be "Template".
630 """
showard29f7cd22009-04-29 21:16:24 +0000631 queue_entry = HostQueueEntry.create(host=self, job=job,
632 is_template=is_template,
633 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000634 # allow recovery of dead hosts from the frontend
635 if not self.active_queue_entry() and self.is_dead():
636 self.status = Host.Status.READY
637 self.save()
638 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000639
showard08f981b2008-06-24 21:59:03 +0000640 block = IneligibleHostQueue(job=job, host=self)
641 block.save()
642
mblighe8819cd2008-02-15 16:48:40 +0000643
jadmanski0afbb632008-06-06 21:10:57 +0000644 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800645 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000646 # TODO(showard): slighly hacky?
647 platforms = self.labels.filter(platform=True)
648 if len(platforms) == 0:
649 return None
650 return platforms[0]
651 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000652
653
showardcafd16e2009-05-29 18:37:49 +0000654 @classmethod
655 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800656 """Verify the specified hosts have no associated platforms.
657
658 @param cls: Implicit class object.
659 @param hosts: The hosts to verify.
660 @raises model_logic.ValidationError if any hosts already have a
661 platform.
662 """
showardcafd16e2009-05-29 18:37:49 +0000663 Host.objects.populate_relationships(hosts, Label, 'label_list')
664 errors = []
665 for host in hosts:
666 platforms = [label.name for label in host.label_list
667 if label.platform]
668 if platforms:
669 # do a join, just in case this host has multiple platforms,
670 # we'll be able to see it
671 errors.append('Host %s already has a platform: %s' % (
672 host.hostname, ', '.join(platforms)))
673 if errors:
674 raise model_logic.ValidationError({'labels': '; '.join(errors)})
675
676
jadmanski0afbb632008-06-06 21:10:57 +0000677 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800678 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000679 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000680
681
jadmanski0afbb632008-06-06 21:10:57 +0000682 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800683 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000684 active = list(self.hostqueueentry_set.filter(active=True))
685 if not active:
686 return None
687 assert len(active) == 1, ('More than one active entry for '
688 'host ' + self.hostname)
689 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000690
691
showardf8b19042009-05-12 17:22:49 +0000692 def _get_attribute_model_and_args(self, attribute):
693 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000694
695
Fang Dengff361592015-02-02 15:27:34 -0800696 @classmethod
697 def get_attribute_model(cls):
698 """Return the attribute model.
699
700 Override method in parent class. See ModelExtensions for details.
701 @returns: The attribute model of Host.
702 """
703 return HostAttribute
704
705
jadmanski0afbb632008-06-06 21:10:57 +0000706 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800707 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000708 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000709
Fang Dengff361592015-02-02 15:27:34 -0800710
showarda5288b42009-07-28 20:06:08 +0000711 def __unicode__(self):
712 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000713
714
MK Ryuacf35922014-10-03 14:56:49 -0700715class HostAttribute(dbmodels.Model, model_logic.ModelExtensions):
showard0957a842009-05-11 19:25:08 +0000716 """Arbitrary keyvals associated with hosts."""
Fang Deng86248502014-12-18 16:38:00 -0800717
718 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Fang Dengff361592015-02-02 15:27:34 -0800719 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
showard0957a842009-05-11 19:25:08 +0000720 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000721 attribute = dbmodels.CharField(max_length=90)
722 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000723
724 objects = model_logic.ExtendedManager()
725
726 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800727 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000728 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000729
730
Fang Dengff361592015-02-02 15:27:34 -0800731 @classmethod
732 def get_record(cls, data):
733 """Check the database for an identical record.
734
735 Use host_id and attribute to search for a existing record.
736
737 @raises: DoesNotExist, if no record found
738 @raises: MultipleObjectsReturned if multiple records found.
739 """
740 # TODO(fdeng): We should use host_id and attribute together as
741 # a primary key in the db.
742 return cls.objects.get(host_id=data['host_id'],
743 attribute=data['attribute'])
744
745
746 @classmethod
747 def deserialize(cls, data):
748 """Override deserialize in parent class.
749
750 Do not deserialize id as id is not kept consistent on master and shards.
751
752 @param data: A dictionary of data to deserialize.
753
754 @returns: A HostAttribute object.
755 """
756 if data:
757 data.pop('id')
758 return super(HostAttribute, cls).deserialize(data)
759
760
showard7c785282008-05-29 19:45:12 +0000761class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000762 """\
763 Required:
showard909c7a62008-07-15 21:52:38 +0000764 author: author name
765 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000766 name: test name
showard909c7a62008-07-15 21:52:38 +0000767 time: short, medium, long
768 test_class: This describes the class for your the test belongs in.
769 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000770 test_type: Client or Server
771 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000772 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
773 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000774 i.e. if sync_count = 2 it is a sync job that requires two
775 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000776 Optional:
showard909c7a62008-07-15 21:52:38 +0000777 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000778 dependency_labels: many-to-many relationship with labels corresponding to
779 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000780 experimental: If this is set to True production servers will ignore the test
781 run_verify: Whether or not the scheduler should run the verify stage
Dan Shi07e09af2013-04-12 09:31:29 -0700782 run_reset: Whether or not the scheduler should run the reset stage
Aviv Keshet9af96d32013-03-05 12:56:24 -0800783 test_retry: Number of times to retry test if the test did not complete
784 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +0000785 """
showard909c7a62008-07-15 21:52:38 +0000786 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000787
showarda5288b42009-07-28 20:06:08 +0000788 name = dbmodels.CharField(max_length=255, unique=True)
789 author = dbmodels.CharField(max_length=255)
790 test_class = dbmodels.CharField(max_length=255)
791 test_category = dbmodels.CharField(max_length=255)
792 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000793 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000794 experimental = dbmodels.BooleanField(default=True)
Dan Shi07e09af2013-04-12 09:31:29 -0700795 run_verify = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +0000796 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
797 default=TestTime.MEDIUM)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700798 test_type = dbmodels.SmallIntegerField(
799 choices=control_data.CONTROL_TYPE.choices())
showard909c7a62008-07-15 21:52:38 +0000800 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000801 path = dbmodels.CharField(max_length=255, unique=True)
Aviv Keshet9af96d32013-03-05 12:56:24 -0800802 test_retry = dbmodels.IntegerField(blank=True, default=0)
Dan Shi07e09af2013-04-12 09:31:29 -0700803 run_reset = dbmodels.BooleanField(default=True)
mblighe8819cd2008-02-15 16:48:40 +0000804
showardeab66ce2009-12-23 00:03:56 +0000805 dependency_labels = (
806 dbmodels.ManyToManyField(Label, blank=True,
807 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000808 name_field = 'name'
809 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000810
811
jamesren35a70222010-02-16 19:30:46 +0000812 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800813 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +0000814 escaped_description = saxutils.escape(self.description)
815 return '<span style="white-space:pre">%s</span>' % escaped_description
816 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000817 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000818
819
jadmanski0afbb632008-06-06 21:10:57 +0000820 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800821 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +0000822 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000823
showarda5288b42009-07-28 20:06:08 +0000824 def __unicode__(self):
825 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000826
827
jamesren4a41e012010-07-16 22:33:48 +0000828class TestParameter(dbmodels.Model):
829 """
830 A declared parameter of a test
831 """
832 test = dbmodels.ForeignKey(Test)
833 name = dbmodels.CharField(max_length=255)
834
835 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800836 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000837 db_table = 'afe_test_parameters'
838 unique_together = ('test', 'name')
839
840 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -0700841 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +0000842
843
showard2b9a88b2008-06-13 20:55:03 +0000844class Profiler(dbmodels.Model, model_logic.ModelExtensions):
845 """\
846 Required:
847 name: profiler name
848 test_type: Client or Server
849
850 Optional:
851 description: arbirary text description
852 """
showarda5288b42009-07-28 20:06:08 +0000853 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000854 description = dbmodels.TextField(blank=True)
855
856 name_field = 'name'
857 objects = model_logic.ExtendedManager()
858
859
860 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800861 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +0000862 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000863
showarda5288b42009-07-28 20:06:08 +0000864 def __unicode__(self):
865 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000866
867
showard7c785282008-05-29 19:45:12 +0000868class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000869 """\
870 Required:
871 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000872
jadmanski0afbb632008-06-06 21:10:57 +0000873 Optional:
874 description: arbitrary description of group
875 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700876
877 SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
878
showarda5288b42009-07-28 20:06:08 +0000879 name = dbmodels.CharField(max_length=255, unique=True)
880 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000881 users = dbmodels.ManyToManyField(User, blank=False,
882 db_table='afe_acl_groups_users')
883 hosts = dbmodels.ManyToManyField(Host, blank=True,
884 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000885
jadmanski0afbb632008-06-06 21:10:57 +0000886 name_field = 'name'
887 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000888
showard08f981b2008-06-24 21:59:03 +0000889 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000890 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800891 """Verify the current user has access to the specified hosts.
892
893 @param hosts: The hosts to verify against.
894 @raises AclAccessViolation if the current user doesn't have access
895 to a host.
896 """
showard64a95952010-01-13 21:27:16 +0000897 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000898 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000899 return
showard3dd47c22008-07-10 00:41:36 +0000900 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000901 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000902 for host in hosts:
903 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800904 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +0000905 no_access = (isinstance(host, Host)
906 and not host.invalid
907 and int(host.id) not in accessible_host_ids)
908 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000909 raise AclAccessViolation("%s does not have access to %s" %
910 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000911
showard9dbdcda2008-10-14 17:34:36 +0000912
913 @staticmethod
showarddc817512008-11-12 18:16:41 +0000914 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800915 """Look for queue entries that aren't abortable by the current user.
916
917 An entry is not abortable if:
918 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +0000919 * the machine isn't ACL-accessible, or
920 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800921
922 @param queue_entries: The queue entries to check.
923 @raises AclAccessViolation if a queue entry is not abortable by the
924 current user.
showarddc817512008-11-12 18:16:41 +0000925 """
showard64a95952010-01-13 21:27:16 +0000926 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000927 if user.is_superuser():
928 return
showarddc817512008-11-12 18:16:41 +0000929 not_owned = queue_entries.exclude(job__owner=user.login)
930 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000931 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
932 # 1.0.
933 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000934 accessible_ids = set(
935 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000936 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000937 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000938 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000939 cannot_abort = [entry for entry in not_owned.select_related()
940 if entry.id not in accessible_ids
941 or entry.id in public_ids]
942 if len(cannot_abort) == 0:
943 return
944 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000945 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000946 for entry in cannot_abort)
947 raise AclAccessViolation('You cannot abort the following job entries: '
948 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000949
950
showard3dd47c22008-07-10 00:41:36 +0000951 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800952 """Verifies the current user has acces to this ACL group.
953
954 @raises AclAccessViolation if the current user doesn't have access to
955 this ACL group.
956 """
showard64a95952010-01-13 21:27:16 +0000957 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000958 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000959 return
960 if self.name == 'Everyone':
961 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000962 if not user in self.users.all():
963 raise AclAccessViolation("You do not have access to %s"
964 % self.name)
965
966 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000967 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800968 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +0000969 everyone = AclGroup.objects.get(name='Everyone')
970
showard3dd47c22008-07-10 00:41:36 +0000971 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000972 # TODO(showard): this is a bit of a hack, since the fact that this query
973 # works is kind of a coincidence of Django internals. This trick
974 # doesn't work in general (on all foreign key relationships). I'll
975 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000976 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000977 everyone.hosts.add(*orphaned_hosts.distinct())
978
979 # find hosts in both Everyone and another ACL group, and remove them
980 # from Everyone
showarda5288b42009-07-28 20:06:08 +0000981 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
982 acled_hosts = set()
983 for host in hosts_in_everyone:
984 # Has an ACL group other than Everyone
985 if host.aclgroup_set.count() > 1:
986 acled_hosts.add(host)
987 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +0000988
989
990 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000991 if (self.name == 'Everyone'):
992 raise AclAccessViolation("You cannot delete 'Everyone'!")
993 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000994 super(AclGroup, self).delete()
995 self.on_host_membership_change()
996
997
showard04f2cd82008-07-25 20:53:31 +0000998 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800999 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +00001000 if not self.users.count():
showard64a95952010-01-13 21:27:16 +00001001 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +00001002
1003
showard8cbaf1e2009-09-08 16:27:04 +00001004 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001005 """Called after a save.
1006
1007 @param change: Whether there was a change.
1008 """
showard8cbaf1e2009-09-08 16:27:04 +00001009 if not change:
showard64a95952010-01-13 21:27:16 +00001010 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +00001011 self.add_current_user_if_empty()
1012 self.on_host_membership_change()
1013
1014
1015 def save(self, *args, **kwargs):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001016 change = bool(self.id)
1017 if change:
showard8cbaf1e2009-09-08 16:27:04 +00001018 # Check the original object for an ACL violation
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001019 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
showard8cbaf1e2009-09-08 16:27:04 +00001020 super(AclGroup, self).save(*args, **kwargs)
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001021 self.perform_after_save(change)
showard8cbaf1e2009-09-08 16:27:04 +00001022
showardeb3be4d2008-04-21 20:59:26 +00001023
jadmanski0afbb632008-06-06 21:10:57 +00001024 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001025 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +00001026 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +00001027
showarda5288b42009-07-28 20:06:08 +00001028 def __unicode__(self):
1029 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +00001030
mblighe8819cd2008-02-15 16:48:40 +00001031
jamesren4a41e012010-07-16 22:33:48 +00001032class Kernel(dbmodels.Model):
1033 """
1034 A kernel configuration for a parameterized job
1035 """
1036 version = dbmodels.CharField(max_length=255)
1037 cmdline = dbmodels.CharField(max_length=255, blank=True)
1038
1039 @classmethod
1040 def create_kernels(cls, kernel_list):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001041 """Creates all kernels in the kernel list.
jamesren4a41e012010-07-16 22:33:48 +00001042
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001043 @param cls: Implicit class object.
1044 @param kernel_list: A list of dictionaries that describe the kernels,
1045 in the same format as the 'kernel' argument to
1046 rpc_interface.generate_control_file.
1047 @return A list of the created kernels.
jamesren4a41e012010-07-16 22:33:48 +00001048 """
1049 if not kernel_list:
1050 return None
1051 return [cls._create(kernel) for kernel in kernel_list]
1052
1053
1054 @classmethod
1055 def _create(cls, kernel_dict):
1056 version = kernel_dict.pop('version')
1057 cmdline = kernel_dict.pop('cmdline', '')
1058
1059 if kernel_dict:
1060 raise Exception('Extraneous kernel arguments remain: %r'
1061 % kernel_dict)
1062
1063 kernel, _ = cls.objects.get_or_create(version=version,
1064 cmdline=cmdline)
1065 return kernel
1066
1067
1068 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001069 """Metadata for class Kernel."""
jamesren4a41e012010-07-16 22:33:48 +00001070 db_table = 'afe_kernels'
1071 unique_together = ('version', 'cmdline')
1072
1073 def __unicode__(self):
1074 return u'%s %s' % (self.version, self.cmdline)
1075
1076
1077class ParameterizedJob(dbmodels.Model):
1078 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001079 Auxiliary configuration for a parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001080 """
1081 test = dbmodels.ForeignKey(Test)
1082 label = dbmodels.ForeignKey(Label, null=True)
1083 use_container = dbmodels.BooleanField(default=False)
1084 profile_only = dbmodels.BooleanField(default=False)
1085 upload_kernel_config = dbmodels.BooleanField(default=False)
1086
1087 kernels = dbmodels.ManyToManyField(
1088 Kernel, db_table='afe_parameterized_job_kernels')
1089 profilers = dbmodels.ManyToManyField(
1090 Profiler, through='ParameterizedJobProfiler')
1091
1092
1093 @classmethod
1094 def smart_get(cls, id_or_name, *args, **kwargs):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001095 """For compatibility with Job.add_object.
1096
1097 @param cls: Implicit class object.
1098 @param id_or_name: The ID or name to get.
1099 @param args: Non-keyword arguments.
1100 @param kwargs: Keyword arguments.
1101 """
jamesren4a41e012010-07-16 22:33:48 +00001102 return cls.objects.get(pk=id_or_name)
1103
1104
1105 def job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001106 """Returns the job if it exists, or else None."""
jamesren4a41e012010-07-16 22:33:48 +00001107 jobs = self.job_set.all()
1108 assert jobs.count() <= 1
1109 return jobs and jobs[0] or None
1110
1111
1112 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001113 """Metadata for class ParameterizedJob."""
jamesren4a41e012010-07-16 22:33:48 +00001114 db_table = 'afe_parameterized_jobs'
1115
1116 def __unicode__(self):
1117 return u'%s (parameterized) - %s' % (self.test.name, self.job())
1118
1119
1120class ParameterizedJobProfiler(dbmodels.Model):
1121 """
1122 A profiler to run on a parameterized job
1123 """
1124 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1125 profiler = dbmodels.ForeignKey(Profiler)
1126
1127 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001128 """Metedata for class ParameterizedJobProfiler."""
jamesren4a41e012010-07-16 22:33:48 +00001129 db_table = 'afe_parameterized_jobs_profilers'
1130 unique_together = ('parameterized_job', 'profiler')
1131
1132
1133class ParameterizedJobProfilerParameter(dbmodels.Model):
1134 """
1135 A parameter for a profiler in a parameterized job
1136 """
1137 parameterized_job_profiler = dbmodels.ForeignKey(ParameterizedJobProfiler)
1138 parameter_name = dbmodels.CharField(max_length=255)
1139 parameter_value = dbmodels.TextField()
1140 parameter_type = dbmodels.CharField(
1141 max_length=8, choices=model_attributes.ParameterTypes.choices())
1142
1143 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001144 """Metadata for class ParameterizedJobProfilerParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001145 db_table = 'afe_parameterized_job_profiler_parameters'
1146 unique_together = ('parameterized_job_profiler', 'parameter_name')
1147
1148 def __unicode__(self):
1149 return u'%s - %s' % (self.parameterized_job_profiler.profiler.name,
1150 self.parameter_name)
1151
1152
1153class ParameterizedJobParameter(dbmodels.Model):
1154 """
1155 Parameters for a parameterized job
1156 """
1157 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1158 test_parameter = dbmodels.ForeignKey(TestParameter)
1159 parameter_value = dbmodels.TextField()
1160 parameter_type = dbmodels.CharField(
1161 max_length=8, choices=model_attributes.ParameterTypes.choices())
1162
1163 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001164 """Metadata for class ParameterizedJobParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001165 db_table = 'afe_parameterized_job_parameters'
1166 unique_together = ('parameterized_job', 'test_parameter')
1167
1168 def __unicode__(self):
1169 return u'%s - %s' % (self.parameterized_job.job().name,
1170 self.test_parameter.name)
1171
1172
showard7c785282008-05-29 19:45:12 +00001173class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +00001174 'Custom manager to provide efficient status counts querying.'
1175 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001176 """Returns a dict mapping the given job IDs to their status count dicts.
1177
1178 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +00001179 """
1180 if not job_ids:
1181 return {}
1182 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1183 cursor = connection.cursor()
1184 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +00001185 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +00001186 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +00001187 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +00001188 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +00001189 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +00001190 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +00001191 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +00001192 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +00001193 full_status = HostQueueEntry.compute_full_status(status, aborted,
1194 complete)
showardb6d16622009-05-26 19:35:29 +00001195 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +00001196 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +00001197 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +00001198
1199
showard7c785282008-05-29 19:45:12 +00001200class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001201 """\
1202 owner: username of job owner
1203 name: job name (does not have to be unique)
Alex Miller7d658cf2013-09-04 16:00:35 -07001204 priority: Integer priority value. Higher is more important.
jadmanski0afbb632008-06-06 21:10:57 +00001205 control_file: contents of control file
1206 control_type: Client or Server
1207 created_on: date of job creation
1208 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +00001209 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +00001210 run_verify: Whether or not to run the verify phase
Dan Shi07e09af2013-04-12 09:31:29 -07001211 run_reset: Whether or not to run the reset phase
Simran Basi94dc0032013-11-12 14:09:46 -08001212 timeout: DEPRECATED - hours from queuing time until job times out
1213 timeout_mins: minutes from job queuing time until the job times out
Simran Basi34217022012-11-06 13:43:15 -08001214 max_runtime_hrs: DEPRECATED - hours from job starting time until job
1215 times out
1216 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +00001217 email_list: list of people to email on completion delimited by any of:
1218 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +00001219 dependency_labels: many-to-many relationship with labels corresponding to
1220 job dependencies
showard21baa452008-10-21 00:08:39 +00001221 reboot_before: Never, If dirty, or Always
1222 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001223 parse_failed_repair: if True, a failed repair launched by this job will have
1224 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001225 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001226 parent_job: Parent job (optional)
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001227 test_retry: Number of times to retry test if the test did not complete
1228 successfully. (optional, default: 0)
Dan Shic9e17142015-02-19 11:50:55 -08001229 require_ssp: Require server-side packaging unless require_ssp is set to
1230 False. (optional, default: None)
jadmanski0afbb632008-06-06 21:10:57 +00001231 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001232
1233 # TODO: Investigate, if jobkeyval_set is really needed.
1234 # dynamic_suite will write them into an attached file for the drone, but
1235 # it doesn't seem like they are actually used. If they aren't used, remove
1236 # jobkeyval_set here.
1237 SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1238 'hostqueueentry_set',
1239 'jobkeyval_set',
1240 'shard'])
1241
1242
Jakob Juelichf88fa932014-09-03 17:58:04 -07001243 def _deserialize_relation(self, link, data):
1244 if link in ['hostqueueentry_set', 'jobkeyval_set']:
1245 for obj in data:
1246 obj['job_id'] = self.id
1247
1248 super(Job, self)._deserialize_relation(link, data)
1249
1250
1251 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001252 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -07001253 self.shard = Shard.deserialize(data)
1254
1255
Jakob Juelicha94efe62014-09-18 16:02:49 -07001256 def sanity_check_update_from_shard(self, shard, updated_serialized):
Jakob Juelich02e61292014-10-17 12:36:55 -07001257 # If the job got aborted on the master after the client fetched it
1258 # no shard_id will be set. The shard might still push updates though,
1259 # as the job might complete before the abort bit syncs to the shard.
1260 # Alternative considered: The master scheduler could be changed to not
1261 # set aborted jobs to completed that are sharded out. But that would
1262 # require database queries and seemed more complicated to implement.
1263 # This seems safe to do, as there won't be updates pushed from the wrong
1264 # shards should be powered off and wiped hen they are removed from the
1265 # master.
1266 if self.shard_id and self.shard_id != shard.id:
Jakob Juelicha94efe62014-09-18 16:02:49 -07001267 raise error.UnallowedRecordsSentToMaster(
1268 'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1269 'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1270 shard.id))
1271
1272
Simran Basi94dc0032013-11-12 14:09:46 -08001273 # TIMEOUT is deprecated.
showardb1e51872008-10-07 11:08:18 +00001274 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
Simran Basi94dc0032013-11-12 14:09:46 -08001275 'AUTOTEST_WEB', 'job_timeout_default', default=24)
1276 DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1277 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
Simran Basi34217022012-11-06 13:43:15 -08001278 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1279 # completed.
showard12f3e322009-05-13 21:27:42 +00001280 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1281 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001282 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1283 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001284 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1285 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1286 default=False)
showardb1e51872008-10-07 11:08:18 +00001287
showarda5288b42009-07-28 20:06:08 +00001288 owner = dbmodels.CharField(max_length=255)
1289 name = dbmodels.CharField(max_length=255)
Alex Miller7d658cf2013-09-04 16:00:35 -07001290 priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
jamesren4a41e012010-07-16 22:33:48 +00001291 control_file = dbmodels.TextField(null=True, blank=True)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001292 control_type = dbmodels.SmallIntegerField(
1293 choices=control_data.CONTROL_TYPE.choices(),
1294 blank=True, # to allow 0
1295 default=control_data.CONTROL_TYPE.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001296 created_on = dbmodels.DateTimeField()
Simran Basi8d89b642014-05-02 17:33:20 -07001297 synch_count = dbmodels.IntegerField(blank=True, default=0)
showardb1e51872008-10-07 11:08:18 +00001298 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
Dan Shi07e09af2013-04-12 09:31:29 -07001299 run_verify = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001300 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001301 dependency_labels = (
1302 dbmodels.ManyToManyField(Label, blank=True,
1303 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001304 reboot_before = dbmodels.SmallIntegerField(
1305 choices=model_attributes.RebootBefore.choices(), blank=True,
1306 default=DEFAULT_REBOOT_BEFORE)
1307 reboot_after = dbmodels.SmallIntegerField(
1308 choices=model_attributes.RebootAfter.choices(), blank=True,
1309 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001310 parse_failed_repair = dbmodels.BooleanField(
1311 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001312 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1313 # completed.
showard12f3e322009-05-13 21:27:42 +00001314 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001315 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001316 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001317
jamesren4a41e012010-07-16 22:33:48 +00001318 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1319 blank=True)
1320
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001321 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001322
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001323 test_retry = dbmodels.IntegerField(blank=True, default=0)
1324
Dan Shi07e09af2013-04-12 09:31:29 -07001325 run_reset = dbmodels.BooleanField(default=True)
1326
Simran Basi94dc0032013-11-12 14:09:46 -08001327 timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1328
Jakob Juelich8421d592014-09-17 15:27:06 -07001329 # If this is None on the master, a slave should be found.
1330 # If this is None on a slave, it should be synced back to the master
Jakob Jülich92c06332014-08-25 19:06:57 +00001331 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1332
Dan Shic9e17142015-02-19 11:50:55 -08001333 # If this is None, server-side packaging will be used for server side test,
1334 # unless it's disabled in global config AUTOSERV/enable_ssp_container.
1335 require_ssp = dbmodels.NullBooleanField(default=None, blank=True, null=True)
1336
jadmanski0afbb632008-06-06 21:10:57 +00001337 # custom manager
1338 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001339
1340
Alex Millerec212252014-02-28 16:48:34 -08001341 @decorators.cached_property
1342 def labels(self):
1343 """All the labels of this job"""
1344 # We need to convert dependency_labels to a list, because all() gives us
1345 # back an iterator, and storing/caching an iterator means we'd only be
1346 # able to read from it once.
1347 return list(self.dependency_labels.all())
1348
1349
jadmanski0afbb632008-06-06 21:10:57 +00001350 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001351 """Returns whether this job is of type server."""
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001352 return self.control_type == control_data.CONTROL_TYPE.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001353
1354
jadmanski0afbb632008-06-06 21:10:57 +00001355 @classmethod
jamesren4a41e012010-07-16 22:33:48 +00001356 def parameterized_jobs_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001357 """Returns whether parameterized jobs are enabled.
1358
1359 @param cls: Implicit class object.
1360 """
jamesren4a41e012010-07-16 22:33:48 +00001361 return global_config.global_config.get_config_value(
1362 'AUTOTEST_WEB', 'parameterized_jobs', type=bool)
1363
1364
1365 @classmethod
1366 def check_parameterized_job(cls, control_file, parameterized_job):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001367 """Checks that the job is valid given the global config settings.
jamesren4a41e012010-07-16 22:33:48 +00001368
1369 First, either control_file must be set, or parameterized_job must be
1370 set, but not both. Second, parameterized_job must be set if and only if
1371 the parameterized_jobs option in the global config is set to True.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001372
1373 @param cls: Implict class object.
1374 @param control_file: A control file.
1375 @param parameterized_job: A parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001376 """
1377 if not (bool(control_file) ^ bool(parameterized_job)):
1378 raise Exception('Job must have either control file or '
1379 'parameterization, but not both')
1380
1381 parameterized_jobs_enabled = cls.parameterized_jobs_enabled()
1382 if control_file and parameterized_jobs_enabled:
1383 raise Exception('Control file specified, but parameterized jobs '
1384 'are enabled')
1385 if parameterized_job and not parameterized_jobs_enabled:
1386 raise Exception('Parameterized job specified, but parameterized '
1387 'jobs are not enabled')
1388
1389
1390 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001391 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001392 """Creates a job.
1393
1394 The job is created by taking some information (the listed args) and
1395 filling in the rest of the necessary information.
1396
1397 @param cls: Implicit class object.
1398 @param owner: The owner for the job.
1399 @param options: An options object.
1400 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001401 """
showard3dd47c22008-07-10 00:41:36 +00001402 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001403
jamesren4a41e012010-07-16 22:33:48 +00001404 control_file = options.get('control_file')
1405 parameterized_job = options.get('parameterized_job')
jamesren4a41e012010-07-16 22:33:48 +00001406
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001407 # The current implementation of parameterized jobs requires that only
1408 # control files or parameterized jobs are used. Using the image
1409 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1410 # parameterized jobs and control files jobs, it does muck enough with
1411 # normal jobs by adding a parameterized id to them that this check will
1412 # fail. So for now we just skip this check.
1413 # cls.check_parameterized_job(control_file=control_file,
1414 # parameterized_job=parameterized_job)
showard4d233752010-01-20 19:06:40 +00001415 user = User.current_user()
1416 if options.get('reboot_before') is None:
1417 options['reboot_before'] = user.get_reboot_before_display()
1418 if options.get('reboot_after') is None:
1419 options['reboot_after'] = user.get_reboot_after_display()
1420
jamesren76fcf192010-04-21 20:39:50 +00001421 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1422
Simran Basi94dc0032013-11-12 14:09:46 -08001423 if options.get('timeout_mins') is None and options.get('timeout'):
1424 options['timeout_mins'] = options['timeout'] * 60
1425
jadmanski0afbb632008-06-06 21:10:57 +00001426 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001427 owner=owner,
1428 name=options['name'],
1429 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001430 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001431 control_type=options['control_type'],
1432 synch_count=options.get('synch_count'),
Simran Basi94dc0032013-11-12 14:09:46 -08001433 # timeout needs to be deleted in the future.
showarda1e74b32009-05-12 17:32:04 +00001434 timeout=options.get('timeout'),
Simran Basi94dc0032013-11-12 14:09:46 -08001435 timeout_mins=options.get('timeout_mins'),
Simran Basi34217022012-11-06 13:43:15 -08001436 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001437 run_verify=options.get('run_verify'),
1438 email_list=options.get('email_list'),
1439 reboot_before=options.get('reboot_before'),
1440 reboot_after=options.get('reboot_after'),
1441 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001442 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001443 drone_set=drone_set,
Aviv Keshet18308922013-02-19 17:49:49 -08001444 parameterized_job=parameterized_job,
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001445 parent_job=options.get('parent_job_id'),
Dan Shi07e09af2013-04-12 09:31:29 -07001446 test_retry=options.get('test_retry'),
Dan Shic9e17142015-02-19 11:50:55 -08001447 run_reset=options.get('run_reset'),
1448 require_ssp=options.get('require_ssp'))
mblighe8819cd2008-02-15 16:48:40 +00001449
showarda1e74b32009-05-12 17:32:04 +00001450 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001451
jamesrend8b6e172010-04-16 23:45:00 +00001452 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001453 for key, value in options['keyvals'].iteritems():
1454 JobKeyval.objects.create(job=job, key=key, value=value)
1455
jadmanski0afbb632008-06-06 21:10:57 +00001456 return job
mblighe8819cd2008-02-15 16:48:40 +00001457
1458
Jakob Juelich59cfe542014-09-02 16:37:46 -07001459 @classmethod
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001460 def _add_filters_for_shard_assignment(cls, query, known_ids):
1461 """Exclude jobs that should be not sent to shard.
1462
1463 This is a helper that filters out the following jobs:
1464 - Non-aborted jobs known to shard as specified in |known_ids|.
1465 Note for jobs aborted on master, even if already known to shard,
1466 will be sent to shard again so that shard can abort them.
1467 - Completed jobs
1468 - Active jobs
1469 @param query: A query that finds jobs for shards, to which the 'exclude'
1470 filters will be applied.
1471 @param known_ids: List of all ids of incomplete jobs, the shard already
1472 knows about.
1473
1474 @returns: A django QuerySet after filtering out unnecessary jobs.
1475
1476 """
1477 return query.exclude(
1478 id__in=known_ids,
1479 hostqueueentry__aborted=False
1480 ).exclude(
1481 hostqueueentry__complete=True
1482 ).exclude(
1483 hostqueueentry__active=True)
1484
1485
1486 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -07001487 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -07001488 """Assigns unassigned jobs to a shard.
1489
Jakob Juelich1b525742014-09-30 13:08:07 -07001490 For all labels that have been assigned to this shard, all jobs that
1491 have this label, are assigned to this shard.
1492
1493 Jobs that are assigned to the shard but aren't already present on the
1494 shard are returned.
1495
Jakob Juelich59cfe542014-09-02 16:37:46 -07001496 @param shard: The shard to assign jobs to.
Jakob Juelich1b525742014-09-30 13:08:07 -07001497 @param known_ids: List of all ids of incomplete jobs, the shard already
1498 knows about.
1499 This is used to figure out which jobs should be sent
1500 to the shard. If shard_ids were used instead, jobs
1501 would only be transferred once, even if the client
1502 failed persisting them.
1503 The number of unfinished jobs usually lies in O(1000).
1504 Assuming one id takes 8 chars in the json, this means
1505 overhead that lies in the lower kilobyte range.
1506 A not in query with 5000 id's takes about 30ms.
1507
Jakob Juelich59cfe542014-09-02 16:37:46 -07001508 @returns The job objects that should be sent to the shard.
1509 """
1510 # Disclaimer: Concurrent heartbeats should not occur in today's setup.
1511 # If this changes or they are triggered manually, this applies:
1512 # Jobs may be returned more than once by concurrent calls of this
1513 # function, as there is a race condition between SELECT and UPDATE.
MK Ryu06a4b522015-04-24 15:06:10 -07001514 query = Job.objects.filter(
MK Ryu638a6cf2015-05-08 14:49:43 -07001515 dependency_labels__in=shard.labels.all(),
MK Ryu06a4b522015-04-24 15:06:10 -07001516 # If an HQE associated with a job is removed in some reasons,
1517 # such jobs should be excluded. Refer crbug.com/479766
1518 hostqueueentry__isnull=False
1519 )
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001520 query = cls._add_filters_for_shard_assignment(query, known_ids)
1521 job_ids = set(query.distinct().values_list('pk', flat=True))
1522
1523 # Combine frontend jobs in the heartbeat.
1524 query = Job.objects.filter(
1525 hostqueueentry__meta_host__isnull=True,
1526 hostqueueentry__host__isnull=False,
MK Ryu638a6cf2015-05-08 14:49:43 -07001527 hostqueueentry__host__labels__in=shard.labels.all()
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001528 )
1529 query = cls._add_filters_for_shard_assignment(query, known_ids)
1530 job_ids |= set(query.distinct().values_list('pk', flat=True))
Jakob Juelich59cfe542014-09-02 16:37:46 -07001531 if job_ids:
1532 Job.objects.filter(pk__in=job_ids).update(shard=shard)
1533 return list(Job.objects.filter(pk__in=job_ids).all())
1534 return []
1535
1536
jamesren4a41e012010-07-16 22:33:48 +00001537 def save(self, *args, **kwargs):
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001538 # The current implementation of parameterized jobs requires that only
1539 # control files or parameterized jobs are used. Using the image
1540 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1541 # parameterized jobs and control files jobs, it does muck enough with
1542 # normal jobs by adding a parameterized id to them that this check will
1543 # fail. So for now we just skip this check.
1544 # cls.check_parameterized_job(control_file=self.control_file,
1545 # parameterized_job=self.parameterized_job)
jamesren4a41e012010-07-16 22:33:48 +00001546 super(Job, self).save(*args, **kwargs)
1547
1548
showard29f7cd22009-04-29 21:16:24 +00001549 def queue(self, hosts, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001550 """Enqueue a job on the given hosts.
1551
1552 @param hosts: The hosts to use.
1553 @param atomic_group: The associated atomic group.
1554 @param is_template: Whether the status should be "Template".
1555 """
showarda9545c02009-12-18 22:44:26 +00001556 if not hosts:
1557 if atomic_group:
1558 # No hosts or labels are required to queue an atomic group
1559 # Job. However, if they are given, we respect them below.
1560 atomic_group.enqueue_job(self, is_template=is_template)
1561 else:
1562 # hostless job
1563 entry = HostQueueEntry.create(job=self, is_template=is_template)
1564 entry.save()
1565 return
1566
jadmanski0afbb632008-06-06 21:10:57 +00001567 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +00001568 host.enqueue_job(self, atomic_group=atomic_group,
1569 is_template=is_template)
1570
1571
1572 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001573 """Creates a recurring job.
1574
1575 @param start_date: The starting date of the job.
1576 @param loop_period: How often to re-run the job, in seconds.
1577 @param loop_count: The re-run count.
1578 @param owner: The owner of the job.
1579 """
showard29f7cd22009-04-29 21:16:24 +00001580 rec = RecurringRun(job=self, start_date=start_date,
1581 loop_period=loop_period,
1582 loop_count=loop_count,
1583 owner=User.objects.get(login=owner))
1584 rec.save()
1585 return rec.id
mblighe8819cd2008-02-15 16:48:40 +00001586
1587
jadmanski0afbb632008-06-06 21:10:57 +00001588 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001589 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001590 try:
1591 return User.objects.get(login=self.owner)
1592 except self.DoesNotExist:
1593 return None
mblighe8819cd2008-02-15 16:48:40 +00001594
1595
showard64a95952010-01-13 21:27:16 +00001596 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001597 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001598 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001599 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001600
1601
showardd1195652009-12-08 22:21:02 +00001602 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001603 """Returns a string tag for this job."""
MK Ryu0c1a37d2015-04-30 12:00:55 -07001604 return server_utils.get_job_tag(self.id, self.owner)
showardd1195652009-12-08 22:21:02 +00001605
1606
showardc1a98d12010-01-15 00:22:22 +00001607 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001608 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001609 return dict((keyval.key, keyval.value)
1610 for keyval in self.jobkeyval_set.all())
1611
1612
Fang Dengff361592015-02-02 15:27:34 -08001613 @classmethod
1614 def get_attribute_model(cls):
1615 """Return the attribute model.
1616
1617 Override method in parent class. This class is called when
1618 deserializing the one-to-many relationship betwen Job and JobKeyval.
1619 On deserialization, we will try to clear any existing job keyvals
1620 associated with a job to avoid any inconsistency.
1621 Though Job doesn't implement ModelWithAttribute, we still treat
1622 it as an attribute model for this purpose.
1623
1624 @returns: The attribute model of Job.
1625 """
1626 return JobKeyval
1627
1628
jadmanski0afbb632008-06-06 21:10:57 +00001629 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001630 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001631 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001632
showarda5288b42009-07-28 20:06:08 +00001633 def __unicode__(self):
1634 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001635
1636
showardc1a98d12010-01-15 00:22:22 +00001637class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1638 """Keyvals associated with jobs"""
Fang Dengff361592015-02-02 15:27:34 -08001639
1640 SERIALIZATION_LINKS_TO_KEEP = set(['job'])
1641 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
1642
showardc1a98d12010-01-15 00:22:22 +00001643 job = dbmodels.ForeignKey(Job)
1644 key = dbmodels.CharField(max_length=90)
1645 value = dbmodels.CharField(max_length=300)
1646
1647 objects = model_logic.ExtendedManager()
1648
Fang Dengff361592015-02-02 15:27:34 -08001649
1650 @classmethod
1651 def get_record(cls, data):
1652 """Check the database for an identical record.
1653
1654 Use job_id and key to search for a existing record.
1655
1656 @raises: DoesNotExist, if no record found
1657 @raises: MultipleObjectsReturned if multiple records found.
1658 """
1659 # TODO(fdeng): We should use job_id and key together as
1660 # a primary key in the db.
1661 return cls.objects.get(job_id=data['job_id'], key=data['key'])
1662
1663
1664 @classmethod
1665 def deserialize(cls, data):
1666 """Override deserialize in parent class.
1667
1668 Do not deserialize id as id is not kept consistent on master and shards.
1669
1670 @param data: A dictionary of data to deserialize.
1671
1672 @returns: A JobKeyval object.
1673 """
1674 if data:
1675 data.pop('id')
1676 return super(JobKeyval, cls).deserialize(data)
1677
1678
showardc1a98d12010-01-15 00:22:22 +00001679 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001680 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001681 db_table = 'afe_job_keyvals'
1682
1683
showard7c785282008-05-29 19:45:12 +00001684class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001685 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001686 job = dbmodels.ForeignKey(Job)
1687 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001688
jadmanski0afbb632008-06-06 21:10:57 +00001689 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001690
jadmanski0afbb632008-06-06 21:10:57 +00001691 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001692 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001693 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001694
mblighe8819cd2008-02-15 16:48:40 +00001695
showard7c785282008-05-29 19:45:12 +00001696class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001697 """Represents a host queue entry."""
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001698
1699 SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001700 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Jakob Juelichf865d332014-09-29 10:47:49 -07001701 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001702
Jakob Juelichf88fa932014-09-03 17:58:04 -07001703
1704 def custom_deserialize_relation(self, link, data):
1705 assert link == 'meta_host'
1706 self.meta_host = Label.deserialize(data)
1707
1708
Jakob Juelicha94efe62014-09-18 16:02:49 -07001709 def sanity_check_update_from_shard(self, shard, updated_serialized,
1710 job_ids_sent):
1711 if self.job_id not in job_ids_sent:
1712 raise error.UnallowedRecordsSentToMaster(
1713 'Sent HostQueueEntry without corresponding '
1714 'job entry: %s' % updated_serialized)
1715
1716
showardeaa408e2009-09-11 18:45:31 +00001717 Status = host_queue_entry_states.Status
1718 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001719 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001720
jadmanski0afbb632008-06-06 21:10:57 +00001721 job = dbmodels.ForeignKey(Job)
1722 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001723 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001724 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1725 db_column='meta_host')
1726 active = dbmodels.BooleanField(default=False)
1727 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001728 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001729 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1730 default='')
showard89f84db2009-03-12 20:39:13 +00001731 # If atomic_group is set, this is a virtual HostQueueEntry that will
1732 # be expanded into many actual hosts within the group at schedule time.
1733 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001734 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001735 started_on = dbmodels.DateTimeField(null=True, blank=True)
Fang Deng51599032014-06-23 17:24:27 -07001736 finished_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001737
jadmanski0afbb632008-06-06 21:10:57 +00001738 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001739
mblighe8819cd2008-02-15 16:48:40 +00001740
showard2bab8f42008-11-12 18:15:22 +00001741 def __init__(self, *args, **kwargs):
1742 super(HostQueueEntry, self).__init__(*args, **kwargs)
1743 self._record_attributes(['status'])
1744
1745
showard29f7cd22009-04-29 21:16:24 +00001746 @classmethod
1747 def create(cls, job, host=None, meta_host=None, atomic_group=None,
1748 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001749 """Creates a new host queue entry.
1750
1751 @param cls: Implicit class object.
1752 @param job: The associated job.
1753 @param host: The associated host.
1754 @param meta_host: The associated meta host.
1755 @param atomic_group: The associated atomic group.
1756 @param is_template: Whether the status should be "Template".
1757 """
showard29f7cd22009-04-29 21:16:24 +00001758 if is_template:
1759 status = cls.Status.TEMPLATE
1760 else:
1761 status = cls.Status.QUEUED
1762
1763 return cls(job=job, host=host, meta_host=meta_host,
1764 atomic_group=atomic_group, status=status)
1765
1766
showarda5288b42009-07-28 20:06:08 +00001767 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001768 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001769 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001770 self._check_for_updated_attributes()
1771
1772
showardc0ac3a72009-07-08 21:14:45 +00001773 def execution_path(self):
1774 """
1775 Path to this entry's results (relative to the base results directory).
1776 """
MK Ryu0c1a37d2015-04-30 12:00:55 -07001777 return server_utils.get_hqe_exec_path(self.job.tag(),
1778 self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001779
1780
showard3f15eed2008-11-14 22:40:48 +00001781 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001782 """Returns the first non-None name found in priority order.
1783
1784 The priority order checked is: (1) host name; (2) meta host name; and
1785 (3) atomic group name.
1786 """
showard3f15eed2008-11-14 22:40:48 +00001787 if self.host:
1788 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001789 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +00001790 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +00001791 else:
1792 assert self.atomic_group, "no host, meta_host or atomic group!"
1793 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +00001794
1795
showard2bab8f42008-11-12 18:15:22 +00001796 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001797 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001798 self.active, self.complete = True, False
1799 elif self.status in self.COMPLETE_STATUSES:
1800 self.active, self.complete = False, True
1801 else:
1802 self.active, self.complete = False, False
1803
1804
1805 def on_attribute_changed(self, attribute, old_value):
1806 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001807 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1808 self.status)
showard2bab8f42008-11-12 18:15:22 +00001809
1810
jadmanski0afbb632008-06-06 21:10:57 +00001811 def is_meta_host_entry(self):
1812 'True if this is a entry has a meta_host instead of a host.'
1813 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001814
showarda3ab0d52008-11-03 19:03:47 +00001815
Simran Basic1b26762013-06-26 14:23:21 -07001816 # This code is shared between rpc_interface and models.HostQueueEntry.
1817 # Sadly due to circular imports between the 2 (crbug.com/230100) making it
1818 # a class method was the best way to refactor it. Attempting to put it in
1819 # rpc_utils or a new utils module failed as that would require us to import
1820 # models.py but to call it from here we would have to import the utils.py
1821 # thus creating a cycle.
1822 @classmethod
1823 def abort_host_queue_entries(cls, host_queue_entries):
1824 """Aborts a collection of host_queue_entries.
1825
1826 Abort these host queue entry and all host queue entries of jobs created
1827 by them.
1828
1829 @param host_queue_entries: List of host queue entries we want to abort.
1830 """
1831 # This isn't completely immune to race conditions since it's not atomic,
1832 # but it should be safe given the scheduler's behavior.
1833
1834 # TODO(milleral): crbug.com/230100
1835 # The |abort_host_queue_entries| rpc does nearly exactly this,
1836 # however, trying to re-use the code generates some horrible
1837 # circular import error. I'd be nice to refactor things around
1838 # sometime so the code could be reused.
1839
1840 # Fixpoint algorithm to find the whole tree of HQEs to abort to
1841 # minimize the total number of database queries:
1842 children = set()
1843 new_children = set(host_queue_entries)
1844 while new_children:
1845 children.update(new_children)
1846 new_child_ids = [hqe.job_id for hqe in new_children]
1847 new_children = HostQueueEntry.objects.filter(
1848 job__parent_job__in=new_child_ids,
1849 complete=False, aborted=False).all()
1850 # To handle circular parental relationships
1851 new_children = set(new_children) - children
1852
1853 # Associate a user with the host queue entries that we're about
1854 # to abort so that we can look up who to blame for the aborts.
1855 now = datetime.now()
1856 user = User.current_user()
1857 aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
1858 aborted_by=user, aborted_on=now) for hqe in children]
1859 AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
1860 # Bulk update all of the HQEs to set the abort bit.
1861 child_ids = [hqe.id for hqe in children]
1862 HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
1863
1864
Scott Zawalski23041432013-04-17 07:39:09 -07001865 def abort(self):
Alex Millerdea67042013-04-22 17:23:34 -07001866 """ Aborts this host queue entry.
Simran Basic1b26762013-06-26 14:23:21 -07001867
Alex Millerdea67042013-04-22 17:23:34 -07001868 Abort this host queue entry and all host queue entries of jobs created by
1869 this one.
1870
1871 """
showardd3dc1992009-04-22 21:01:40 +00001872 if not self.complete and not self.aborted:
Simran Basi97582a22013-06-27 12:03:21 -07001873 HostQueueEntry.abort_host_queue_entries([self])
mblighe8819cd2008-02-15 16:48:40 +00001874
showardd3dc1992009-04-22 21:01:40 +00001875
1876 @classmethod
1877 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001878 """Returns a modified status msg if the host queue entry was aborted.
1879
1880 @param cls: Implicit class object.
1881 @param status: The original status message.
1882 @param aborted: Whether the host queue entry was aborted.
1883 @param complete: Whether the host queue entry was completed.
1884 """
showardd3dc1992009-04-22 21:01:40 +00001885 if aborted and not complete:
1886 return 'Aborted (%s)' % status
1887 return status
1888
1889
1890 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001891 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00001892 return self.compute_full_status(self.status, self.aborted,
1893 self.complete)
1894
1895
1896 def _postprocess_object_dict(self, object_dict):
1897 object_dict['full_status'] = self.full_status()
1898
1899
jadmanski0afbb632008-06-06 21:10:57 +00001900 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001901 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001902 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00001903
showard12f3e322009-05-13 21:27:42 +00001904
showard4c119042008-09-29 19:16:18 +00001905
showarda5288b42009-07-28 20:06:08 +00001906 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00001907 hostname = None
1908 if self.host:
1909 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00001910 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00001911
1912
showard4c119042008-09-29 19:16:18 +00001913class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001914 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00001915 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
1916 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00001917 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00001918
1919 objects = model_logic.ExtendedManager()
1920
showard68c7aa02008-10-09 16:49:11 +00001921
showarda5288b42009-07-28 20:06:08 +00001922 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00001923 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00001924 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00001925
showard4c119042008-09-29 19:16:18 +00001926 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001927 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001928 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00001929
1930
1931class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
1932 """\
1933 job: job to use as a template
1934 owner: owner of the instantiated template
1935 start_date: Run the job at scheduled date
1936 loop_period: Re-run (loop) the job periodically
1937 (in every loop_period seconds)
1938 loop_count: Re-run (loop) count
1939 """
1940
1941 job = dbmodels.ForeignKey(Job)
1942 owner = dbmodels.ForeignKey(User)
1943 start_date = dbmodels.DateTimeField()
1944 loop_period = dbmodels.IntegerField(blank=True)
1945 loop_count = dbmodels.IntegerField(blank=True)
1946
1947 objects = model_logic.ExtendedManager()
1948
1949 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001950 """Metadata for class RecurringRun."""
showardeab66ce2009-12-23 00:03:56 +00001951 db_table = 'afe_recurring_run'
showard29f7cd22009-04-29 21:16:24 +00001952
showarda5288b42009-07-28 20:06:08 +00001953 def __unicode__(self):
1954 return u'RecurringRun(job %s, start %s, period %s, count %s)' % (
showard29f7cd22009-04-29 21:16:24 +00001955 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +00001956
1957
1958class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
1959 """\
1960 Tasks to run on hosts at the next time they are in the Ready state. Use this
1961 for high-priority tasks, such as forced repair or forced reinstall.
1962
1963 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00001964 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00001965 time_requested: date and time the request for this task was made
1966 is_active: task is currently running
1967 is_complete: task has finished running
beeps8bb1f7d2013-08-05 01:30:09 -07001968 is_aborted: task was aborted
showard2fe3f1d2009-07-06 20:19:11 +00001969 time_started: date and time the task started
Dan Shid0725542014-06-23 15:34:27 -07001970 time_finished: date and time the task finished
showard2fe3f1d2009-07-06 20:19:11 +00001971 queue_entry: Host queue entry waiting on this task (or None, if task was not
1972 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00001973 """
Alex Millerdfff2fd2013-05-28 13:05:06 -07001974 Task = enum.Enum('Verify', 'Cleanup', 'Repair', 'Reset', 'Provision',
Dan Shi07e09af2013-04-12 09:31:29 -07001975 string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00001976
1977 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00001978 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00001979 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00001980 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00001981 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
1982 null=False)
1983 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
1984 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
beeps8bb1f7d2013-08-05 01:30:09 -07001985 is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00001986 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00001987 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00001988 success = dbmodels.BooleanField(default=False, blank=False, null=False)
Dan Shid0725542014-06-23 15:34:27 -07001989 time_finished = dbmodels.DateTimeField(null=True, blank=True)
showard6d7b2ff2009-06-10 00:16:47 +00001990
1991 objects = model_logic.ExtendedManager()
1992
1993
showard9bb960b2009-11-19 01:02:11 +00001994 def save(self, **kwargs):
1995 if self.queue_entry:
1996 self.requested_by = User.objects.get(
1997 login=self.queue_entry.job.owner)
1998 super(SpecialTask, self).save(**kwargs)
1999
2000
showarded2afea2009-07-07 20:54:07 +00002001 def execution_path(self):
MK Ryu0c1a37d2015-04-30 12:00:55 -07002002 """Returns the execution path for a special task."""
2003 return server_utils.get_special_task_exec_path(
2004 self.host.hostname, self.id, self.task, self.time_requested)
showarded2afea2009-07-07 20:54:07 +00002005
2006
showardc0ac3a72009-07-08 21:14:45 +00002007 # property to emulate HostQueueEntry.status
2008 @property
2009 def status(self):
MK Ryu0c1a37d2015-04-30 12:00:55 -07002010 """Returns a host queue entry status appropriate for a speical task."""
2011 return server_utils.get_special_task_status(
2012 self.is_complete, self.success, self.is_active)
showardc0ac3a72009-07-08 21:14:45 +00002013
2014
2015 # property to emulate HostQueueEntry.started_on
2016 @property
2017 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002018 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00002019 return self.time_started
2020
2021
showard6d7b2ff2009-06-10 00:16:47 +00002022 @classmethod
showardc5103442010-01-15 00:20:26 +00002023 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002024 """Schedules a special task on a host if not already scheduled.
2025
2026 @param cls: Implicit class object.
2027 @param host: The host to use.
2028 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00002029 """
showardc5103442010-01-15 00:20:26 +00002030 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
2031 is_active=False,
2032 is_complete=False)
2033 if existing_tasks:
2034 return existing_tasks[0]
2035
2036 special_task = SpecialTask(host=host, task=task,
2037 requested_by=User.current_user())
2038 special_task.save()
2039 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00002040
2041
beeps8bb1f7d2013-08-05 01:30:09 -07002042 def abort(self):
2043 """ Abort this special task."""
2044 self.is_aborted = True
2045 self.save()
2046
2047
showarded2afea2009-07-07 20:54:07 +00002048 def activate(self):
showard474d1362009-08-20 23:32:01 +00002049 """
2050 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00002051 """
showard97446882009-07-20 22:37:28 +00002052 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00002053 self.is_active = True
2054 self.time_started = datetime.now()
2055 self.save()
2056
2057
showarde60e44e2009-11-13 20:45:38 +00002058 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002059 """Sets a task as completed.
2060
2061 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00002062 """
showard97446882009-07-20 22:37:28 +00002063 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00002064 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00002065 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00002066 self.success = success
Dan Shid85d6112014-07-14 10:32:55 -07002067 if self.time_started:
2068 self.time_finished = datetime.now()
showard2fe3f1d2009-07-06 20:19:11 +00002069 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00002070
2071
2072 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002073 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00002074 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00002075
showard474d1362009-08-20 23:32:01 +00002076
showarda5288b42009-07-28 20:06:08 +00002077 def __unicode__(self):
2078 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00002079 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00002080 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00002081 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00002082 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00002083 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00002084
2085 return result
Dan Shi6964fa52014-12-18 11:04:27 -08002086
2087
2088class StableVersion(dbmodels.Model, model_logic.ModelExtensions):
2089
2090 board = dbmodels.CharField(max_length=255, unique=True)
2091 version = dbmodels.CharField(max_length=255)
2092
2093 class Meta:
2094 """Metadata for class StableVersion."""
Fang Deng86248502014-12-18 16:38:00 -08002095 db_table = 'afe_stable_versions'