blob: 2f1b97a53d1719434fb18763a28dfaeed1d89526 [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
mblighe8819cd2008-02-15 16:48:40 +000025
showard0fc38302008-10-23 00:44:07 +000026# job options and user preferences
jamesrendd855242010-03-02 22:23:44 +000027DEFAULT_REBOOT_BEFORE = model_attributes.RebootBefore.IF_DIRTY
Dan Shi07e09af2013-04-12 09:31:29 -070028DEFAULT_REBOOT_AFTER = model_attributes.RebootBefore.NEVER
mblighe8819cd2008-02-15 16:48:40 +000029
showard89f84db2009-03-12 20:39:13 +000030
mblighe8819cd2008-02-15 16:48:40 +000031class AclAccessViolation(Exception):
jadmanski0afbb632008-06-06 21:10:57 +000032 """\
33 Raised when an operation is attempted with proper permissions as
34 dictated by ACLs.
35 """
mblighe8819cd2008-02-15 16:48:40 +000036
37
showard205fd602009-03-21 00:17:35 +000038class AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model):
showard89f84db2009-03-12 20:39:13 +000039 """\
40 An atomic group defines a collection of hosts which must only be scheduled
41 all at once. Any host with a label having an atomic group will only be
42 scheduled for a job at the same time as other hosts sharing that label.
43
44 Required:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080045 name: A name for this atomic group, e.g. 'rack23' or 'funky_net'.
showard89f84db2009-03-12 20:39:13 +000046 max_number_of_machines: The maximum number of machines that will be
47 scheduled at once when scheduling jobs to this atomic group.
48 The job.synch_count is considered the minimum.
49
50 Optional:
51 description: Arbitrary text description of this group's purpose.
52 """
showarda5288b42009-07-28 20:06:08 +000053 name = dbmodels.CharField(max_length=255, unique=True)
showard89f84db2009-03-12 20:39:13 +000054 description = dbmodels.TextField(blank=True)
showarde9450c92009-06-30 01:58:52 +000055 # This magic value is the default to simplify the scheduler logic.
56 # It must be "large". The common use of atomic groups is to want all
57 # machines in the group to be used, limits on which subset used are
58 # often chosen via dependency labels.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080059 # TODO(dennisjeffrey): Revisit this so we don't have to assume that
60 # "infinity" is around 3.3 million.
showarde9450c92009-06-30 01:58:52 +000061 INFINITE_MACHINES = 333333333
62 max_number_of_machines = dbmodels.IntegerField(default=INFINITE_MACHINES)
showard205fd602009-03-21 00:17:35 +000063 invalid = dbmodels.BooleanField(default=False,
showarda5288b42009-07-28 20:06:08 +000064 editable=settings.FULL_ADMIN)
showard89f84db2009-03-12 20:39:13 +000065
showard89f84db2009-03-12 20:39:13 +000066 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +000067 objects = model_logic.ModelWithInvalidManager()
showard205fd602009-03-21 00:17:35 +000068 valid_objects = model_logic.ValidObjectsManager()
69
70
showard29f7cd22009-04-29 21:16:24 +000071 def enqueue_job(self, job, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080072 """Enqueue a job on an associated atomic group of hosts.
73
74 @param job: A job to enqueue.
75 @param is_template: Whether the status should be "Template".
76 """
showard29f7cd22009-04-29 21:16:24 +000077 queue_entry = HostQueueEntry.create(atomic_group=self, job=job,
78 is_template=is_template)
showardc92da832009-04-07 18:14:34 +000079 queue_entry.save()
80
81
showard205fd602009-03-21 00:17:35 +000082 def clean_object(self):
83 self.label_set.clear()
showard89f84db2009-03-12 20:39:13 +000084
85
86 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080087 """Metadata for class AtomicGroup."""
showardeab66ce2009-12-23 00:03:56 +000088 db_table = 'afe_atomic_groups'
showard89f84db2009-03-12 20:39:13 +000089
showard205fd602009-03-21 00:17:35 +000090
showarda5288b42009-07-28 20:06:08 +000091 def __unicode__(self):
92 return unicode(self.name)
showard89f84db2009-03-12 20:39:13 +000093
94
showard7c785282008-05-29 19:45:12 +000095class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000096 """\
97 Required:
showard89f84db2009-03-12 20:39:13 +000098 name: label name
mblighe8819cd2008-02-15 16:48:40 +000099
jadmanski0afbb632008-06-06 21:10:57 +0000100 Optional:
showard89f84db2009-03-12 20:39:13 +0000101 kernel_config: URL/path to kernel config for jobs run on this label.
102 platform: If True, this is a platform label (defaults to False).
103 only_if_needed: If True, a Host with this label can only be used if that
104 label is requested by the job/test (either as the meta_host or
105 in the job_dependencies).
106 atomic_group: The atomic group associated with this label.
jadmanski0afbb632008-06-06 21:10:57 +0000107 """
showarda5288b42009-07-28 20:06:08 +0000108 name = dbmodels.CharField(max_length=255, unique=True)
109 kernel_config = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000110 platform = dbmodels.BooleanField(default=False)
111 invalid = dbmodels.BooleanField(default=False,
112 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +0000113 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +0000114
jadmanski0afbb632008-06-06 21:10:57 +0000115 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +0000116 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000117 valid_objects = model_logic.ValidObjectsManager()
showard89f84db2009-03-12 20:39:13 +0000118 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
119
mbligh5244cbb2008-04-24 20:39:52 +0000120
jadmanski0afbb632008-06-06 21:10:57 +0000121 def clean_object(self):
122 self.host_set.clear()
showard01a51672009-05-29 18:42:37 +0000123 self.test_set.clear()
mblighe8819cd2008-02-15 16:48:40 +0000124
125
showard29f7cd22009-04-29 21:16:24 +0000126 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800127 """Enqueue a job on any host of this label.
128
129 @param job: A job to enqueue.
130 @param atomic_group: The associated atomic group.
131 @param is_template: Whether the status should be "Template".
132 """
showard29f7cd22009-04-29 21:16:24 +0000133 queue_entry = HostQueueEntry.create(meta_host=self, job=job,
134 is_template=is_template,
135 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000136 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000137
138
jadmanski0afbb632008-06-06 21:10:57 +0000139 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800140 """Metadata for class Label."""
showardeab66ce2009-12-23 00:03:56 +0000141 db_table = 'afe_labels'
mblighe8819cd2008-02-15 16:48:40 +0000142
showarda5288b42009-07-28 20:06:08 +0000143 def __unicode__(self):
144 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000145
146
Jakob Jülich92c06332014-08-25 19:06:57 +0000147class Shard(dbmodels.Model, model_logic.ModelExtensions):
148
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700149 hostname = dbmodels.CharField(max_length=255, unique=True)
150
151 name_field = 'hostname'
152
Jakob Jülich92c06332014-08-25 19:06:57 +0000153 labels = dbmodels.ManyToManyField(Label, blank=True,
154 db_table='afe_shards_labels')
155
156 class Meta:
157 """Metadata for class ParameterizedJob."""
158 db_table = 'afe_shards'
159
160
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -0800161 def rpc_hostname(self):
162 """Get the rpc hostname of the shard.
163
164 @return: Just the shard hostname for all non-testing environments.
165 The address of the default gateway for vm testing environments.
166 """
167 # TODO: Figure out a better solution for testing. Since no 2 shards
168 # can run on the same host, if the shard hostname is localhost we
169 # conclude that it must be a vm in a test cluster. In such situations
170 # a name of localhost:<port> is necessary to achieve the correct
171 # afe links/redirection from the frontend (this happens through the
172 # host), but for rpcs that are performed *on* the shard, they need to
173 # use the address of the gateway.
174 hostname = self.hostname.split(':')[0]
175 if site_utils.is_localhost(hostname):
176 return self.hostname.replace(
177 hostname, site_utils.DEFAULT_VM_GATEWAY)
178 return self.hostname
179
180
jamesren76fcf192010-04-21 20:39:50 +0000181class Drone(dbmodels.Model, model_logic.ModelExtensions):
182 """
183 A scheduler drone
184
185 hostname: the drone's hostname
186 """
187 hostname = dbmodels.CharField(max_length=255, unique=True)
188
189 name_field = 'hostname'
190 objects = model_logic.ExtendedManager()
191
192
193 def save(self, *args, **kwargs):
194 if not User.current_user().is_superuser():
195 raise Exception('Only superusers may edit drones')
196 super(Drone, self).save(*args, **kwargs)
197
198
199 def delete(self):
200 if not User.current_user().is_superuser():
201 raise Exception('Only superusers may delete drones')
202 super(Drone, self).delete()
203
204
205 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800206 """Metadata for class Drone."""
jamesren76fcf192010-04-21 20:39:50 +0000207 db_table = 'afe_drones'
208
209 def __unicode__(self):
210 return unicode(self.hostname)
211
212
213class DroneSet(dbmodels.Model, model_logic.ModelExtensions):
214 """
215 A set of scheduler drones
216
217 These will be used by the scheduler to decide what drones a job is allowed
218 to run on.
219
220 name: the drone set's name
221 drones: the drones that are part of the set
222 """
223 DRONE_SETS_ENABLED = global_config.global_config.get_config_value(
224 'SCHEDULER', 'drone_sets_enabled', type=bool, default=False)
225 DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value(
226 'SCHEDULER', 'default_drone_set_name', default=None)
227
228 name = dbmodels.CharField(max_length=255, unique=True)
229 drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones')
230
231 name_field = 'name'
232 objects = model_logic.ExtendedManager()
233
234
235 def save(self, *args, **kwargs):
236 if not User.current_user().is_superuser():
237 raise Exception('Only superusers may edit drone sets')
238 super(DroneSet, self).save(*args, **kwargs)
239
240
241 def delete(self):
242 if not User.current_user().is_superuser():
243 raise Exception('Only superusers may delete drone sets')
244 super(DroneSet, self).delete()
245
246
247 @classmethod
248 def drone_sets_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800249 """Returns whether drone sets are enabled.
250
251 @param cls: Implicit class object.
252 """
jamesren76fcf192010-04-21 20:39:50 +0000253 return cls.DRONE_SETS_ENABLED
254
255
256 @classmethod
257 def default_drone_set_name(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800258 """Returns the default drone set name.
259
260 @param cls: Implicit class object.
261 """
jamesren76fcf192010-04-21 20:39:50 +0000262 return cls.DEFAULT_DRONE_SET_NAME
263
264
265 @classmethod
266 def get_default(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800267 """Gets the default drone set name, compatible with Job.add_object.
268
269 @param cls: Implicit class object.
270 """
jamesren76fcf192010-04-21 20:39:50 +0000271 return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME)
272
273
274 @classmethod
275 def resolve_name(cls, drone_set_name):
276 """
277 Returns the name of one of these, if not None, in order of preference:
278 1) the drone set given,
279 2) the current user's default drone set, or
280 3) the global default drone set
281
282 or returns None if drone sets are disabled
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800283
284 @param cls: Implicit class object.
285 @param drone_set_name: A drone set name.
jamesren76fcf192010-04-21 20:39:50 +0000286 """
287 if not cls.drone_sets_enabled():
288 return None
289
290 user = User.current_user()
291 user_drone_set_name = user.drone_set and user.drone_set.name
292
293 return drone_set_name or user_drone_set_name or cls.get_default().name
294
295
296 def get_drone_hostnames(self):
297 """
298 Gets the hostnames of all drones in this drone set
299 """
300 return set(self.drones.all().values_list('hostname', flat=True))
301
302
303 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800304 """Metadata for class DroneSet."""
jamesren76fcf192010-04-21 20:39:50 +0000305 db_table = 'afe_drone_sets'
306
307 def __unicode__(self):
308 return unicode(self.name)
309
310
showardfb2a7fa2008-07-17 17:04:12 +0000311class User(dbmodels.Model, model_logic.ModelExtensions):
312 """\
313 Required:
314 login :user login name
315
316 Optional:
317 access_level: 0=User (default), 1=Admin, 100=Root
318 """
319 ACCESS_ROOT = 100
320 ACCESS_ADMIN = 1
321 ACCESS_USER = 0
322
showard64a95952010-01-13 21:27:16 +0000323 AUTOTEST_SYSTEM = 'autotest_system'
324
showarda5288b42009-07-28 20:06:08 +0000325 login = dbmodels.CharField(max_length=255, unique=True)
showardfb2a7fa2008-07-17 17:04:12 +0000326 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
327
showard0fc38302008-10-23 00:44:07 +0000328 # user preferences
jamesrendd855242010-03-02 22:23:44 +0000329 reboot_before = dbmodels.SmallIntegerField(
330 choices=model_attributes.RebootBefore.choices(), blank=True,
331 default=DEFAULT_REBOOT_BEFORE)
332 reboot_after = dbmodels.SmallIntegerField(
333 choices=model_attributes.RebootAfter.choices(), blank=True,
334 default=DEFAULT_REBOOT_AFTER)
jamesren76fcf192010-04-21 20:39:50 +0000335 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
showard97db5ba2008-11-12 18:18:02 +0000336 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000337
showardfb2a7fa2008-07-17 17:04:12 +0000338 name_field = 'login'
339 objects = model_logic.ExtendedManager()
340
341
showarda5288b42009-07-28 20:06:08 +0000342 def save(self, *args, **kwargs):
showardfb2a7fa2008-07-17 17:04:12 +0000343 # is this a new object being saved for the first time?
344 first_time = (self.id is None)
345 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000346 if user and not user.is_superuser() and user.login != self.login:
347 raise AclAccessViolation("You cannot modify user " + self.login)
showarda5288b42009-07-28 20:06:08 +0000348 super(User, self).save(*args, **kwargs)
showardfb2a7fa2008-07-17 17:04:12 +0000349 if first_time:
350 everyone = AclGroup.objects.get(name='Everyone')
351 everyone.users.add(self)
352
353
354 def is_superuser(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800355 """Returns whether the user has superuser access."""
showardfb2a7fa2008-07-17 17:04:12 +0000356 return self.access_level >= self.ACCESS_ROOT
357
358
showard64a95952010-01-13 21:27:16 +0000359 @classmethod
360 def current_user(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800361 """Returns the current user.
362
363 @param cls: Implicit class object.
364 """
showard64a95952010-01-13 21:27:16 +0000365 user = thread_local.get_user()
366 if user is None:
showardcfcdd802010-01-15 00:16:33 +0000367 user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM)
showard64a95952010-01-13 21:27:16 +0000368 user.access_level = cls.ACCESS_ROOT
369 user.save()
370 return user
371
372
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -0800373 @classmethod
374 def get_record(cls, data):
375 """Check the database for an identical record.
376
377 Check for a record with matching id and login. If one exists,
378 return it. If one does not exist there is a possibility that
379 the following cases have happened:
380 1. Same id, different login
381 We received: "1 chromeos-test"
382 And we have: "1 debug-user"
383 In this case we need to delete "1 debug_user" and insert
384 "1 chromeos-test".
385
386 2. Same login, different id:
387 We received: "1 chromeos-test"
388 And we have: "2 chromeos-test"
389 In this case we need to delete "2 chromeos-test" and insert
390 "1 chromeos-test".
391
392 As long as this method deletes bad records and raises the
393 DoesNotExist exception the caller will handle creating the
394 new record.
395
396 @raises: DoesNotExist, if a record with the matching login and id
397 does not exist.
398 """
399
400 # Both the id and login should be uniqe but there are cases when
401 # we might already have a user with the same login/id because
402 # current_user will proactively create a user record if it doesn't
403 # exist. Since we want to avoid conflict between the master and
404 # shard, just delete any existing user records that don't match
405 # what we're about to deserialize from the master.
406 try:
407 return cls.objects.get(login=data['login'], id=data['id'])
408 except cls.DoesNotExist:
409 cls.delete_matching_record(login=data['login'])
410 cls.delete_matching_record(id=data['id'])
411 raise
412
413
showardfb2a7fa2008-07-17 17:04:12 +0000414 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800415 """Metadata for class User."""
showardeab66ce2009-12-23 00:03:56 +0000416 db_table = 'afe_users'
showardfb2a7fa2008-07-17 17:04:12 +0000417
showarda5288b42009-07-28 20:06:08 +0000418 def __unicode__(self):
419 return unicode(self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000420
421
Prashanth B489b91d2014-03-15 12:17:16 -0700422class Host(model_logic.ModelWithInvalid, rdb_model_extensions.AbstractHostModel,
showardf8b19042009-05-12 17:22:49 +0000423 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000424 """\
425 Required:
426 hostname
mblighe8819cd2008-02-15 16:48:40 +0000427
jadmanski0afbb632008-06-06 21:10:57 +0000428 optional:
showard21baa452008-10-21 00:08:39 +0000429 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000430
jadmanski0afbb632008-06-06 21:10:57 +0000431 Internal:
Prashanth B489b91d2014-03-15 12:17:16 -0700432 From AbstractHostModel:
433 synch_id: currently unused
434 status: string describing status of host
435 invalid: true if the host has been deleted
436 protection: indicates what can be done to this host during repair
437 lock_time: DateTime at which the host was locked
438 dirty: true if the host has been used without being rebooted
439 Local:
440 locked_by: user that locked the host, or null if the host is unlocked
jadmanski0afbb632008-06-06 21:10:57 +0000441 """
mblighe8819cd2008-02-15 16:48:40 +0000442
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700443 SERIALIZATION_LINKS_TO_FOLLOW = set(['aclgroup_set',
444 'hostattribute_set',
445 'labels',
446 'shard'])
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800447 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['invalid'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700448
Jakob Juelichf88fa932014-09-03 17:58:04 -0700449
450 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700451 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -0700452 self.shard = Shard.deserialize(data)
453
454
Prashanth B489b91d2014-03-15 12:17:16 -0700455 # Note: Only specify foreign keys here, specify all native host columns in
456 # rdb_model_extensions instead.
457 Protection = host_protections.Protection
showardeab66ce2009-12-23 00:03:56 +0000458 labels = dbmodels.ManyToManyField(Label, blank=True,
459 db_table='afe_hosts_labels')
showardfb2a7fa2008-07-17 17:04:12 +0000460 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
jadmanski0afbb632008-06-06 21:10:57 +0000461 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000462 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000463 valid_objects = model_logic.ValidObjectsManager()
beepscc9fc702013-12-02 12:45:38 -0800464 leased_objects = model_logic.LeasedHostManager()
mbligh5244cbb2008-04-24 20:39:52 +0000465
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700466 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
showard2bab8f42008-11-12 18:15:22 +0000467
468 def __init__(self, *args, **kwargs):
469 super(Host, self).__init__(*args, **kwargs)
470 self._record_attributes(['status'])
471
472
showardb8471e32008-07-03 19:51:08 +0000473 @staticmethod
474 def create_one_time_host(hostname):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800475 """Creates a one-time host.
476
477 @param hostname: The name for the host.
478 """
showardb8471e32008-07-03 19:51:08 +0000479 query = Host.objects.filter(hostname=hostname)
480 if query.count() == 0:
481 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000482 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000483 else:
484 host = query[0]
485 if not host.invalid:
486 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000487 'hostname' : '%s already exists in the autotest DB. '
488 'Select it rather than entering it as a one time '
489 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000490 })
showard1ab512b2008-07-30 23:39:04 +0000491 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000492 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000493 host.save()
showard2924b0a2009-06-18 23:16:15 +0000494 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000495 return host
mbligh5244cbb2008-04-24 20:39:52 +0000496
showard1ff7b2e2009-05-15 23:17:18 +0000497
Jakob Juelich59cfe542014-09-02 16:37:46 -0700498 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -0700499 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -0700500 """Assigns hosts to a shard.
501
Jakob Juelich1b525742014-09-30 13:08:07 -0700502 For all labels that have been assigned to this shard, all hosts that
503 have this label, are assigned to this shard.
504
505 Hosts that are assigned to the shard but aren't already present on the
506 shard are returned.
Jakob Juelich59cfe542014-09-02 16:37:46 -0700507
508 @param shard: The shard object to assign labels/hosts for.
Jakob Juelich1b525742014-09-30 13:08:07 -0700509 @param known_ids: List of all host-ids the shard already knows.
510 This is used to figure out which hosts should be sent
511 to the shard. If shard_ids were used instead, hosts
512 would only be transferred once, even if the client
513 failed persisting them.
514 The number of hosts usually lies in O(100), so the
515 overhead is acceptable.
516
Jakob Juelich59cfe542014-09-02 16:37:46 -0700517 @returns the hosts objects that should be sent to the shard.
518 """
519
520 # Disclaimer: concurrent heartbeats should theoretically not occur in
521 # the current setup. As they may be introduced in the near future,
522 # this comment will be left here.
523
524 # Sending stuff twice is acceptable, but forgetting something isn't.
525 # Detecting duplicates on the client is easy, but here it's harder. The
526 # following options were considered:
527 # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
528 # than select returned, as concurrently more hosts might have been
529 # inserted
530 # - UPDATE and then SELECT WHERE shard=shard: select always returns all
531 # hosts for the shard, this is overhead
532 # - SELECT and then UPDATE only selected without requerying afterwards:
533 # returns the old state of the records.
534 host_ids = list(Host.objects.filter(
Jakob Juelich59cfe542014-09-02 16:37:46 -0700535 labels=shard.labels.all(),
536 leased=False
Jakob Juelich1b525742014-09-30 13:08:07 -0700537 ).exclude(
538 id__in=known_ids,
Jakob Juelich59cfe542014-09-02 16:37:46 -0700539 ).values_list('pk', flat=True))
540
541 if host_ids:
542 Host.objects.filter(pk__in=host_ids).update(shard=shard)
543 return list(Host.objects.filter(pk__in=host_ids).all())
544 return []
545
showardafd97de2009-10-01 18:45:09 +0000546 def resurrect_object(self, old_object):
547 super(Host, self).resurrect_object(old_object)
548 # invalid hosts can be in use by the scheduler (as one-time hosts), so
549 # don't change the status
550 self.status = old_object.status
551
552
jadmanski0afbb632008-06-06 21:10:57 +0000553 def clean_object(self):
554 self.aclgroup_set.clear()
555 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000556
557
Dan Shia0acfbc2014-10-14 15:56:23 -0700558 def record_state(self, type_str, state, value, other_metadata=None):
559 """Record metadata in elasticsearch.
560
561 @param type_str: sets the _type field in elasticsearch db.
562 @param state: string representing what state we are recording,
563 e.g. 'locked'
564 @param value: value of the state, e.g. True
565 @param other_metadata: Other metadata to store in metaDB.
566 """
567 metadata = {
568 state: value,
569 'hostname': self.hostname,
570 }
571 if other_metadata:
572 metadata = dict(metadata.items() + other_metadata.items())
Gabe Blackb72f4fb2015-01-20 16:47:13 -0800573 autotest_es.post(type_str=type_str, metadata=metadata)
Dan Shia0acfbc2014-10-14 15:56:23 -0700574
575
showarda5288b42009-07-28 20:06:08 +0000576 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000577 # extra spaces in the hostname can be a sneaky source of errors
578 self.hostname = self.hostname.strip()
579 # is this a new object being saved for the first time?
580 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000581 if not first_time:
582 AclGroup.check_for_acl_violation_hosts([self])
Dan Shia0acfbc2014-10-14 15:56:23 -0700583 # If locked is changed, send its status and user made the change to
584 # metaDB. Locks are important in host history because if a device is
585 # locked then we don't really care what state it is in.
showardfb2a7fa2008-07-17 17:04:12 +0000586 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000587 self.locked_by = User.current_user()
showardfb2a7fa2008-07-17 17:04:12 +0000588 self.lock_time = datetime.now()
Dan Shia0acfbc2014-10-14 15:56:23 -0700589 self.record_state('lock_history', 'locked', self.locked,
590 {'changed_by': self.locked_by.login})
showard21baa452008-10-21 00:08:39 +0000591 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000592 elif not self.locked and self.locked_by:
Dan Shia0acfbc2014-10-14 15:56:23 -0700593 self.record_state('lock_history', 'locked', self.locked,
594 {'changed_by': self.locked_by.login})
showardfb2a7fa2008-07-17 17:04:12 +0000595 self.locked_by = None
596 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000597 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000598 if first_time:
599 everyone = AclGroup.objects.get(name='Everyone')
600 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000601 self._check_for_updated_attributes()
602
mblighe8819cd2008-02-15 16:48:40 +0000603
showardb8471e32008-07-03 19:51:08 +0000604 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000605 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000606 for queue_entry in self.hostqueueentry_set.all():
607 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000608 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000609 super(Host, self).delete()
610
mblighe8819cd2008-02-15 16:48:40 +0000611
showard2bab8f42008-11-12 18:15:22 +0000612 def on_attribute_changed(self, attribute, old_value):
613 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000614 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000615
616
showard29f7cd22009-04-29 21:16:24 +0000617 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800618 """Enqueue a job on this host.
619
620 @param job: A job to enqueue.
621 @param atomic_group: The associated atomic group.
622 @param is_template: Whther the status should be "Template".
623 """
showard29f7cd22009-04-29 21:16:24 +0000624 queue_entry = HostQueueEntry.create(host=self, job=job,
625 is_template=is_template,
626 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000627 # allow recovery of dead hosts from the frontend
628 if not self.active_queue_entry() and self.is_dead():
629 self.status = Host.Status.READY
630 self.save()
631 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000632
showard08f981b2008-06-24 21:59:03 +0000633 block = IneligibleHostQueue(job=job, host=self)
634 block.save()
635
mblighe8819cd2008-02-15 16:48:40 +0000636
jadmanski0afbb632008-06-06 21:10:57 +0000637 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800638 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000639 # TODO(showard): slighly hacky?
640 platforms = self.labels.filter(platform=True)
641 if len(platforms) == 0:
642 return None
643 return platforms[0]
644 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000645
646
showardcafd16e2009-05-29 18:37:49 +0000647 @classmethod
648 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800649 """Verify the specified hosts have no associated platforms.
650
651 @param cls: Implicit class object.
652 @param hosts: The hosts to verify.
653 @raises model_logic.ValidationError if any hosts already have a
654 platform.
655 """
showardcafd16e2009-05-29 18:37:49 +0000656 Host.objects.populate_relationships(hosts, Label, 'label_list')
657 errors = []
658 for host in hosts:
659 platforms = [label.name for label in host.label_list
660 if label.platform]
661 if platforms:
662 # do a join, just in case this host has multiple platforms,
663 # we'll be able to see it
664 errors.append('Host %s already has a platform: %s' % (
665 host.hostname, ', '.join(platforms)))
666 if errors:
667 raise model_logic.ValidationError({'labels': '; '.join(errors)})
668
669
jadmanski0afbb632008-06-06 21:10:57 +0000670 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800671 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000672 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000673
674
jadmanski0afbb632008-06-06 21:10:57 +0000675 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800676 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000677 active = list(self.hostqueueentry_set.filter(active=True))
678 if not active:
679 return None
680 assert len(active) == 1, ('More than one active entry for '
681 'host ' + self.hostname)
682 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000683
684
showardf8b19042009-05-12 17:22:49 +0000685 def _get_attribute_model_and_args(self, attribute):
686 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000687
688
jadmanski0afbb632008-06-06 21:10:57 +0000689 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800690 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000691 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000692
showarda5288b42009-07-28 20:06:08 +0000693 def __unicode__(self):
694 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000695
696
MK Ryuacf35922014-10-03 14:56:49 -0700697class HostAttribute(dbmodels.Model, model_logic.ModelExtensions):
showard0957a842009-05-11 19:25:08 +0000698 """Arbitrary keyvals associated with hosts."""
Fang Deng86248502014-12-18 16:38:00 -0800699
700 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
showard0957a842009-05-11 19:25:08 +0000701 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000702 attribute = dbmodels.CharField(max_length=90)
703 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000704
705 objects = model_logic.ExtendedManager()
706
707 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800708 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000709 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000710
711
showard7c785282008-05-29 19:45:12 +0000712class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000713 """\
714 Required:
showard909c7a62008-07-15 21:52:38 +0000715 author: author name
716 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000717 name: test name
showard909c7a62008-07-15 21:52:38 +0000718 time: short, medium, long
719 test_class: This describes the class for your the test belongs in.
720 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000721 test_type: Client or Server
722 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000723 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
724 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000725 i.e. if sync_count = 2 it is a sync job that requires two
726 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000727 Optional:
showard909c7a62008-07-15 21:52:38 +0000728 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000729 dependency_labels: many-to-many relationship with labels corresponding to
730 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000731 experimental: If this is set to True production servers will ignore the test
732 run_verify: Whether or not the scheduler should run the verify stage
Dan Shi07e09af2013-04-12 09:31:29 -0700733 run_reset: Whether or not the scheduler should run the reset stage
Aviv Keshet9af96d32013-03-05 12:56:24 -0800734 test_retry: Number of times to retry test if the test did not complete
735 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +0000736 """
showard909c7a62008-07-15 21:52:38 +0000737 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000738
showarda5288b42009-07-28 20:06:08 +0000739 name = dbmodels.CharField(max_length=255, unique=True)
740 author = dbmodels.CharField(max_length=255)
741 test_class = dbmodels.CharField(max_length=255)
742 test_category = dbmodels.CharField(max_length=255)
743 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000744 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000745 experimental = dbmodels.BooleanField(default=True)
Dan Shi07e09af2013-04-12 09:31:29 -0700746 run_verify = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +0000747 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
748 default=TestTime.MEDIUM)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700749 test_type = dbmodels.SmallIntegerField(
750 choices=control_data.CONTROL_TYPE.choices())
showard909c7a62008-07-15 21:52:38 +0000751 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000752 path = dbmodels.CharField(max_length=255, unique=True)
Aviv Keshet9af96d32013-03-05 12:56:24 -0800753 test_retry = dbmodels.IntegerField(blank=True, default=0)
Dan Shi07e09af2013-04-12 09:31:29 -0700754 run_reset = dbmodels.BooleanField(default=True)
mblighe8819cd2008-02-15 16:48:40 +0000755
showardeab66ce2009-12-23 00:03:56 +0000756 dependency_labels = (
757 dbmodels.ManyToManyField(Label, blank=True,
758 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000759 name_field = 'name'
760 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000761
762
jamesren35a70222010-02-16 19:30:46 +0000763 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800764 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +0000765 escaped_description = saxutils.escape(self.description)
766 return '<span style="white-space:pre">%s</span>' % escaped_description
767 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000768 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000769
770
jadmanski0afbb632008-06-06 21:10:57 +0000771 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800772 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +0000773 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000774
showarda5288b42009-07-28 20:06:08 +0000775 def __unicode__(self):
776 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000777
778
jamesren4a41e012010-07-16 22:33:48 +0000779class TestParameter(dbmodels.Model):
780 """
781 A declared parameter of a test
782 """
783 test = dbmodels.ForeignKey(Test)
784 name = dbmodels.CharField(max_length=255)
785
786 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800787 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000788 db_table = 'afe_test_parameters'
789 unique_together = ('test', 'name')
790
791 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -0700792 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +0000793
794
showard2b9a88b2008-06-13 20:55:03 +0000795class Profiler(dbmodels.Model, model_logic.ModelExtensions):
796 """\
797 Required:
798 name: profiler name
799 test_type: Client or Server
800
801 Optional:
802 description: arbirary text description
803 """
showarda5288b42009-07-28 20:06:08 +0000804 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000805 description = dbmodels.TextField(blank=True)
806
807 name_field = 'name'
808 objects = model_logic.ExtendedManager()
809
810
811 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800812 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +0000813 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000814
showarda5288b42009-07-28 20:06:08 +0000815 def __unicode__(self):
816 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000817
818
showard7c785282008-05-29 19:45:12 +0000819class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000820 """\
821 Required:
822 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000823
jadmanski0afbb632008-06-06 21:10:57 +0000824 Optional:
825 description: arbitrary description of group
826 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700827
828 SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
829
showarda5288b42009-07-28 20:06:08 +0000830 name = dbmodels.CharField(max_length=255, unique=True)
831 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000832 users = dbmodels.ManyToManyField(User, blank=False,
833 db_table='afe_acl_groups_users')
834 hosts = dbmodels.ManyToManyField(Host, blank=True,
835 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000836
jadmanski0afbb632008-06-06 21:10:57 +0000837 name_field = 'name'
838 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000839
showard08f981b2008-06-24 21:59:03 +0000840 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000841 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800842 """Verify the current user has access to the specified hosts.
843
844 @param hosts: The hosts to verify against.
845 @raises AclAccessViolation if the current user doesn't have access
846 to a host.
847 """
showard64a95952010-01-13 21:27:16 +0000848 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000849 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000850 return
showard3dd47c22008-07-10 00:41:36 +0000851 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000852 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000853 for host in hosts:
854 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800855 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +0000856 no_access = (isinstance(host, Host)
857 and not host.invalid
858 and int(host.id) not in accessible_host_ids)
859 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000860 raise AclAccessViolation("%s does not have access to %s" %
861 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000862
showard9dbdcda2008-10-14 17:34:36 +0000863
864 @staticmethod
showarddc817512008-11-12 18:16:41 +0000865 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800866 """Look for queue entries that aren't abortable by the current user.
867
868 An entry is not abortable if:
869 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +0000870 * the machine isn't ACL-accessible, or
871 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800872
873 @param queue_entries: The queue entries to check.
874 @raises AclAccessViolation if a queue entry is not abortable by the
875 current user.
showarddc817512008-11-12 18:16:41 +0000876 """
showard64a95952010-01-13 21:27:16 +0000877 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000878 if user.is_superuser():
879 return
showarddc817512008-11-12 18:16:41 +0000880 not_owned = queue_entries.exclude(job__owner=user.login)
881 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000882 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
883 # 1.0.
884 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000885 accessible_ids = set(
886 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000887 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000888 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000889 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000890 cannot_abort = [entry for entry in not_owned.select_related()
891 if entry.id not in accessible_ids
892 or entry.id in public_ids]
893 if len(cannot_abort) == 0:
894 return
895 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000896 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000897 for entry in cannot_abort)
898 raise AclAccessViolation('You cannot abort the following job entries: '
899 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000900
901
showard3dd47c22008-07-10 00:41:36 +0000902 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800903 """Verifies the current user has acces to this ACL group.
904
905 @raises AclAccessViolation if the current user doesn't have access to
906 this ACL group.
907 """
showard64a95952010-01-13 21:27:16 +0000908 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000909 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000910 return
911 if self.name == 'Everyone':
912 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000913 if not user in self.users.all():
914 raise AclAccessViolation("You do not have access to %s"
915 % self.name)
916
917 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000918 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800919 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +0000920 everyone = AclGroup.objects.get(name='Everyone')
921
showard3dd47c22008-07-10 00:41:36 +0000922 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000923 # TODO(showard): this is a bit of a hack, since the fact that this query
924 # works is kind of a coincidence of Django internals. This trick
925 # doesn't work in general (on all foreign key relationships). I'll
926 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000927 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000928 everyone.hosts.add(*orphaned_hosts.distinct())
929
930 # find hosts in both Everyone and another ACL group, and remove them
931 # from Everyone
showarda5288b42009-07-28 20:06:08 +0000932 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
933 acled_hosts = set()
934 for host in hosts_in_everyone:
935 # Has an ACL group other than Everyone
936 if host.aclgroup_set.count() > 1:
937 acled_hosts.add(host)
938 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +0000939
940
941 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000942 if (self.name == 'Everyone'):
943 raise AclAccessViolation("You cannot delete 'Everyone'!")
944 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000945 super(AclGroup, self).delete()
946 self.on_host_membership_change()
947
948
showard04f2cd82008-07-25 20:53:31 +0000949 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800950 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +0000951 if not self.users.count():
showard64a95952010-01-13 21:27:16 +0000952 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +0000953
954
showard8cbaf1e2009-09-08 16:27:04 +0000955 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800956 """Called after a save.
957
958 @param change: Whether there was a change.
959 """
showard8cbaf1e2009-09-08 16:27:04 +0000960 if not change:
showard64a95952010-01-13 21:27:16 +0000961 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +0000962 self.add_current_user_if_empty()
963 self.on_host_membership_change()
964
965
966 def save(self, *args, **kwargs):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700967 change = bool(self.id)
968 if change:
showard8cbaf1e2009-09-08 16:27:04 +0000969 # Check the original object for an ACL violation
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700970 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
showard8cbaf1e2009-09-08 16:27:04 +0000971 super(AclGroup, self).save(*args, **kwargs)
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700972 self.perform_after_save(change)
showard8cbaf1e2009-09-08 16:27:04 +0000973
showardeb3be4d2008-04-21 20:59:26 +0000974
jadmanski0afbb632008-06-06 21:10:57 +0000975 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800976 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +0000977 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000978
showarda5288b42009-07-28 20:06:08 +0000979 def __unicode__(self):
980 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000981
mblighe8819cd2008-02-15 16:48:40 +0000982
jamesren4a41e012010-07-16 22:33:48 +0000983class Kernel(dbmodels.Model):
984 """
985 A kernel configuration for a parameterized job
986 """
987 version = dbmodels.CharField(max_length=255)
988 cmdline = dbmodels.CharField(max_length=255, blank=True)
989
990 @classmethod
991 def create_kernels(cls, kernel_list):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800992 """Creates all kernels in the kernel list.
jamesren4a41e012010-07-16 22:33:48 +0000993
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800994 @param cls: Implicit class object.
995 @param kernel_list: A list of dictionaries that describe the kernels,
996 in the same format as the 'kernel' argument to
997 rpc_interface.generate_control_file.
998 @return A list of the created kernels.
jamesren4a41e012010-07-16 22:33:48 +0000999 """
1000 if not kernel_list:
1001 return None
1002 return [cls._create(kernel) for kernel in kernel_list]
1003
1004
1005 @classmethod
1006 def _create(cls, kernel_dict):
1007 version = kernel_dict.pop('version')
1008 cmdline = kernel_dict.pop('cmdline', '')
1009
1010 if kernel_dict:
1011 raise Exception('Extraneous kernel arguments remain: %r'
1012 % kernel_dict)
1013
1014 kernel, _ = cls.objects.get_or_create(version=version,
1015 cmdline=cmdline)
1016 return kernel
1017
1018
1019 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001020 """Metadata for class Kernel."""
jamesren4a41e012010-07-16 22:33:48 +00001021 db_table = 'afe_kernels'
1022 unique_together = ('version', 'cmdline')
1023
1024 def __unicode__(self):
1025 return u'%s %s' % (self.version, self.cmdline)
1026
1027
1028class ParameterizedJob(dbmodels.Model):
1029 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001030 Auxiliary configuration for a parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001031 """
1032 test = dbmodels.ForeignKey(Test)
1033 label = dbmodels.ForeignKey(Label, null=True)
1034 use_container = dbmodels.BooleanField(default=False)
1035 profile_only = dbmodels.BooleanField(default=False)
1036 upload_kernel_config = dbmodels.BooleanField(default=False)
1037
1038 kernels = dbmodels.ManyToManyField(
1039 Kernel, db_table='afe_parameterized_job_kernels')
1040 profilers = dbmodels.ManyToManyField(
1041 Profiler, through='ParameterizedJobProfiler')
1042
1043
1044 @classmethod
1045 def smart_get(cls, id_or_name, *args, **kwargs):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001046 """For compatibility with Job.add_object.
1047
1048 @param cls: Implicit class object.
1049 @param id_or_name: The ID or name to get.
1050 @param args: Non-keyword arguments.
1051 @param kwargs: Keyword arguments.
1052 """
jamesren4a41e012010-07-16 22:33:48 +00001053 return cls.objects.get(pk=id_or_name)
1054
1055
1056 def job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001057 """Returns the job if it exists, or else None."""
jamesren4a41e012010-07-16 22:33:48 +00001058 jobs = self.job_set.all()
1059 assert jobs.count() <= 1
1060 return jobs and jobs[0] or None
1061
1062
1063 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001064 """Metadata for class ParameterizedJob."""
jamesren4a41e012010-07-16 22:33:48 +00001065 db_table = 'afe_parameterized_jobs'
1066
1067 def __unicode__(self):
1068 return u'%s (parameterized) - %s' % (self.test.name, self.job())
1069
1070
1071class ParameterizedJobProfiler(dbmodels.Model):
1072 """
1073 A profiler to run on a parameterized job
1074 """
1075 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1076 profiler = dbmodels.ForeignKey(Profiler)
1077
1078 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001079 """Metedata for class ParameterizedJobProfiler."""
jamesren4a41e012010-07-16 22:33:48 +00001080 db_table = 'afe_parameterized_jobs_profilers'
1081 unique_together = ('parameterized_job', 'profiler')
1082
1083
1084class ParameterizedJobProfilerParameter(dbmodels.Model):
1085 """
1086 A parameter for a profiler in a parameterized job
1087 """
1088 parameterized_job_profiler = dbmodels.ForeignKey(ParameterizedJobProfiler)
1089 parameter_name = dbmodels.CharField(max_length=255)
1090 parameter_value = dbmodels.TextField()
1091 parameter_type = dbmodels.CharField(
1092 max_length=8, choices=model_attributes.ParameterTypes.choices())
1093
1094 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001095 """Metadata for class ParameterizedJobProfilerParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001096 db_table = 'afe_parameterized_job_profiler_parameters'
1097 unique_together = ('parameterized_job_profiler', 'parameter_name')
1098
1099 def __unicode__(self):
1100 return u'%s - %s' % (self.parameterized_job_profiler.profiler.name,
1101 self.parameter_name)
1102
1103
1104class ParameterizedJobParameter(dbmodels.Model):
1105 """
1106 Parameters for a parameterized job
1107 """
1108 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1109 test_parameter = dbmodels.ForeignKey(TestParameter)
1110 parameter_value = dbmodels.TextField()
1111 parameter_type = dbmodels.CharField(
1112 max_length=8, choices=model_attributes.ParameterTypes.choices())
1113
1114 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001115 """Metadata for class ParameterizedJobParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001116 db_table = 'afe_parameterized_job_parameters'
1117 unique_together = ('parameterized_job', 'test_parameter')
1118
1119 def __unicode__(self):
1120 return u'%s - %s' % (self.parameterized_job.job().name,
1121 self.test_parameter.name)
1122
1123
showard7c785282008-05-29 19:45:12 +00001124class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +00001125 'Custom manager to provide efficient status counts querying.'
1126 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001127 """Returns a dict mapping the given job IDs to their status count dicts.
1128
1129 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +00001130 """
1131 if not job_ids:
1132 return {}
1133 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1134 cursor = connection.cursor()
1135 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +00001136 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +00001137 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +00001138 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +00001139 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +00001140 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +00001141 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +00001142 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +00001143 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +00001144 full_status = HostQueueEntry.compute_full_status(status, aborted,
1145 complete)
showardb6d16622009-05-26 19:35:29 +00001146 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +00001147 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +00001148 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +00001149
1150
showard7c785282008-05-29 19:45:12 +00001151class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001152 """\
1153 owner: username of job owner
1154 name: job name (does not have to be unique)
Alex Miller7d658cf2013-09-04 16:00:35 -07001155 priority: Integer priority value. Higher is more important.
jadmanski0afbb632008-06-06 21:10:57 +00001156 control_file: contents of control file
1157 control_type: Client or Server
1158 created_on: date of job creation
1159 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +00001160 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +00001161 run_verify: Whether or not to run the verify phase
Dan Shi07e09af2013-04-12 09:31:29 -07001162 run_reset: Whether or not to run the reset phase
Simran Basi94dc0032013-11-12 14:09:46 -08001163 timeout: DEPRECATED - hours from queuing time until job times out
1164 timeout_mins: minutes from job queuing time until the job times out
Simran Basi34217022012-11-06 13:43:15 -08001165 max_runtime_hrs: DEPRECATED - hours from job starting time until job
1166 times out
1167 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +00001168 email_list: list of people to email on completion delimited by any of:
1169 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +00001170 dependency_labels: many-to-many relationship with labels corresponding to
1171 job dependencies
showard21baa452008-10-21 00:08:39 +00001172 reboot_before: Never, If dirty, or Always
1173 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001174 parse_failed_repair: if True, a failed repair launched by this job will have
1175 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001176 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001177 parent_job: Parent job (optional)
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001178 test_retry: Number of times to retry test if the test did not complete
1179 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +00001180 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001181
1182 # TODO: Investigate, if jobkeyval_set is really needed.
1183 # dynamic_suite will write them into an attached file for the drone, but
1184 # it doesn't seem like they are actually used. If they aren't used, remove
1185 # jobkeyval_set here.
1186 SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1187 'hostqueueentry_set',
1188 'jobkeyval_set',
1189 'shard'])
1190
1191
Jakob Juelichf88fa932014-09-03 17:58:04 -07001192 def _deserialize_relation(self, link, data):
1193 if link in ['hostqueueentry_set', 'jobkeyval_set']:
1194 for obj in data:
1195 obj['job_id'] = self.id
1196
1197 super(Job, self)._deserialize_relation(link, data)
1198
1199
1200 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001201 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -07001202 self.shard = Shard.deserialize(data)
1203
1204
Jakob Juelicha94efe62014-09-18 16:02:49 -07001205 def sanity_check_update_from_shard(self, shard, updated_serialized):
Jakob Juelich02e61292014-10-17 12:36:55 -07001206 # If the job got aborted on the master after the client fetched it
1207 # no shard_id will be set. The shard might still push updates though,
1208 # as the job might complete before the abort bit syncs to the shard.
1209 # Alternative considered: The master scheduler could be changed to not
1210 # set aborted jobs to completed that are sharded out. But that would
1211 # require database queries and seemed more complicated to implement.
1212 # This seems safe to do, as there won't be updates pushed from the wrong
1213 # shards should be powered off and wiped hen they are removed from the
1214 # master.
1215 if self.shard_id and self.shard_id != shard.id:
Jakob Juelicha94efe62014-09-18 16:02:49 -07001216 raise error.UnallowedRecordsSentToMaster(
1217 'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1218 'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1219 shard.id))
1220
1221
Simran Basi94dc0032013-11-12 14:09:46 -08001222 # TIMEOUT is deprecated.
showardb1e51872008-10-07 11:08:18 +00001223 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
Simran Basi94dc0032013-11-12 14:09:46 -08001224 'AUTOTEST_WEB', 'job_timeout_default', default=24)
1225 DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1226 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
Simran Basi34217022012-11-06 13:43:15 -08001227 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1228 # completed.
showard12f3e322009-05-13 21:27:42 +00001229 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1230 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001231 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1232 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001233 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1234 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1235 default=False)
showardb1e51872008-10-07 11:08:18 +00001236
showarda5288b42009-07-28 20:06:08 +00001237 owner = dbmodels.CharField(max_length=255)
1238 name = dbmodels.CharField(max_length=255)
Alex Miller7d658cf2013-09-04 16:00:35 -07001239 priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
jamesren4a41e012010-07-16 22:33:48 +00001240 control_file = dbmodels.TextField(null=True, blank=True)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001241 control_type = dbmodels.SmallIntegerField(
1242 choices=control_data.CONTROL_TYPE.choices(),
1243 blank=True, # to allow 0
1244 default=control_data.CONTROL_TYPE.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001245 created_on = dbmodels.DateTimeField()
Simran Basi8d89b642014-05-02 17:33:20 -07001246 synch_count = dbmodels.IntegerField(blank=True, default=0)
showardb1e51872008-10-07 11:08:18 +00001247 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
Dan Shi07e09af2013-04-12 09:31:29 -07001248 run_verify = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001249 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001250 dependency_labels = (
1251 dbmodels.ManyToManyField(Label, blank=True,
1252 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001253 reboot_before = dbmodels.SmallIntegerField(
1254 choices=model_attributes.RebootBefore.choices(), blank=True,
1255 default=DEFAULT_REBOOT_BEFORE)
1256 reboot_after = dbmodels.SmallIntegerField(
1257 choices=model_attributes.RebootAfter.choices(), blank=True,
1258 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001259 parse_failed_repair = dbmodels.BooleanField(
1260 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001261 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1262 # completed.
showard12f3e322009-05-13 21:27:42 +00001263 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001264 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001265 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001266
jamesren4a41e012010-07-16 22:33:48 +00001267 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1268 blank=True)
1269
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001270 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001271
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001272 test_retry = dbmodels.IntegerField(blank=True, default=0)
1273
Dan Shi07e09af2013-04-12 09:31:29 -07001274 run_reset = dbmodels.BooleanField(default=True)
1275
Simran Basi94dc0032013-11-12 14:09:46 -08001276 timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1277
Jakob Juelich8421d592014-09-17 15:27:06 -07001278 # If this is None on the master, a slave should be found.
1279 # If this is None on a slave, it should be synced back to the master
Jakob Jülich92c06332014-08-25 19:06:57 +00001280 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1281
jadmanski0afbb632008-06-06 21:10:57 +00001282 # custom manager
1283 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001284
1285
Alex Millerec212252014-02-28 16:48:34 -08001286 @decorators.cached_property
1287 def labels(self):
1288 """All the labels of this job"""
1289 # We need to convert dependency_labels to a list, because all() gives us
1290 # back an iterator, and storing/caching an iterator means we'd only be
1291 # able to read from it once.
1292 return list(self.dependency_labels.all())
1293
1294
jadmanski0afbb632008-06-06 21:10:57 +00001295 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001296 """Returns whether this job is of type server."""
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001297 return self.control_type == control_data.CONTROL_TYPE.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001298
1299
jadmanski0afbb632008-06-06 21:10:57 +00001300 @classmethod
jamesren4a41e012010-07-16 22:33:48 +00001301 def parameterized_jobs_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001302 """Returns whether parameterized jobs are enabled.
1303
1304 @param cls: Implicit class object.
1305 """
jamesren4a41e012010-07-16 22:33:48 +00001306 return global_config.global_config.get_config_value(
1307 'AUTOTEST_WEB', 'parameterized_jobs', type=bool)
1308
1309
1310 @classmethod
1311 def check_parameterized_job(cls, control_file, parameterized_job):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001312 """Checks that the job is valid given the global config settings.
jamesren4a41e012010-07-16 22:33:48 +00001313
1314 First, either control_file must be set, or parameterized_job must be
1315 set, but not both. Second, parameterized_job must be set if and only if
1316 the parameterized_jobs option in the global config is set to True.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001317
1318 @param cls: Implict class object.
1319 @param control_file: A control file.
1320 @param parameterized_job: A parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001321 """
1322 if not (bool(control_file) ^ bool(parameterized_job)):
1323 raise Exception('Job must have either control file or '
1324 'parameterization, but not both')
1325
1326 parameterized_jobs_enabled = cls.parameterized_jobs_enabled()
1327 if control_file and parameterized_jobs_enabled:
1328 raise Exception('Control file specified, but parameterized jobs '
1329 'are enabled')
1330 if parameterized_job and not parameterized_jobs_enabled:
1331 raise Exception('Parameterized job specified, but parameterized '
1332 'jobs are not enabled')
1333
1334
1335 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001336 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001337 """Creates a job.
1338
1339 The job is created by taking some information (the listed args) and
1340 filling in the rest of the necessary information.
1341
1342 @param cls: Implicit class object.
1343 @param owner: The owner for the job.
1344 @param options: An options object.
1345 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001346 """
showard3dd47c22008-07-10 00:41:36 +00001347 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001348
jamesren4a41e012010-07-16 22:33:48 +00001349 control_file = options.get('control_file')
1350 parameterized_job = options.get('parameterized_job')
jamesren4a41e012010-07-16 22:33:48 +00001351
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001352 # The current implementation of parameterized jobs requires that only
1353 # control files or parameterized jobs are used. Using the image
1354 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1355 # parameterized jobs and control files jobs, it does muck enough with
1356 # normal jobs by adding a parameterized id to them that this check will
1357 # fail. So for now we just skip this check.
1358 # cls.check_parameterized_job(control_file=control_file,
1359 # parameterized_job=parameterized_job)
showard4d233752010-01-20 19:06:40 +00001360 user = User.current_user()
1361 if options.get('reboot_before') is None:
1362 options['reboot_before'] = user.get_reboot_before_display()
1363 if options.get('reboot_after') is None:
1364 options['reboot_after'] = user.get_reboot_after_display()
1365
jamesren76fcf192010-04-21 20:39:50 +00001366 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1367
Simran Basi94dc0032013-11-12 14:09:46 -08001368 if options.get('timeout_mins') is None and options.get('timeout'):
1369 options['timeout_mins'] = options['timeout'] * 60
1370
jadmanski0afbb632008-06-06 21:10:57 +00001371 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001372 owner=owner,
1373 name=options['name'],
1374 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001375 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001376 control_type=options['control_type'],
1377 synch_count=options.get('synch_count'),
Simran Basi94dc0032013-11-12 14:09:46 -08001378 # timeout needs to be deleted in the future.
showarda1e74b32009-05-12 17:32:04 +00001379 timeout=options.get('timeout'),
Simran Basi94dc0032013-11-12 14:09:46 -08001380 timeout_mins=options.get('timeout_mins'),
Simran Basi34217022012-11-06 13:43:15 -08001381 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001382 run_verify=options.get('run_verify'),
1383 email_list=options.get('email_list'),
1384 reboot_before=options.get('reboot_before'),
1385 reboot_after=options.get('reboot_after'),
1386 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001387 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001388 drone_set=drone_set,
Aviv Keshet18308922013-02-19 17:49:49 -08001389 parameterized_job=parameterized_job,
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001390 parent_job=options.get('parent_job_id'),
Dan Shi07e09af2013-04-12 09:31:29 -07001391 test_retry=options.get('test_retry'),
1392 run_reset=options.get('run_reset'))
mblighe8819cd2008-02-15 16:48:40 +00001393
showarda1e74b32009-05-12 17:32:04 +00001394 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001395
jamesrend8b6e172010-04-16 23:45:00 +00001396 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001397 for key, value in options['keyvals'].iteritems():
1398 JobKeyval.objects.create(job=job, key=key, value=value)
1399
jadmanski0afbb632008-06-06 21:10:57 +00001400 return job
mblighe8819cd2008-02-15 16:48:40 +00001401
1402
Jakob Juelich59cfe542014-09-02 16:37:46 -07001403 @classmethod
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001404 def _add_filters_for_shard_assignment(cls, query, known_ids):
1405 """Exclude jobs that should be not sent to shard.
1406
1407 This is a helper that filters out the following jobs:
1408 - Non-aborted jobs known to shard as specified in |known_ids|.
1409 Note for jobs aborted on master, even if already known to shard,
1410 will be sent to shard again so that shard can abort them.
1411 - Completed jobs
1412 - Active jobs
1413 @param query: A query that finds jobs for shards, to which the 'exclude'
1414 filters will be applied.
1415 @param known_ids: List of all ids of incomplete jobs, the shard already
1416 knows about.
1417
1418 @returns: A django QuerySet after filtering out unnecessary jobs.
1419
1420 """
1421 return query.exclude(
1422 id__in=known_ids,
1423 hostqueueentry__aborted=False
1424 ).exclude(
1425 hostqueueentry__complete=True
1426 ).exclude(
1427 hostqueueentry__active=True)
1428
1429
1430 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -07001431 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -07001432 """Assigns unassigned jobs to a shard.
1433
Jakob Juelich1b525742014-09-30 13:08:07 -07001434 For all labels that have been assigned to this shard, all jobs that
1435 have this label, are assigned to this shard.
1436
1437 Jobs that are assigned to the shard but aren't already present on the
1438 shard are returned.
1439
Jakob Juelich59cfe542014-09-02 16:37:46 -07001440 @param shard: The shard to assign jobs to.
Jakob Juelich1b525742014-09-30 13:08:07 -07001441 @param known_ids: List of all ids of incomplete jobs, the shard already
1442 knows about.
1443 This is used to figure out which jobs should be sent
1444 to the shard. If shard_ids were used instead, jobs
1445 would only be transferred once, even if the client
1446 failed persisting them.
1447 The number of unfinished jobs usually lies in O(1000).
1448 Assuming one id takes 8 chars in the json, this means
1449 overhead that lies in the lower kilobyte range.
1450 A not in query with 5000 id's takes about 30ms.
1451
Jakob Juelich59cfe542014-09-02 16:37:46 -07001452 @returns The job objects that should be sent to the shard.
1453 """
1454 # Disclaimer: Concurrent heartbeats should not occur in today's setup.
1455 # If this changes or they are triggered manually, this applies:
1456 # Jobs may be returned more than once by concurrent calls of this
1457 # function, as there is a race condition between SELECT and UPDATE.
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001458 query = Job.objects.filter(dependency_labels=shard.labels.all())
1459 query = cls._add_filters_for_shard_assignment(query, known_ids)
1460 job_ids = set(query.distinct().values_list('pk', flat=True))
1461
1462 # Combine frontend jobs in the heartbeat.
1463 query = Job.objects.filter(
1464 hostqueueentry__meta_host__isnull=True,
1465 hostqueueentry__host__isnull=False,
1466 hostqueueentry__host__labels=shard.labels.all()
1467 )
1468 query = cls._add_filters_for_shard_assignment(query, known_ids)
1469 job_ids |= set(query.distinct().values_list('pk', flat=True))
Jakob Juelich59cfe542014-09-02 16:37:46 -07001470 if job_ids:
1471 Job.objects.filter(pk__in=job_ids).update(shard=shard)
1472 return list(Job.objects.filter(pk__in=job_ids).all())
1473 return []
1474
1475
jamesren4a41e012010-07-16 22:33:48 +00001476 def save(self, *args, **kwargs):
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001477 # The current implementation of parameterized jobs requires that only
1478 # control files or parameterized jobs are used. Using the image
1479 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1480 # parameterized jobs and control files jobs, it does muck enough with
1481 # normal jobs by adding a parameterized id to them that this check will
1482 # fail. So for now we just skip this check.
1483 # cls.check_parameterized_job(control_file=self.control_file,
1484 # parameterized_job=self.parameterized_job)
jamesren4a41e012010-07-16 22:33:48 +00001485 super(Job, self).save(*args, **kwargs)
1486
1487
showard29f7cd22009-04-29 21:16:24 +00001488 def queue(self, hosts, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001489 """Enqueue a job on the given hosts.
1490
1491 @param hosts: The hosts to use.
1492 @param atomic_group: The associated atomic group.
1493 @param is_template: Whether the status should be "Template".
1494 """
showarda9545c02009-12-18 22:44:26 +00001495 if not hosts:
1496 if atomic_group:
1497 # No hosts or labels are required to queue an atomic group
1498 # Job. However, if they are given, we respect them below.
1499 atomic_group.enqueue_job(self, is_template=is_template)
1500 else:
1501 # hostless job
1502 entry = HostQueueEntry.create(job=self, is_template=is_template)
1503 entry.save()
1504 return
1505
jadmanski0afbb632008-06-06 21:10:57 +00001506 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +00001507 host.enqueue_job(self, atomic_group=atomic_group,
1508 is_template=is_template)
1509
1510
1511 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001512 """Creates a recurring job.
1513
1514 @param start_date: The starting date of the job.
1515 @param loop_period: How often to re-run the job, in seconds.
1516 @param loop_count: The re-run count.
1517 @param owner: The owner of the job.
1518 """
showard29f7cd22009-04-29 21:16:24 +00001519 rec = RecurringRun(job=self, start_date=start_date,
1520 loop_period=loop_period,
1521 loop_count=loop_count,
1522 owner=User.objects.get(login=owner))
1523 rec.save()
1524 return rec.id
mblighe8819cd2008-02-15 16:48:40 +00001525
1526
jadmanski0afbb632008-06-06 21:10:57 +00001527 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001528 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001529 try:
1530 return User.objects.get(login=self.owner)
1531 except self.DoesNotExist:
1532 return None
mblighe8819cd2008-02-15 16:48:40 +00001533
1534
showard64a95952010-01-13 21:27:16 +00001535 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001536 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001537 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001538 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001539
1540
showardd1195652009-12-08 22:21:02 +00001541 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001542 """Returns a string tag for this job."""
showardd1195652009-12-08 22:21:02 +00001543 return '%s-%s' % (self.id, self.owner)
1544
1545
showardc1a98d12010-01-15 00:22:22 +00001546 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001547 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001548 return dict((keyval.key, keyval.value)
1549 for keyval in self.jobkeyval_set.all())
1550
1551
jadmanski0afbb632008-06-06 21:10:57 +00001552 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001553 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001554 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001555
showarda5288b42009-07-28 20:06:08 +00001556 def __unicode__(self):
1557 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001558
1559
showardc1a98d12010-01-15 00:22:22 +00001560class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1561 """Keyvals associated with jobs"""
1562 job = dbmodels.ForeignKey(Job)
1563 key = dbmodels.CharField(max_length=90)
1564 value = dbmodels.CharField(max_length=300)
1565
1566 objects = model_logic.ExtendedManager()
1567
1568 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001569 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001570 db_table = 'afe_job_keyvals'
1571
1572
showard7c785282008-05-29 19:45:12 +00001573class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001574 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001575 job = dbmodels.ForeignKey(Job)
1576 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001577
jadmanski0afbb632008-06-06 21:10:57 +00001578 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001579
jadmanski0afbb632008-06-06 21:10:57 +00001580 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001581 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001582 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001583
mblighe8819cd2008-02-15 16:48:40 +00001584
showard7c785282008-05-29 19:45:12 +00001585class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001586 """Represents a host queue entry."""
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001587
1588 SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001589 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Jakob Juelichf865d332014-09-29 10:47:49 -07001590 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001591
Jakob Juelichf88fa932014-09-03 17:58:04 -07001592
1593 def custom_deserialize_relation(self, link, data):
1594 assert link == 'meta_host'
1595 self.meta_host = Label.deserialize(data)
1596
1597
Jakob Juelicha94efe62014-09-18 16:02:49 -07001598 def sanity_check_update_from_shard(self, shard, updated_serialized,
1599 job_ids_sent):
1600 if self.job_id not in job_ids_sent:
1601 raise error.UnallowedRecordsSentToMaster(
1602 'Sent HostQueueEntry without corresponding '
1603 'job entry: %s' % updated_serialized)
1604
1605
showardeaa408e2009-09-11 18:45:31 +00001606 Status = host_queue_entry_states.Status
1607 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001608 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001609
jadmanski0afbb632008-06-06 21:10:57 +00001610 job = dbmodels.ForeignKey(Job)
1611 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001612 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001613 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1614 db_column='meta_host')
1615 active = dbmodels.BooleanField(default=False)
1616 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001617 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001618 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1619 default='')
showard89f84db2009-03-12 20:39:13 +00001620 # If atomic_group is set, this is a virtual HostQueueEntry that will
1621 # be expanded into many actual hosts within the group at schedule time.
1622 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001623 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001624 started_on = dbmodels.DateTimeField(null=True, blank=True)
Fang Deng51599032014-06-23 17:24:27 -07001625 finished_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001626
jadmanski0afbb632008-06-06 21:10:57 +00001627 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001628
mblighe8819cd2008-02-15 16:48:40 +00001629
showard2bab8f42008-11-12 18:15:22 +00001630 def __init__(self, *args, **kwargs):
1631 super(HostQueueEntry, self).__init__(*args, **kwargs)
1632 self._record_attributes(['status'])
1633
1634
showard29f7cd22009-04-29 21:16:24 +00001635 @classmethod
1636 def create(cls, job, host=None, meta_host=None, atomic_group=None,
1637 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001638 """Creates a new host queue entry.
1639
1640 @param cls: Implicit class object.
1641 @param job: The associated job.
1642 @param host: The associated host.
1643 @param meta_host: The associated meta host.
1644 @param atomic_group: The associated atomic group.
1645 @param is_template: Whether the status should be "Template".
1646 """
showard29f7cd22009-04-29 21:16:24 +00001647 if is_template:
1648 status = cls.Status.TEMPLATE
1649 else:
1650 status = cls.Status.QUEUED
1651
1652 return cls(job=job, host=host, meta_host=meta_host,
1653 atomic_group=atomic_group, status=status)
1654
1655
showarda5288b42009-07-28 20:06:08 +00001656 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001657 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001658 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001659 self._check_for_updated_attributes()
1660
1661
showardc0ac3a72009-07-08 21:14:45 +00001662 def execution_path(self):
1663 """
1664 Path to this entry's results (relative to the base results directory).
1665 """
showardd1195652009-12-08 22:21:02 +00001666 return os.path.join(self.job.tag(), self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001667
1668
showard3f15eed2008-11-14 22:40:48 +00001669 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001670 """Returns the first non-None name found in priority order.
1671
1672 The priority order checked is: (1) host name; (2) meta host name; and
1673 (3) atomic group name.
1674 """
showard3f15eed2008-11-14 22:40:48 +00001675 if self.host:
1676 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001677 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +00001678 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +00001679 else:
1680 assert self.atomic_group, "no host, meta_host or atomic group!"
1681 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +00001682
1683
showard2bab8f42008-11-12 18:15:22 +00001684 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001685 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001686 self.active, self.complete = True, False
1687 elif self.status in self.COMPLETE_STATUSES:
1688 self.active, self.complete = False, True
1689 else:
1690 self.active, self.complete = False, False
1691
1692
1693 def on_attribute_changed(self, attribute, old_value):
1694 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001695 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1696 self.status)
showard2bab8f42008-11-12 18:15:22 +00001697
1698
jadmanski0afbb632008-06-06 21:10:57 +00001699 def is_meta_host_entry(self):
1700 'True if this is a entry has a meta_host instead of a host.'
1701 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001702
showarda3ab0d52008-11-03 19:03:47 +00001703
Simran Basic1b26762013-06-26 14:23:21 -07001704 # This code is shared between rpc_interface and models.HostQueueEntry.
1705 # Sadly due to circular imports between the 2 (crbug.com/230100) making it
1706 # a class method was the best way to refactor it. Attempting to put it in
1707 # rpc_utils or a new utils module failed as that would require us to import
1708 # models.py but to call it from here we would have to import the utils.py
1709 # thus creating a cycle.
1710 @classmethod
1711 def abort_host_queue_entries(cls, host_queue_entries):
1712 """Aborts a collection of host_queue_entries.
1713
1714 Abort these host queue entry and all host queue entries of jobs created
1715 by them.
1716
1717 @param host_queue_entries: List of host queue entries we want to abort.
1718 """
1719 # This isn't completely immune to race conditions since it's not atomic,
1720 # but it should be safe given the scheduler's behavior.
1721
1722 # TODO(milleral): crbug.com/230100
1723 # The |abort_host_queue_entries| rpc does nearly exactly this,
1724 # however, trying to re-use the code generates some horrible
1725 # circular import error. I'd be nice to refactor things around
1726 # sometime so the code could be reused.
1727
1728 # Fixpoint algorithm to find the whole tree of HQEs to abort to
1729 # minimize the total number of database queries:
1730 children = set()
1731 new_children = set(host_queue_entries)
1732 while new_children:
1733 children.update(new_children)
1734 new_child_ids = [hqe.job_id for hqe in new_children]
1735 new_children = HostQueueEntry.objects.filter(
1736 job__parent_job__in=new_child_ids,
1737 complete=False, aborted=False).all()
1738 # To handle circular parental relationships
1739 new_children = set(new_children) - children
1740
1741 # Associate a user with the host queue entries that we're about
1742 # to abort so that we can look up who to blame for the aborts.
1743 now = datetime.now()
1744 user = User.current_user()
1745 aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
1746 aborted_by=user, aborted_on=now) for hqe in children]
1747 AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
1748 # Bulk update all of the HQEs to set the abort bit.
1749 child_ids = [hqe.id for hqe in children]
1750 HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
1751
1752
Scott Zawalski23041432013-04-17 07:39:09 -07001753 def abort(self):
Alex Millerdea67042013-04-22 17:23:34 -07001754 """ Aborts this host queue entry.
Simran Basic1b26762013-06-26 14:23:21 -07001755
Alex Millerdea67042013-04-22 17:23:34 -07001756 Abort this host queue entry and all host queue entries of jobs created by
1757 this one.
1758
1759 """
showardd3dc1992009-04-22 21:01:40 +00001760 if not self.complete and not self.aborted:
Simran Basi97582a22013-06-27 12:03:21 -07001761 HostQueueEntry.abort_host_queue_entries([self])
mblighe8819cd2008-02-15 16:48:40 +00001762
showardd3dc1992009-04-22 21:01:40 +00001763
1764 @classmethod
1765 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001766 """Returns a modified status msg if the host queue entry was aborted.
1767
1768 @param cls: Implicit class object.
1769 @param status: The original status message.
1770 @param aborted: Whether the host queue entry was aborted.
1771 @param complete: Whether the host queue entry was completed.
1772 """
showardd3dc1992009-04-22 21:01:40 +00001773 if aborted and not complete:
1774 return 'Aborted (%s)' % status
1775 return status
1776
1777
1778 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001779 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00001780 return self.compute_full_status(self.status, self.aborted,
1781 self.complete)
1782
1783
1784 def _postprocess_object_dict(self, object_dict):
1785 object_dict['full_status'] = self.full_status()
1786
1787
jadmanski0afbb632008-06-06 21:10:57 +00001788 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001789 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001790 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00001791
showard12f3e322009-05-13 21:27:42 +00001792
showard4c119042008-09-29 19:16:18 +00001793
showarda5288b42009-07-28 20:06:08 +00001794 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00001795 hostname = None
1796 if self.host:
1797 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00001798 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00001799
1800
showard4c119042008-09-29 19:16:18 +00001801class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001802 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00001803 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
1804 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00001805 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00001806
1807 objects = model_logic.ExtendedManager()
1808
showard68c7aa02008-10-09 16:49:11 +00001809
showarda5288b42009-07-28 20:06:08 +00001810 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00001811 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00001812 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00001813
showard4c119042008-09-29 19:16:18 +00001814 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001815 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001816 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00001817
1818
1819class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
1820 """\
1821 job: job to use as a template
1822 owner: owner of the instantiated template
1823 start_date: Run the job at scheduled date
1824 loop_period: Re-run (loop) the job periodically
1825 (in every loop_period seconds)
1826 loop_count: Re-run (loop) count
1827 """
1828
1829 job = dbmodels.ForeignKey(Job)
1830 owner = dbmodels.ForeignKey(User)
1831 start_date = dbmodels.DateTimeField()
1832 loop_period = dbmodels.IntegerField(blank=True)
1833 loop_count = dbmodels.IntegerField(blank=True)
1834
1835 objects = model_logic.ExtendedManager()
1836
1837 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001838 """Metadata for class RecurringRun."""
showardeab66ce2009-12-23 00:03:56 +00001839 db_table = 'afe_recurring_run'
showard29f7cd22009-04-29 21:16:24 +00001840
showarda5288b42009-07-28 20:06:08 +00001841 def __unicode__(self):
1842 return u'RecurringRun(job %s, start %s, period %s, count %s)' % (
showard29f7cd22009-04-29 21:16:24 +00001843 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +00001844
1845
1846class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
1847 """\
1848 Tasks to run on hosts at the next time they are in the Ready state. Use this
1849 for high-priority tasks, such as forced repair or forced reinstall.
1850
1851 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00001852 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00001853 time_requested: date and time the request for this task was made
1854 is_active: task is currently running
1855 is_complete: task has finished running
beeps8bb1f7d2013-08-05 01:30:09 -07001856 is_aborted: task was aborted
showard2fe3f1d2009-07-06 20:19:11 +00001857 time_started: date and time the task started
Dan Shid0725542014-06-23 15:34:27 -07001858 time_finished: date and time the task finished
showard2fe3f1d2009-07-06 20:19:11 +00001859 queue_entry: Host queue entry waiting on this task (or None, if task was not
1860 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00001861 """
Alex Millerdfff2fd2013-05-28 13:05:06 -07001862 Task = enum.Enum('Verify', 'Cleanup', 'Repair', 'Reset', 'Provision',
Dan Shi07e09af2013-04-12 09:31:29 -07001863 string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00001864
1865 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00001866 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00001867 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00001868 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00001869 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
1870 null=False)
1871 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
1872 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
beeps8bb1f7d2013-08-05 01:30:09 -07001873 is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00001874 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00001875 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00001876 success = dbmodels.BooleanField(default=False, blank=False, null=False)
Dan Shid0725542014-06-23 15:34:27 -07001877 time_finished = dbmodels.DateTimeField(null=True, blank=True)
showard6d7b2ff2009-06-10 00:16:47 +00001878
1879 objects = model_logic.ExtendedManager()
1880
1881
showard9bb960b2009-11-19 01:02:11 +00001882 def save(self, **kwargs):
1883 if self.queue_entry:
1884 self.requested_by = User.objects.get(
1885 login=self.queue_entry.job.owner)
1886 super(SpecialTask, self).save(**kwargs)
1887
1888
showarded2afea2009-07-07 20:54:07 +00001889 def execution_path(self):
Prashanth Balasubramaniande87dea2014-11-09 17:47:10 -08001890 """Get the execution path of the SpecialTask.
1891
1892 This method returns different paths depending on where a
1893 the task ran:
1894 * Master: hosts/hostname/task_id-task_type
1895 * Shard: Master_path/time_created
1896 This is to work around the fact that a shard can fail independent
1897 of the master, and be replaced by another shard that has the same
1898 hosts. Without the time_created stamp the logs of the tasks running
1899 on the second shard will clobber the logs from the first in google
1900 storage, because task ids are not globally unique.
1901
1902 @return: An execution path for the task.
1903 """
1904 results_path = 'hosts/%s/%s-%s' % (self.host.hostname, self.id,
1905 self.task.lower())
1906
1907 # If we do this on the master it will break backward compatibility,
1908 # as there are tasks that currently don't have timestamps. If a host
1909 # or job has been sent to a shard, the rpc for that host/job will
1910 # be redirected to the shard, so this global_config check will happen
1911 # on the shard the logs are on.
1912 is_shard = global_config.global_config.get_config_value(
1913 'SHARD', 'shard_hostname', type=str, default='')
1914 if not is_shard:
1915 return results_path
1916
1917 # Generate a uid to disambiguate special task result directories
1918 # in case this shard fails. The simplest uid is the job_id, however
1919 # in rare cases tasks do not have jobs associated with them (eg:
1920 # frontend verify), so just use the creation timestamp. The clocks
1921 # between a shard and master should always be in sync. Any discrepancies
1922 # will be brought to our attention in the form of job timeouts.
1923 uid = self.time_requested.strftime('%Y%d%m%H%M%S')
1924
1925 # TODO: This is a hack, however it is the easiest way to achieve
1926 # correctness. There is currently some debate over the future of
1927 # tasks in our infrastructure and refactoring everything right
1928 # now isn't worth the time.
1929 return '%s/%s' % (results_path, uid)
showarded2afea2009-07-07 20:54:07 +00001930
1931
showardc0ac3a72009-07-08 21:14:45 +00001932 # property to emulate HostQueueEntry.status
1933 @property
1934 def status(self):
1935 """
1936 Return a host queue entry status appropriate for this task. Although
1937 SpecialTasks are not HostQueueEntries, it is helpful to the user to
1938 present similar statuses.
1939 """
1940 if self.is_complete:
showarde60e44e2009-11-13 20:45:38 +00001941 if self.success:
1942 return HostQueueEntry.Status.COMPLETED
1943 return HostQueueEntry.Status.FAILED
showardc0ac3a72009-07-08 21:14:45 +00001944 if self.is_active:
1945 return HostQueueEntry.Status.RUNNING
1946 return HostQueueEntry.Status.QUEUED
1947
1948
1949 # property to emulate HostQueueEntry.started_on
1950 @property
1951 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001952 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00001953 return self.time_started
1954
1955
showard6d7b2ff2009-06-10 00:16:47 +00001956 @classmethod
showardc5103442010-01-15 00:20:26 +00001957 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001958 """Schedules a special task on a host if not already scheduled.
1959
1960 @param cls: Implicit class object.
1961 @param host: The host to use.
1962 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00001963 """
showardc5103442010-01-15 00:20:26 +00001964 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
1965 is_active=False,
1966 is_complete=False)
1967 if existing_tasks:
1968 return existing_tasks[0]
1969
1970 special_task = SpecialTask(host=host, task=task,
1971 requested_by=User.current_user())
1972 special_task.save()
1973 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00001974
1975
beeps8bb1f7d2013-08-05 01:30:09 -07001976 def abort(self):
1977 """ Abort this special task."""
1978 self.is_aborted = True
1979 self.save()
1980
1981
showarded2afea2009-07-07 20:54:07 +00001982 def activate(self):
showard474d1362009-08-20 23:32:01 +00001983 """
1984 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00001985 """
showard97446882009-07-20 22:37:28 +00001986 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00001987 self.is_active = True
1988 self.time_started = datetime.now()
1989 self.save()
1990
1991
showarde60e44e2009-11-13 20:45:38 +00001992 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001993 """Sets a task as completed.
1994
1995 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00001996 """
showard97446882009-07-20 22:37:28 +00001997 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00001998 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00001999 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00002000 self.success = success
Dan Shid85d6112014-07-14 10:32:55 -07002001 if self.time_started:
2002 self.time_finished = datetime.now()
showard2fe3f1d2009-07-06 20:19:11 +00002003 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00002004
2005
2006 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002007 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00002008 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00002009
showard474d1362009-08-20 23:32:01 +00002010
showarda5288b42009-07-28 20:06:08 +00002011 def __unicode__(self):
2012 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00002013 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00002014 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00002015 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00002016 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00002017 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00002018
2019 return result
Dan Shi6964fa52014-12-18 11:04:27 -08002020
2021
2022class StableVersion(dbmodels.Model, model_logic.ModelExtensions):
2023
2024 board = dbmodels.CharField(max_length=255, unique=True)
2025 version = dbmodels.CharField(max_length=255)
2026
2027 class Meta:
2028 """Metadata for class StableVersion."""
Fang Deng86248502014-12-18 16:38:00 -08002029 db_table = 'afe_stable_versions'