blob: b0c7ad5fb3200c45f90b1f15fb08a13c39dd6858 [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
Fang Dengff361592015-02-02 15:27:34 -0800139
jadmanski0afbb632008-06-06 21:10:57 +0000140 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800141 """Metadata for class Label."""
showardeab66ce2009-12-23 00:03:56 +0000142 db_table = 'afe_labels'
mblighe8819cd2008-02-15 16:48:40 +0000143
Fang Dengff361592015-02-02 15:27:34 -0800144
showarda5288b42009-07-28 20:06:08 +0000145 def __unicode__(self):
146 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000147
148
Jakob Jülich92c06332014-08-25 19:06:57 +0000149class Shard(dbmodels.Model, model_logic.ModelExtensions):
150
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700151 hostname = dbmodels.CharField(max_length=255, unique=True)
152
153 name_field = 'hostname'
154
Jakob Jülich92c06332014-08-25 19:06:57 +0000155 labels = dbmodels.ManyToManyField(Label, blank=True,
156 db_table='afe_shards_labels')
157
158 class Meta:
159 """Metadata for class ParameterizedJob."""
160 db_table = 'afe_shards'
161
162
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -0800163 def rpc_hostname(self):
164 """Get the rpc hostname of the shard.
165
166 @return: Just the shard hostname for all non-testing environments.
167 The address of the default gateway for vm testing environments.
168 """
169 # TODO: Figure out a better solution for testing. Since no 2 shards
170 # can run on the same host, if the shard hostname is localhost we
171 # conclude that it must be a vm in a test cluster. In such situations
172 # a name of localhost:<port> is necessary to achieve the correct
173 # afe links/redirection from the frontend (this happens through the
174 # host), but for rpcs that are performed *on* the shard, they need to
175 # use the address of the gateway.
176 hostname = self.hostname.split(':')[0]
177 if site_utils.is_localhost(hostname):
178 return self.hostname.replace(
179 hostname, site_utils.DEFAULT_VM_GATEWAY)
180 return self.hostname
181
182
jamesren76fcf192010-04-21 20:39:50 +0000183class Drone(dbmodels.Model, model_logic.ModelExtensions):
184 """
185 A scheduler drone
186
187 hostname: the drone's hostname
188 """
189 hostname = dbmodels.CharField(max_length=255, unique=True)
190
191 name_field = 'hostname'
192 objects = model_logic.ExtendedManager()
193
194
195 def save(self, *args, **kwargs):
196 if not User.current_user().is_superuser():
197 raise Exception('Only superusers may edit drones')
198 super(Drone, self).save(*args, **kwargs)
199
200
201 def delete(self):
202 if not User.current_user().is_superuser():
203 raise Exception('Only superusers may delete drones')
204 super(Drone, self).delete()
205
206
207 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800208 """Metadata for class Drone."""
jamesren76fcf192010-04-21 20:39:50 +0000209 db_table = 'afe_drones'
210
211 def __unicode__(self):
212 return unicode(self.hostname)
213
214
215class DroneSet(dbmodels.Model, model_logic.ModelExtensions):
216 """
217 A set of scheduler drones
218
219 These will be used by the scheduler to decide what drones a job is allowed
220 to run on.
221
222 name: the drone set's name
223 drones: the drones that are part of the set
224 """
225 DRONE_SETS_ENABLED = global_config.global_config.get_config_value(
226 'SCHEDULER', 'drone_sets_enabled', type=bool, default=False)
227 DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value(
228 'SCHEDULER', 'default_drone_set_name', default=None)
229
230 name = dbmodels.CharField(max_length=255, unique=True)
231 drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones')
232
233 name_field = 'name'
234 objects = model_logic.ExtendedManager()
235
236
237 def save(self, *args, **kwargs):
238 if not User.current_user().is_superuser():
239 raise Exception('Only superusers may edit drone sets')
240 super(DroneSet, self).save(*args, **kwargs)
241
242
243 def delete(self):
244 if not User.current_user().is_superuser():
245 raise Exception('Only superusers may delete drone sets')
246 super(DroneSet, self).delete()
247
248
249 @classmethod
250 def drone_sets_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800251 """Returns whether drone sets are enabled.
252
253 @param cls: Implicit class object.
254 """
jamesren76fcf192010-04-21 20:39:50 +0000255 return cls.DRONE_SETS_ENABLED
256
257
258 @classmethod
259 def default_drone_set_name(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800260 """Returns the default drone set name.
261
262 @param cls: Implicit class object.
263 """
jamesren76fcf192010-04-21 20:39:50 +0000264 return cls.DEFAULT_DRONE_SET_NAME
265
266
267 @classmethod
268 def get_default(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800269 """Gets the default drone set name, compatible with Job.add_object.
270
271 @param cls: Implicit class object.
272 """
jamesren76fcf192010-04-21 20:39:50 +0000273 return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME)
274
275
276 @classmethod
277 def resolve_name(cls, drone_set_name):
278 """
279 Returns the name of one of these, if not None, in order of preference:
280 1) the drone set given,
281 2) the current user's default drone set, or
282 3) the global default drone set
283
284 or returns None if drone sets are disabled
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800285
286 @param cls: Implicit class object.
287 @param drone_set_name: A drone set name.
jamesren76fcf192010-04-21 20:39:50 +0000288 """
289 if not cls.drone_sets_enabled():
290 return None
291
292 user = User.current_user()
293 user_drone_set_name = user.drone_set and user.drone_set.name
294
295 return drone_set_name or user_drone_set_name or cls.get_default().name
296
297
298 def get_drone_hostnames(self):
299 """
300 Gets the hostnames of all drones in this drone set
301 """
302 return set(self.drones.all().values_list('hostname', flat=True))
303
304
305 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800306 """Metadata for class DroneSet."""
jamesren76fcf192010-04-21 20:39:50 +0000307 db_table = 'afe_drone_sets'
308
309 def __unicode__(self):
310 return unicode(self.name)
311
312
showardfb2a7fa2008-07-17 17:04:12 +0000313class User(dbmodels.Model, model_logic.ModelExtensions):
314 """\
315 Required:
316 login :user login name
317
318 Optional:
319 access_level: 0=User (default), 1=Admin, 100=Root
320 """
321 ACCESS_ROOT = 100
322 ACCESS_ADMIN = 1
323 ACCESS_USER = 0
324
showard64a95952010-01-13 21:27:16 +0000325 AUTOTEST_SYSTEM = 'autotest_system'
326
showarda5288b42009-07-28 20:06:08 +0000327 login = dbmodels.CharField(max_length=255, unique=True)
showardfb2a7fa2008-07-17 17:04:12 +0000328 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
329
showard0fc38302008-10-23 00:44:07 +0000330 # user preferences
jamesrendd855242010-03-02 22:23:44 +0000331 reboot_before = dbmodels.SmallIntegerField(
332 choices=model_attributes.RebootBefore.choices(), blank=True,
333 default=DEFAULT_REBOOT_BEFORE)
334 reboot_after = dbmodels.SmallIntegerField(
335 choices=model_attributes.RebootAfter.choices(), blank=True,
336 default=DEFAULT_REBOOT_AFTER)
jamesren76fcf192010-04-21 20:39:50 +0000337 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
showard97db5ba2008-11-12 18:18:02 +0000338 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000339
showardfb2a7fa2008-07-17 17:04:12 +0000340 name_field = 'login'
341 objects = model_logic.ExtendedManager()
342
343
showarda5288b42009-07-28 20:06:08 +0000344 def save(self, *args, **kwargs):
showardfb2a7fa2008-07-17 17:04:12 +0000345 # is this a new object being saved for the first time?
346 first_time = (self.id is None)
347 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000348 if user and not user.is_superuser() and user.login != self.login:
349 raise AclAccessViolation("You cannot modify user " + self.login)
showarda5288b42009-07-28 20:06:08 +0000350 super(User, self).save(*args, **kwargs)
showardfb2a7fa2008-07-17 17:04:12 +0000351 if first_time:
352 everyone = AclGroup.objects.get(name='Everyone')
353 everyone.users.add(self)
354
355
356 def is_superuser(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800357 """Returns whether the user has superuser access."""
showardfb2a7fa2008-07-17 17:04:12 +0000358 return self.access_level >= self.ACCESS_ROOT
359
360
showard64a95952010-01-13 21:27:16 +0000361 @classmethod
362 def current_user(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800363 """Returns the current user.
364
365 @param cls: Implicit class object.
366 """
showard64a95952010-01-13 21:27:16 +0000367 user = thread_local.get_user()
368 if user is None:
showardcfcdd802010-01-15 00:16:33 +0000369 user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM)
showard64a95952010-01-13 21:27:16 +0000370 user.access_level = cls.ACCESS_ROOT
371 user.save()
372 return user
373
374
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -0800375 @classmethod
376 def get_record(cls, data):
377 """Check the database for an identical record.
378
379 Check for a record with matching id and login. If one exists,
380 return it. If one does not exist there is a possibility that
381 the following cases have happened:
382 1. Same id, different login
383 We received: "1 chromeos-test"
384 And we have: "1 debug-user"
385 In this case we need to delete "1 debug_user" and insert
386 "1 chromeos-test".
387
388 2. Same login, different id:
389 We received: "1 chromeos-test"
390 And we have: "2 chromeos-test"
391 In this case we need to delete "2 chromeos-test" and insert
392 "1 chromeos-test".
393
394 As long as this method deletes bad records and raises the
395 DoesNotExist exception the caller will handle creating the
396 new record.
397
398 @raises: DoesNotExist, if a record with the matching login and id
399 does not exist.
400 """
401
402 # Both the id and login should be uniqe but there are cases when
403 # we might already have a user with the same login/id because
404 # current_user will proactively create a user record if it doesn't
405 # exist. Since we want to avoid conflict between the master and
406 # shard, just delete any existing user records that don't match
407 # what we're about to deserialize from the master.
408 try:
409 return cls.objects.get(login=data['login'], id=data['id'])
410 except cls.DoesNotExist:
411 cls.delete_matching_record(login=data['login'])
412 cls.delete_matching_record(id=data['id'])
413 raise
414
415
showardfb2a7fa2008-07-17 17:04:12 +0000416 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800417 """Metadata for class User."""
showardeab66ce2009-12-23 00:03:56 +0000418 db_table = 'afe_users'
showardfb2a7fa2008-07-17 17:04:12 +0000419
showarda5288b42009-07-28 20:06:08 +0000420 def __unicode__(self):
421 return unicode(self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000422
423
Prashanth B489b91d2014-03-15 12:17:16 -0700424class Host(model_logic.ModelWithInvalid, rdb_model_extensions.AbstractHostModel,
showardf8b19042009-05-12 17:22:49 +0000425 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000426 """\
427 Required:
428 hostname
mblighe8819cd2008-02-15 16:48:40 +0000429
jadmanski0afbb632008-06-06 21:10:57 +0000430 optional:
showard21baa452008-10-21 00:08:39 +0000431 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000432
jadmanski0afbb632008-06-06 21:10:57 +0000433 Internal:
Prashanth B489b91d2014-03-15 12:17:16 -0700434 From AbstractHostModel:
435 synch_id: currently unused
436 status: string describing status of host
437 invalid: true if the host has been deleted
438 protection: indicates what can be done to this host during repair
439 lock_time: DateTime at which the host was locked
440 dirty: true if the host has been used without being rebooted
441 Local:
442 locked_by: user that locked the host, or null if the host is unlocked
jadmanski0afbb632008-06-06 21:10:57 +0000443 """
mblighe8819cd2008-02-15 16:48:40 +0000444
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700445 SERIALIZATION_LINKS_TO_FOLLOW = set(['aclgroup_set',
446 'hostattribute_set',
447 'labels',
448 'shard'])
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800449 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['invalid'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700450
Jakob Juelichf88fa932014-09-03 17:58:04 -0700451
452 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700453 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -0700454 self.shard = Shard.deserialize(data)
455
456
Prashanth B489b91d2014-03-15 12:17:16 -0700457 # Note: Only specify foreign keys here, specify all native host columns in
458 # rdb_model_extensions instead.
459 Protection = host_protections.Protection
showardeab66ce2009-12-23 00:03:56 +0000460 labels = dbmodels.ManyToManyField(Label, blank=True,
461 db_table='afe_hosts_labels')
showardfb2a7fa2008-07-17 17:04:12 +0000462 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
jadmanski0afbb632008-06-06 21:10:57 +0000463 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000464 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000465 valid_objects = model_logic.ValidObjectsManager()
beepscc9fc702013-12-02 12:45:38 -0800466 leased_objects = model_logic.LeasedHostManager()
mbligh5244cbb2008-04-24 20:39:52 +0000467
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700468 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
showard2bab8f42008-11-12 18:15:22 +0000469
470 def __init__(self, *args, **kwargs):
471 super(Host, self).__init__(*args, **kwargs)
472 self._record_attributes(['status'])
473
474
showardb8471e32008-07-03 19:51:08 +0000475 @staticmethod
476 def create_one_time_host(hostname):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800477 """Creates a one-time host.
478
479 @param hostname: The name for the host.
480 """
showardb8471e32008-07-03 19:51:08 +0000481 query = Host.objects.filter(hostname=hostname)
482 if query.count() == 0:
483 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000484 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000485 else:
486 host = query[0]
487 if not host.invalid:
488 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000489 'hostname' : '%s already exists in the autotest DB. '
490 'Select it rather than entering it as a one time '
491 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000492 })
showard1ab512b2008-07-30 23:39:04 +0000493 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000494 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000495 host.save()
showard2924b0a2009-06-18 23:16:15 +0000496 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000497 return host
mbligh5244cbb2008-04-24 20:39:52 +0000498
showard1ff7b2e2009-05-15 23:17:18 +0000499
Jakob Juelich59cfe542014-09-02 16:37:46 -0700500 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -0700501 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -0700502 """Assigns hosts to a shard.
503
Jakob Juelich1b525742014-09-30 13:08:07 -0700504 For all labels that have been assigned to this shard, all hosts that
505 have this label, are assigned to this shard.
506
507 Hosts that are assigned to the shard but aren't already present on the
508 shard are returned.
Jakob Juelich59cfe542014-09-02 16:37:46 -0700509
510 @param shard: The shard object to assign labels/hosts for.
Jakob Juelich1b525742014-09-30 13:08:07 -0700511 @param known_ids: List of all host-ids the shard already knows.
512 This is used to figure out which hosts should be sent
513 to the shard. If shard_ids were used instead, hosts
514 would only be transferred once, even if the client
515 failed persisting them.
516 The number of hosts usually lies in O(100), so the
517 overhead is acceptable.
518
Jakob Juelich59cfe542014-09-02 16:37:46 -0700519 @returns the hosts objects that should be sent to the shard.
520 """
521
522 # Disclaimer: concurrent heartbeats should theoretically not occur in
523 # the current setup. As they may be introduced in the near future,
524 # this comment will be left here.
525
526 # Sending stuff twice is acceptable, but forgetting something isn't.
527 # Detecting duplicates on the client is easy, but here it's harder. The
528 # following options were considered:
529 # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
530 # than select returned, as concurrently more hosts might have been
531 # inserted
532 # - UPDATE and then SELECT WHERE shard=shard: select always returns all
533 # hosts for the shard, this is overhead
534 # - SELECT and then UPDATE only selected without requerying afterwards:
535 # returns the old state of the records.
536 host_ids = list(Host.objects.filter(
Jakob Juelich59cfe542014-09-02 16:37:46 -0700537 labels=shard.labels.all(),
538 leased=False
Jakob Juelich1b525742014-09-30 13:08:07 -0700539 ).exclude(
540 id__in=known_ids,
Jakob Juelich59cfe542014-09-02 16:37:46 -0700541 ).values_list('pk', flat=True))
542
543 if host_ids:
544 Host.objects.filter(pk__in=host_ids).update(shard=shard)
545 return list(Host.objects.filter(pk__in=host_ids).all())
546 return []
547
showardafd97de2009-10-01 18:45:09 +0000548 def resurrect_object(self, old_object):
549 super(Host, self).resurrect_object(old_object)
550 # invalid hosts can be in use by the scheduler (as one-time hosts), so
551 # don't change the status
552 self.status = old_object.status
553
554
jadmanski0afbb632008-06-06 21:10:57 +0000555 def clean_object(self):
556 self.aclgroup_set.clear()
557 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000558
559
Dan Shia0acfbc2014-10-14 15:56:23 -0700560 def record_state(self, type_str, state, value, other_metadata=None):
561 """Record metadata in elasticsearch.
562
563 @param type_str: sets the _type field in elasticsearch db.
564 @param state: string representing what state we are recording,
565 e.g. 'locked'
566 @param value: value of the state, e.g. True
567 @param other_metadata: Other metadata to store in metaDB.
568 """
569 metadata = {
570 state: value,
571 'hostname': self.hostname,
572 }
573 if other_metadata:
574 metadata = dict(metadata.items() + other_metadata.items())
Matthew Sartori68186332015-04-27 17:19:53 -0700575 autotest_es.post(use_http=True, type_str=type_str, metadata=metadata)
Dan Shia0acfbc2014-10-14 15:56:23 -0700576
577
showarda5288b42009-07-28 20:06:08 +0000578 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000579 # extra spaces in the hostname can be a sneaky source of errors
580 self.hostname = self.hostname.strip()
581 # is this a new object being saved for the first time?
582 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000583 if not first_time:
584 AclGroup.check_for_acl_violation_hosts([self])
Dan Shia0acfbc2014-10-14 15:56:23 -0700585 # If locked is changed, send its status and user made the change to
586 # metaDB. Locks are important in host history because if a device is
587 # locked then we don't really care what state it is in.
showardfb2a7fa2008-07-17 17:04:12 +0000588 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000589 self.locked_by = User.current_user()
showardfb2a7fa2008-07-17 17:04:12 +0000590 self.lock_time = datetime.now()
Dan Shia0acfbc2014-10-14 15:56:23 -0700591 self.record_state('lock_history', 'locked', self.locked,
Matthew Sartori68186332015-04-27 17:19:53 -0700592 {'changed_by': self.locked_by.login,
593 'lock_reason': self.lock_reason})
showard21baa452008-10-21 00:08:39 +0000594 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000595 elif not self.locked and self.locked_by:
Dan Shia0acfbc2014-10-14 15:56:23 -0700596 self.record_state('lock_history', 'locked', self.locked,
597 {'changed_by': self.locked_by.login})
showardfb2a7fa2008-07-17 17:04:12 +0000598 self.locked_by = None
599 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000600 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000601 if first_time:
602 everyone = AclGroup.objects.get(name='Everyone')
603 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000604 self._check_for_updated_attributes()
605
mblighe8819cd2008-02-15 16:48:40 +0000606
showardb8471e32008-07-03 19:51:08 +0000607 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000608 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000609 for queue_entry in self.hostqueueentry_set.all():
610 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000611 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000612 super(Host, self).delete()
613
mblighe8819cd2008-02-15 16:48:40 +0000614
showard2bab8f42008-11-12 18:15:22 +0000615 def on_attribute_changed(self, attribute, old_value):
616 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000617 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000618
619
showard29f7cd22009-04-29 21:16:24 +0000620 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800621 """Enqueue a job on this host.
622
623 @param job: A job to enqueue.
624 @param atomic_group: The associated atomic group.
625 @param is_template: Whther the status should be "Template".
626 """
showard29f7cd22009-04-29 21:16:24 +0000627 queue_entry = HostQueueEntry.create(host=self, job=job,
628 is_template=is_template,
629 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000630 # allow recovery of dead hosts from the frontend
631 if not self.active_queue_entry() and self.is_dead():
632 self.status = Host.Status.READY
633 self.save()
634 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000635
showard08f981b2008-06-24 21:59:03 +0000636 block = IneligibleHostQueue(job=job, host=self)
637 block.save()
638
mblighe8819cd2008-02-15 16:48:40 +0000639
jadmanski0afbb632008-06-06 21:10:57 +0000640 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800641 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000642 # TODO(showard): slighly hacky?
643 platforms = self.labels.filter(platform=True)
644 if len(platforms) == 0:
645 return None
646 return platforms[0]
647 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000648
649
showardcafd16e2009-05-29 18:37:49 +0000650 @classmethod
651 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800652 """Verify the specified hosts have no associated platforms.
653
654 @param cls: Implicit class object.
655 @param hosts: The hosts to verify.
656 @raises model_logic.ValidationError if any hosts already have a
657 platform.
658 """
showardcafd16e2009-05-29 18:37:49 +0000659 Host.objects.populate_relationships(hosts, Label, 'label_list')
660 errors = []
661 for host in hosts:
662 platforms = [label.name for label in host.label_list
663 if label.platform]
664 if platforms:
665 # do a join, just in case this host has multiple platforms,
666 # we'll be able to see it
667 errors.append('Host %s already has a platform: %s' % (
668 host.hostname, ', '.join(platforms)))
669 if errors:
670 raise model_logic.ValidationError({'labels': '; '.join(errors)})
671
672
jadmanski0afbb632008-06-06 21:10:57 +0000673 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800674 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000675 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000676
677
jadmanski0afbb632008-06-06 21:10:57 +0000678 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800679 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000680 active = list(self.hostqueueentry_set.filter(active=True))
681 if not active:
682 return None
683 assert len(active) == 1, ('More than one active entry for '
684 'host ' + self.hostname)
685 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000686
687
showardf8b19042009-05-12 17:22:49 +0000688 def _get_attribute_model_and_args(self, attribute):
689 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000690
691
Fang Dengff361592015-02-02 15:27:34 -0800692 @classmethod
693 def get_attribute_model(cls):
694 """Return the attribute model.
695
696 Override method in parent class. See ModelExtensions for details.
697 @returns: The attribute model of Host.
698 """
699 return HostAttribute
700
701
jadmanski0afbb632008-06-06 21:10:57 +0000702 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800703 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000704 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000705
Fang Dengff361592015-02-02 15:27:34 -0800706
showarda5288b42009-07-28 20:06:08 +0000707 def __unicode__(self):
708 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000709
710
MK Ryuacf35922014-10-03 14:56:49 -0700711class HostAttribute(dbmodels.Model, model_logic.ModelExtensions):
showard0957a842009-05-11 19:25:08 +0000712 """Arbitrary keyvals associated with hosts."""
Fang Deng86248502014-12-18 16:38:00 -0800713
714 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Fang Dengff361592015-02-02 15:27:34 -0800715 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
showard0957a842009-05-11 19:25:08 +0000716 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000717 attribute = dbmodels.CharField(max_length=90)
718 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000719
720 objects = model_logic.ExtendedManager()
721
722 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800723 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000724 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000725
726
Fang Dengff361592015-02-02 15:27:34 -0800727 @classmethod
728 def get_record(cls, data):
729 """Check the database for an identical record.
730
731 Use host_id and attribute to search for a existing record.
732
733 @raises: DoesNotExist, if no record found
734 @raises: MultipleObjectsReturned if multiple records found.
735 """
736 # TODO(fdeng): We should use host_id and attribute together as
737 # a primary key in the db.
738 return cls.objects.get(host_id=data['host_id'],
739 attribute=data['attribute'])
740
741
742 @classmethod
743 def deserialize(cls, data):
744 """Override deserialize in parent class.
745
746 Do not deserialize id as id is not kept consistent on master and shards.
747
748 @param data: A dictionary of data to deserialize.
749
750 @returns: A HostAttribute object.
751 """
752 if data:
753 data.pop('id')
754 return super(HostAttribute, cls).deserialize(data)
755
756
showard7c785282008-05-29 19:45:12 +0000757class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000758 """\
759 Required:
showard909c7a62008-07-15 21:52:38 +0000760 author: author name
761 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000762 name: test name
showard909c7a62008-07-15 21:52:38 +0000763 time: short, medium, long
764 test_class: This describes the class for your the test belongs in.
765 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000766 test_type: Client or Server
767 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000768 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
769 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000770 i.e. if sync_count = 2 it is a sync job that requires two
771 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000772 Optional:
showard909c7a62008-07-15 21:52:38 +0000773 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000774 dependency_labels: many-to-many relationship with labels corresponding to
775 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000776 experimental: If this is set to True production servers will ignore the test
777 run_verify: Whether or not the scheduler should run the verify stage
Dan Shi07e09af2013-04-12 09:31:29 -0700778 run_reset: Whether or not the scheduler should run the reset stage
Aviv Keshet9af96d32013-03-05 12:56:24 -0800779 test_retry: Number of times to retry test if the test did not complete
780 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +0000781 """
showard909c7a62008-07-15 21:52:38 +0000782 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000783
showarda5288b42009-07-28 20:06:08 +0000784 name = dbmodels.CharField(max_length=255, unique=True)
785 author = dbmodels.CharField(max_length=255)
786 test_class = dbmodels.CharField(max_length=255)
787 test_category = dbmodels.CharField(max_length=255)
788 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000789 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000790 experimental = dbmodels.BooleanField(default=True)
Dan Shi07e09af2013-04-12 09:31:29 -0700791 run_verify = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +0000792 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
793 default=TestTime.MEDIUM)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700794 test_type = dbmodels.SmallIntegerField(
795 choices=control_data.CONTROL_TYPE.choices())
showard909c7a62008-07-15 21:52:38 +0000796 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000797 path = dbmodels.CharField(max_length=255, unique=True)
Aviv Keshet9af96d32013-03-05 12:56:24 -0800798 test_retry = dbmodels.IntegerField(blank=True, default=0)
Dan Shi07e09af2013-04-12 09:31:29 -0700799 run_reset = dbmodels.BooleanField(default=True)
mblighe8819cd2008-02-15 16:48:40 +0000800
showardeab66ce2009-12-23 00:03:56 +0000801 dependency_labels = (
802 dbmodels.ManyToManyField(Label, blank=True,
803 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000804 name_field = 'name'
805 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000806
807
jamesren35a70222010-02-16 19:30:46 +0000808 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800809 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +0000810 escaped_description = saxutils.escape(self.description)
811 return '<span style="white-space:pre">%s</span>' % escaped_description
812 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000813 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000814
815
jadmanski0afbb632008-06-06 21:10:57 +0000816 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800817 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +0000818 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000819
showarda5288b42009-07-28 20:06:08 +0000820 def __unicode__(self):
821 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000822
823
jamesren4a41e012010-07-16 22:33:48 +0000824class TestParameter(dbmodels.Model):
825 """
826 A declared parameter of a test
827 """
828 test = dbmodels.ForeignKey(Test)
829 name = dbmodels.CharField(max_length=255)
830
831 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800832 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000833 db_table = 'afe_test_parameters'
834 unique_together = ('test', 'name')
835
836 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -0700837 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +0000838
839
showard2b9a88b2008-06-13 20:55:03 +0000840class Profiler(dbmodels.Model, model_logic.ModelExtensions):
841 """\
842 Required:
843 name: profiler name
844 test_type: Client or Server
845
846 Optional:
847 description: arbirary text description
848 """
showarda5288b42009-07-28 20:06:08 +0000849 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000850 description = dbmodels.TextField(blank=True)
851
852 name_field = 'name'
853 objects = model_logic.ExtendedManager()
854
855
856 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800857 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +0000858 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000859
showarda5288b42009-07-28 20:06:08 +0000860 def __unicode__(self):
861 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000862
863
showard7c785282008-05-29 19:45:12 +0000864class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000865 """\
866 Required:
867 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000868
jadmanski0afbb632008-06-06 21:10:57 +0000869 Optional:
870 description: arbitrary description of group
871 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700872
873 SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
874
showarda5288b42009-07-28 20:06:08 +0000875 name = dbmodels.CharField(max_length=255, unique=True)
876 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000877 users = dbmodels.ManyToManyField(User, blank=False,
878 db_table='afe_acl_groups_users')
879 hosts = dbmodels.ManyToManyField(Host, blank=True,
880 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000881
jadmanski0afbb632008-06-06 21:10:57 +0000882 name_field = 'name'
883 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000884
showard08f981b2008-06-24 21:59:03 +0000885 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000886 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800887 """Verify the current user has access to the specified hosts.
888
889 @param hosts: The hosts to verify against.
890 @raises AclAccessViolation if the current user doesn't have access
891 to a host.
892 """
showard64a95952010-01-13 21:27:16 +0000893 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000894 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000895 return
showard3dd47c22008-07-10 00:41:36 +0000896 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000897 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000898 for host in hosts:
899 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800900 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +0000901 no_access = (isinstance(host, Host)
902 and not host.invalid
903 and int(host.id) not in accessible_host_ids)
904 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000905 raise AclAccessViolation("%s does not have access to %s" %
906 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000907
showard9dbdcda2008-10-14 17:34:36 +0000908
909 @staticmethod
showarddc817512008-11-12 18:16:41 +0000910 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800911 """Look for queue entries that aren't abortable by the current user.
912
913 An entry is not abortable if:
914 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +0000915 * the machine isn't ACL-accessible, or
916 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800917
918 @param queue_entries: The queue entries to check.
919 @raises AclAccessViolation if a queue entry is not abortable by the
920 current user.
showarddc817512008-11-12 18:16:41 +0000921 """
showard64a95952010-01-13 21:27:16 +0000922 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000923 if user.is_superuser():
924 return
showarddc817512008-11-12 18:16:41 +0000925 not_owned = queue_entries.exclude(job__owner=user.login)
926 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000927 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
928 # 1.0.
929 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000930 accessible_ids = set(
931 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000932 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000933 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000934 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000935 cannot_abort = [entry for entry in not_owned.select_related()
936 if entry.id not in accessible_ids
937 or entry.id in public_ids]
938 if len(cannot_abort) == 0:
939 return
940 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000941 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000942 for entry in cannot_abort)
943 raise AclAccessViolation('You cannot abort the following job entries: '
944 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000945
946
showard3dd47c22008-07-10 00:41:36 +0000947 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800948 """Verifies the current user has acces to this ACL group.
949
950 @raises AclAccessViolation if the current user doesn't have access to
951 this ACL group.
952 """
showard64a95952010-01-13 21:27:16 +0000953 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000954 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000955 return
956 if self.name == 'Everyone':
957 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000958 if not user in self.users.all():
959 raise AclAccessViolation("You do not have access to %s"
960 % self.name)
961
962 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000963 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800964 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +0000965 everyone = AclGroup.objects.get(name='Everyone')
966
showard3dd47c22008-07-10 00:41:36 +0000967 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000968 # TODO(showard): this is a bit of a hack, since the fact that this query
969 # works is kind of a coincidence of Django internals. This trick
970 # doesn't work in general (on all foreign key relationships). I'll
971 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000972 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000973 everyone.hosts.add(*orphaned_hosts.distinct())
974
975 # find hosts in both Everyone and another ACL group, and remove them
976 # from Everyone
showarda5288b42009-07-28 20:06:08 +0000977 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
978 acled_hosts = set()
979 for host in hosts_in_everyone:
980 # Has an ACL group other than Everyone
981 if host.aclgroup_set.count() > 1:
982 acled_hosts.add(host)
983 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +0000984
985
986 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000987 if (self.name == 'Everyone'):
988 raise AclAccessViolation("You cannot delete 'Everyone'!")
989 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000990 super(AclGroup, self).delete()
991 self.on_host_membership_change()
992
993
showard04f2cd82008-07-25 20:53:31 +0000994 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800995 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +0000996 if not self.users.count():
showard64a95952010-01-13 21:27:16 +0000997 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +0000998
999
showard8cbaf1e2009-09-08 16:27:04 +00001000 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001001 """Called after a save.
1002
1003 @param change: Whether there was a change.
1004 """
showard8cbaf1e2009-09-08 16:27:04 +00001005 if not change:
showard64a95952010-01-13 21:27:16 +00001006 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +00001007 self.add_current_user_if_empty()
1008 self.on_host_membership_change()
1009
1010
1011 def save(self, *args, **kwargs):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001012 change = bool(self.id)
1013 if change:
showard8cbaf1e2009-09-08 16:27:04 +00001014 # Check the original object for an ACL violation
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001015 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
showard8cbaf1e2009-09-08 16:27:04 +00001016 super(AclGroup, self).save(*args, **kwargs)
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001017 self.perform_after_save(change)
showard8cbaf1e2009-09-08 16:27:04 +00001018
showardeb3be4d2008-04-21 20:59:26 +00001019
jadmanski0afbb632008-06-06 21:10:57 +00001020 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001021 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +00001022 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +00001023
showarda5288b42009-07-28 20:06:08 +00001024 def __unicode__(self):
1025 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +00001026
mblighe8819cd2008-02-15 16:48:40 +00001027
jamesren4a41e012010-07-16 22:33:48 +00001028class Kernel(dbmodels.Model):
1029 """
1030 A kernel configuration for a parameterized job
1031 """
1032 version = dbmodels.CharField(max_length=255)
1033 cmdline = dbmodels.CharField(max_length=255, blank=True)
1034
1035 @classmethod
1036 def create_kernels(cls, kernel_list):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001037 """Creates all kernels in the kernel list.
jamesren4a41e012010-07-16 22:33:48 +00001038
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001039 @param cls: Implicit class object.
1040 @param kernel_list: A list of dictionaries that describe the kernels,
1041 in the same format as the 'kernel' argument to
1042 rpc_interface.generate_control_file.
1043 @return A list of the created kernels.
jamesren4a41e012010-07-16 22:33:48 +00001044 """
1045 if not kernel_list:
1046 return None
1047 return [cls._create(kernel) for kernel in kernel_list]
1048
1049
1050 @classmethod
1051 def _create(cls, kernel_dict):
1052 version = kernel_dict.pop('version')
1053 cmdline = kernel_dict.pop('cmdline', '')
1054
1055 if kernel_dict:
1056 raise Exception('Extraneous kernel arguments remain: %r'
1057 % kernel_dict)
1058
1059 kernel, _ = cls.objects.get_or_create(version=version,
1060 cmdline=cmdline)
1061 return kernel
1062
1063
1064 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001065 """Metadata for class Kernel."""
jamesren4a41e012010-07-16 22:33:48 +00001066 db_table = 'afe_kernels'
1067 unique_together = ('version', 'cmdline')
1068
1069 def __unicode__(self):
1070 return u'%s %s' % (self.version, self.cmdline)
1071
1072
1073class ParameterizedJob(dbmodels.Model):
1074 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001075 Auxiliary configuration for a parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001076 """
1077 test = dbmodels.ForeignKey(Test)
1078 label = dbmodels.ForeignKey(Label, null=True)
1079 use_container = dbmodels.BooleanField(default=False)
1080 profile_only = dbmodels.BooleanField(default=False)
1081 upload_kernel_config = dbmodels.BooleanField(default=False)
1082
1083 kernels = dbmodels.ManyToManyField(
1084 Kernel, db_table='afe_parameterized_job_kernels')
1085 profilers = dbmodels.ManyToManyField(
1086 Profiler, through='ParameterizedJobProfiler')
1087
1088
1089 @classmethod
1090 def smart_get(cls, id_or_name, *args, **kwargs):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001091 """For compatibility with Job.add_object.
1092
1093 @param cls: Implicit class object.
1094 @param id_or_name: The ID or name to get.
1095 @param args: Non-keyword arguments.
1096 @param kwargs: Keyword arguments.
1097 """
jamesren4a41e012010-07-16 22:33:48 +00001098 return cls.objects.get(pk=id_or_name)
1099
1100
1101 def job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001102 """Returns the job if it exists, or else None."""
jamesren4a41e012010-07-16 22:33:48 +00001103 jobs = self.job_set.all()
1104 assert jobs.count() <= 1
1105 return jobs and jobs[0] or None
1106
1107
1108 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001109 """Metadata for class ParameterizedJob."""
jamesren4a41e012010-07-16 22:33:48 +00001110 db_table = 'afe_parameterized_jobs'
1111
1112 def __unicode__(self):
1113 return u'%s (parameterized) - %s' % (self.test.name, self.job())
1114
1115
1116class ParameterizedJobProfiler(dbmodels.Model):
1117 """
1118 A profiler to run on a parameterized job
1119 """
1120 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1121 profiler = dbmodels.ForeignKey(Profiler)
1122
1123 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001124 """Metedata for class ParameterizedJobProfiler."""
jamesren4a41e012010-07-16 22:33:48 +00001125 db_table = 'afe_parameterized_jobs_profilers'
1126 unique_together = ('parameterized_job', 'profiler')
1127
1128
1129class ParameterizedJobProfilerParameter(dbmodels.Model):
1130 """
1131 A parameter for a profiler in a parameterized job
1132 """
1133 parameterized_job_profiler = dbmodels.ForeignKey(ParameterizedJobProfiler)
1134 parameter_name = dbmodels.CharField(max_length=255)
1135 parameter_value = dbmodels.TextField()
1136 parameter_type = dbmodels.CharField(
1137 max_length=8, choices=model_attributes.ParameterTypes.choices())
1138
1139 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001140 """Metadata for class ParameterizedJobProfilerParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001141 db_table = 'afe_parameterized_job_profiler_parameters'
1142 unique_together = ('parameterized_job_profiler', 'parameter_name')
1143
1144 def __unicode__(self):
1145 return u'%s - %s' % (self.parameterized_job_profiler.profiler.name,
1146 self.parameter_name)
1147
1148
1149class ParameterizedJobParameter(dbmodels.Model):
1150 """
1151 Parameters for a parameterized job
1152 """
1153 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1154 test_parameter = dbmodels.ForeignKey(TestParameter)
1155 parameter_value = dbmodels.TextField()
1156 parameter_type = dbmodels.CharField(
1157 max_length=8, choices=model_attributes.ParameterTypes.choices())
1158
1159 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001160 """Metadata for class ParameterizedJobParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001161 db_table = 'afe_parameterized_job_parameters'
1162 unique_together = ('parameterized_job', 'test_parameter')
1163
1164 def __unicode__(self):
1165 return u'%s - %s' % (self.parameterized_job.job().name,
1166 self.test_parameter.name)
1167
1168
showard7c785282008-05-29 19:45:12 +00001169class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +00001170 'Custom manager to provide efficient status counts querying.'
1171 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001172 """Returns a dict mapping the given job IDs to their status count dicts.
1173
1174 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +00001175 """
1176 if not job_ids:
1177 return {}
1178 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1179 cursor = connection.cursor()
1180 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +00001181 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +00001182 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +00001183 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +00001184 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +00001185 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +00001186 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +00001187 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +00001188 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +00001189 full_status = HostQueueEntry.compute_full_status(status, aborted,
1190 complete)
showardb6d16622009-05-26 19:35:29 +00001191 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +00001192 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +00001193 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +00001194
1195
showard7c785282008-05-29 19:45:12 +00001196class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001197 """\
1198 owner: username of job owner
1199 name: job name (does not have to be unique)
Alex Miller7d658cf2013-09-04 16:00:35 -07001200 priority: Integer priority value. Higher is more important.
jadmanski0afbb632008-06-06 21:10:57 +00001201 control_file: contents of control file
1202 control_type: Client or Server
1203 created_on: date of job creation
1204 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +00001205 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +00001206 run_verify: Whether or not to run the verify phase
Dan Shi07e09af2013-04-12 09:31:29 -07001207 run_reset: Whether or not to run the reset phase
Simran Basi94dc0032013-11-12 14:09:46 -08001208 timeout: DEPRECATED - hours from queuing time until job times out
1209 timeout_mins: minutes from job queuing time until the job times out
Simran Basi34217022012-11-06 13:43:15 -08001210 max_runtime_hrs: DEPRECATED - hours from job starting time until job
1211 times out
1212 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +00001213 email_list: list of people to email on completion delimited by any of:
1214 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +00001215 dependency_labels: many-to-many relationship with labels corresponding to
1216 job dependencies
showard21baa452008-10-21 00:08:39 +00001217 reboot_before: Never, If dirty, or Always
1218 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001219 parse_failed_repair: if True, a failed repair launched by this job will have
1220 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001221 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001222 parent_job: Parent job (optional)
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001223 test_retry: Number of times to retry test if the test did not complete
1224 successfully. (optional, default: 0)
Dan Shic9e17142015-02-19 11:50:55 -08001225 require_ssp: Require server-side packaging unless require_ssp is set to
1226 False. (optional, default: None)
jadmanski0afbb632008-06-06 21:10:57 +00001227 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001228
1229 # TODO: Investigate, if jobkeyval_set is really needed.
1230 # dynamic_suite will write them into an attached file for the drone, but
1231 # it doesn't seem like they are actually used. If they aren't used, remove
1232 # jobkeyval_set here.
1233 SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1234 'hostqueueentry_set',
1235 'jobkeyval_set',
1236 'shard'])
1237
1238
Jakob Juelichf88fa932014-09-03 17:58:04 -07001239 def _deserialize_relation(self, link, data):
1240 if link in ['hostqueueentry_set', 'jobkeyval_set']:
1241 for obj in data:
1242 obj['job_id'] = self.id
1243
1244 super(Job, self)._deserialize_relation(link, data)
1245
1246
1247 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001248 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -07001249 self.shard = Shard.deserialize(data)
1250
1251
Jakob Juelicha94efe62014-09-18 16:02:49 -07001252 def sanity_check_update_from_shard(self, shard, updated_serialized):
Jakob Juelich02e61292014-10-17 12:36:55 -07001253 # If the job got aborted on the master after the client fetched it
1254 # no shard_id will be set. The shard might still push updates though,
1255 # as the job might complete before the abort bit syncs to the shard.
1256 # Alternative considered: The master scheduler could be changed to not
1257 # set aborted jobs to completed that are sharded out. But that would
1258 # require database queries and seemed more complicated to implement.
1259 # This seems safe to do, as there won't be updates pushed from the wrong
1260 # shards should be powered off and wiped hen they are removed from the
1261 # master.
1262 if self.shard_id and self.shard_id != shard.id:
Jakob Juelicha94efe62014-09-18 16:02:49 -07001263 raise error.UnallowedRecordsSentToMaster(
1264 'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1265 'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1266 shard.id))
1267
1268
Simran Basi94dc0032013-11-12 14:09:46 -08001269 # TIMEOUT is deprecated.
showardb1e51872008-10-07 11:08:18 +00001270 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
Simran Basi94dc0032013-11-12 14:09:46 -08001271 'AUTOTEST_WEB', 'job_timeout_default', default=24)
1272 DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1273 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
Simran Basi34217022012-11-06 13:43:15 -08001274 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1275 # completed.
showard12f3e322009-05-13 21:27:42 +00001276 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1277 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001278 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1279 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001280 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1281 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1282 default=False)
showardb1e51872008-10-07 11:08:18 +00001283
showarda5288b42009-07-28 20:06:08 +00001284 owner = dbmodels.CharField(max_length=255)
1285 name = dbmodels.CharField(max_length=255)
Alex Miller7d658cf2013-09-04 16:00:35 -07001286 priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
jamesren4a41e012010-07-16 22:33:48 +00001287 control_file = dbmodels.TextField(null=True, blank=True)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001288 control_type = dbmodels.SmallIntegerField(
1289 choices=control_data.CONTROL_TYPE.choices(),
1290 blank=True, # to allow 0
1291 default=control_data.CONTROL_TYPE.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001292 created_on = dbmodels.DateTimeField()
Simran Basi8d89b642014-05-02 17:33:20 -07001293 synch_count = dbmodels.IntegerField(blank=True, default=0)
showardb1e51872008-10-07 11:08:18 +00001294 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
Dan Shi07e09af2013-04-12 09:31:29 -07001295 run_verify = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001296 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001297 dependency_labels = (
1298 dbmodels.ManyToManyField(Label, blank=True,
1299 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001300 reboot_before = dbmodels.SmallIntegerField(
1301 choices=model_attributes.RebootBefore.choices(), blank=True,
1302 default=DEFAULT_REBOOT_BEFORE)
1303 reboot_after = dbmodels.SmallIntegerField(
1304 choices=model_attributes.RebootAfter.choices(), blank=True,
1305 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001306 parse_failed_repair = dbmodels.BooleanField(
1307 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001308 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1309 # completed.
showard12f3e322009-05-13 21:27:42 +00001310 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001311 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001312 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001313
jamesren4a41e012010-07-16 22:33:48 +00001314 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1315 blank=True)
1316
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001317 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001318
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001319 test_retry = dbmodels.IntegerField(blank=True, default=0)
1320
Dan Shi07e09af2013-04-12 09:31:29 -07001321 run_reset = dbmodels.BooleanField(default=True)
1322
Simran Basi94dc0032013-11-12 14:09:46 -08001323 timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1324
Jakob Juelich8421d592014-09-17 15:27:06 -07001325 # If this is None on the master, a slave should be found.
1326 # If this is None on a slave, it should be synced back to the master
Jakob Jülich92c06332014-08-25 19:06:57 +00001327 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1328
Dan Shic9e17142015-02-19 11:50:55 -08001329 # If this is None, server-side packaging will be used for server side test,
1330 # unless it's disabled in global config AUTOSERV/enable_ssp_container.
1331 require_ssp = dbmodels.NullBooleanField(default=None, blank=True, null=True)
1332
jadmanski0afbb632008-06-06 21:10:57 +00001333 # custom manager
1334 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001335
1336
Alex Millerec212252014-02-28 16:48:34 -08001337 @decorators.cached_property
1338 def labels(self):
1339 """All the labels of this job"""
1340 # We need to convert dependency_labels to a list, because all() gives us
1341 # back an iterator, and storing/caching an iterator means we'd only be
1342 # able to read from it once.
1343 return list(self.dependency_labels.all())
1344
1345
jadmanski0afbb632008-06-06 21:10:57 +00001346 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001347 """Returns whether this job is of type server."""
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001348 return self.control_type == control_data.CONTROL_TYPE.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001349
1350
jadmanski0afbb632008-06-06 21:10:57 +00001351 @classmethod
jamesren4a41e012010-07-16 22:33:48 +00001352 def parameterized_jobs_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001353 """Returns whether parameterized jobs are enabled.
1354
1355 @param cls: Implicit class object.
1356 """
jamesren4a41e012010-07-16 22:33:48 +00001357 return global_config.global_config.get_config_value(
1358 'AUTOTEST_WEB', 'parameterized_jobs', type=bool)
1359
1360
1361 @classmethod
1362 def check_parameterized_job(cls, control_file, parameterized_job):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001363 """Checks that the job is valid given the global config settings.
jamesren4a41e012010-07-16 22:33:48 +00001364
1365 First, either control_file must be set, or parameterized_job must be
1366 set, but not both. Second, parameterized_job must be set if and only if
1367 the parameterized_jobs option in the global config is set to True.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001368
1369 @param cls: Implict class object.
1370 @param control_file: A control file.
1371 @param parameterized_job: A parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001372 """
1373 if not (bool(control_file) ^ bool(parameterized_job)):
1374 raise Exception('Job must have either control file or '
1375 'parameterization, but not both')
1376
1377 parameterized_jobs_enabled = cls.parameterized_jobs_enabled()
1378 if control_file and parameterized_jobs_enabled:
1379 raise Exception('Control file specified, but parameterized jobs '
1380 'are enabled')
1381 if parameterized_job and not parameterized_jobs_enabled:
1382 raise Exception('Parameterized job specified, but parameterized '
1383 'jobs are not enabled')
1384
1385
1386 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001387 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001388 """Creates a job.
1389
1390 The job is created by taking some information (the listed args) and
1391 filling in the rest of the necessary information.
1392
1393 @param cls: Implicit class object.
1394 @param owner: The owner for the job.
1395 @param options: An options object.
1396 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001397 """
showard3dd47c22008-07-10 00:41:36 +00001398 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001399
jamesren4a41e012010-07-16 22:33:48 +00001400 control_file = options.get('control_file')
1401 parameterized_job = options.get('parameterized_job')
jamesren4a41e012010-07-16 22:33:48 +00001402
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001403 # The current implementation of parameterized jobs requires that only
1404 # control files or parameterized jobs are used. Using the image
1405 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1406 # parameterized jobs and control files jobs, it does muck enough with
1407 # normal jobs by adding a parameterized id to them that this check will
1408 # fail. So for now we just skip this check.
1409 # cls.check_parameterized_job(control_file=control_file,
1410 # parameterized_job=parameterized_job)
showard4d233752010-01-20 19:06:40 +00001411 user = User.current_user()
1412 if options.get('reboot_before') is None:
1413 options['reboot_before'] = user.get_reboot_before_display()
1414 if options.get('reboot_after') is None:
1415 options['reboot_after'] = user.get_reboot_after_display()
1416
jamesren76fcf192010-04-21 20:39:50 +00001417 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1418
Simran Basi94dc0032013-11-12 14:09:46 -08001419 if options.get('timeout_mins') is None and options.get('timeout'):
1420 options['timeout_mins'] = options['timeout'] * 60
1421
jadmanski0afbb632008-06-06 21:10:57 +00001422 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001423 owner=owner,
1424 name=options['name'],
1425 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001426 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001427 control_type=options['control_type'],
1428 synch_count=options.get('synch_count'),
Simran Basi94dc0032013-11-12 14:09:46 -08001429 # timeout needs to be deleted in the future.
showarda1e74b32009-05-12 17:32:04 +00001430 timeout=options.get('timeout'),
Simran Basi94dc0032013-11-12 14:09:46 -08001431 timeout_mins=options.get('timeout_mins'),
Simran Basi34217022012-11-06 13:43:15 -08001432 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001433 run_verify=options.get('run_verify'),
1434 email_list=options.get('email_list'),
1435 reboot_before=options.get('reboot_before'),
1436 reboot_after=options.get('reboot_after'),
1437 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001438 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001439 drone_set=drone_set,
Aviv Keshet18308922013-02-19 17:49:49 -08001440 parameterized_job=parameterized_job,
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001441 parent_job=options.get('parent_job_id'),
Dan Shi07e09af2013-04-12 09:31:29 -07001442 test_retry=options.get('test_retry'),
Dan Shic9e17142015-02-19 11:50:55 -08001443 run_reset=options.get('run_reset'),
1444 require_ssp=options.get('require_ssp'))
mblighe8819cd2008-02-15 16:48:40 +00001445
showarda1e74b32009-05-12 17:32:04 +00001446 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001447
jamesrend8b6e172010-04-16 23:45:00 +00001448 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001449 for key, value in options['keyvals'].iteritems():
1450 JobKeyval.objects.create(job=job, key=key, value=value)
1451
jadmanski0afbb632008-06-06 21:10:57 +00001452 return job
mblighe8819cd2008-02-15 16:48:40 +00001453
1454
Jakob Juelich59cfe542014-09-02 16:37:46 -07001455 @classmethod
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001456 def _add_filters_for_shard_assignment(cls, query, known_ids):
1457 """Exclude jobs that should be not sent to shard.
1458
1459 This is a helper that filters out the following jobs:
1460 - Non-aborted jobs known to shard as specified in |known_ids|.
1461 Note for jobs aborted on master, even if already known to shard,
1462 will be sent to shard again so that shard can abort them.
1463 - Completed jobs
1464 - Active jobs
1465 @param query: A query that finds jobs for shards, to which the 'exclude'
1466 filters will be applied.
1467 @param known_ids: List of all ids of incomplete jobs, the shard already
1468 knows about.
1469
1470 @returns: A django QuerySet after filtering out unnecessary jobs.
1471
1472 """
1473 return query.exclude(
1474 id__in=known_ids,
1475 hostqueueentry__aborted=False
1476 ).exclude(
1477 hostqueueentry__complete=True
1478 ).exclude(
1479 hostqueueentry__active=True)
1480
1481
1482 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -07001483 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -07001484 """Assigns unassigned jobs to a shard.
1485
Jakob Juelich1b525742014-09-30 13:08:07 -07001486 For all labels that have been assigned to this shard, all jobs that
1487 have this label, are assigned to this shard.
1488
1489 Jobs that are assigned to the shard but aren't already present on the
1490 shard are returned.
1491
Jakob Juelich59cfe542014-09-02 16:37:46 -07001492 @param shard: The shard to assign jobs to.
Jakob Juelich1b525742014-09-30 13:08:07 -07001493 @param known_ids: List of all ids of incomplete jobs, the shard already
1494 knows about.
1495 This is used to figure out which jobs should be sent
1496 to the shard. If shard_ids were used instead, jobs
1497 would only be transferred once, even if the client
1498 failed persisting them.
1499 The number of unfinished jobs usually lies in O(1000).
1500 Assuming one id takes 8 chars in the json, this means
1501 overhead that lies in the lower kilobyte range.
1502 A not in query with 5000 id's takes about 30ms.
1503
Jakob Juelich59cfe542014-09-02 16:37:46 -07001504 @returns The job objects that should be sent to the shard.
1505 """
1506 # Disclaimer: Concurrent heartbeats should not occur in today's setup.
1507 # If this changes or they are triggered manually, this applies:
1508 # Jobs may be returned more than once by concurrent calls of this
1509 # function, as there is a race condition between SELECT and UPDATE.
MK Ryu06a4b522015-04-24 15:06:10 -07001510 query = Job.objects.filter(
1511 dependency_labels=shard.labels.all(),
1512 # If an HQE associated with a job is removed in some reasons,
1513 # such jobs should be excluded. Refer crbug.com/479766
1514 hostqueueentry__isnull=False
1515 )
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001516 query = cls._add_filters_for_shard_assignment(query, known_ids)
1517 job_ids = set(query.distinct().values_list('pk', flat=True))
1518
1519 # Combine frontend jobs in the heartbeat.
1520 query = Job.objects.filter(
1521 hostqueueentry__meta_host__isnull=True,
1522 hostqueueentry__host__isnull=False,
1523 hostqueueentry__host__labels=shard.labels.all()
1524 )
1525 query = cls._add_filters_for_shard_assignment(query, known_ids)
1526 job_ids |= set(query.distinct().values_list('pk', flat=True))
Jakob Juelich59cfe542014-09-02 16:37:46 -07001527 if job_ids:
1528 Job.objects.filter(pk__in=job_ids).update(shard=shard)
1529 return list(Job.objects.filter(pk__in=job_ids).all())
1530 return []
1531
1532
jamesren4a41e012010-07-16 22:33:48 +00001533 def save(self, *args, **kwargs):
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001534 # The current implementation of parameterized jobs requires that only
1535 # control files or parameterized jobs are used. Using the image
1536 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1537 # parameterized jobs and control files jobs, it does muck enough with
1538 # normal jobs by adding a parameterized id to them that this check will
1539 # fail. So for now we just skip this check.
1540 # cls.check_parameterized_job(control_file=self.control_file,
1541 # parameterized_job=self.parameterized_job)
jamesren4a41e012010-07-16 22:33:48 +00001542 super(Job, self).save(*args, **kwargs)
1543
1544
showard29f7cd22009-04-29 21:16:24 +00001545 def queue(self, hosts, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001546 """Enqueue a job on the given hosts.
1547
1548 @param hosts: The hosts to use.
1549 @param atomic_group: The associated atomic group.
1550 @param is_template: Whether the status should be "Template".
1551 """
showarda9545c02009-12-18 22:44:26 +00001552 if not hosts:
1553 if atomic_group:
1554 # No hosts or labels are required to queue an atomic group
1555 # Job. However, if they are given, we respect them below.
1556 atomic_group.enqueue_job(self, is_template=is_template)
1557 else:
1558 # hostless job
1559 entry = HostQueueEntry.create(job=self, is_template=is_template)
1560 entry.save()
1561 return
1562
jadmanski0afbb632008-06-06 21:10:57 +00001563 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +00001564 host.enqueue_job(self, atomic_group=atomic_group,
1565 is_template=is_template)
1566
1567
1568 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001569 """Creates a recurring job.
1570
1571 @param start_date: The starting date of the job.
1572 @param loop_period: How often to re-run the job, in seconds.
1573 @param loop_count: The re-run count.
1574 @param owner: The owner of the job.
1575 """
showard29f7cd22009-04-29 21:16:24 +00001576 rec = RecurringRun(job=self, start_date=start_date,
1577 loop_period=loop_period,
1578 loop_count=loop_count,
1579 owner=User.objects.get(login=owner))
1580 rec.save()
1581 return rec.id
mblighe8819cd2008-02-15 16:48:40 +00001582
1583
jadmanski0afbb632008-06-06 21:10:57 +00001584 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001585 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001586 try:
1587 return User.objects.get(login=self.owner)
1588 except self.DoesNotExist:
1589 return None
mblighe8819cd2008-02-15 16:48:40 +00001590
1591
showard64a95952010-01-13 21:27:16 +00001592 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001593 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001594 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001595 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001596
1597
showardd1195652009-12-08 22:21:02 +00001598 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001599 """Returns a string tag for this job."""
showardd1195652009-12-08 22:21:02 +00001600 return '%s-%s' % (self.id, self.owner)
1601
1602
showardc1a98d12010-01-15 00:22:22 +00001603 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001604 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001605 return dict((keyval.key, keyval.value)
1606 for keyval in self.jobkeyval_set.all())
1607
1608
Fang Dengff361592015-02-02 15:27:34 -08001609 @classmethod
1610 def get_attribute_model(cls):
1611 """Return the attribute model.
1612
1613 Override method in parent class. This class is called when
1614 deserializing the one-to-many relationship betwen Job and JobKeyval.
1615 On deserialization, we will try to clear any existing job keyvals
1616 associated with a job to avoid any inconsistency.
1617 Though Job doesn't implement ModelWithAttribute, we still treat
1618 it as an attribute model for this purpose.
1619
1620 @returns: The attribute model of Job.
1621 """
1622 return JobKeyval
1623
1624
jadmanski0afbb632008-06-06 21:10:57 +00001625 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001626 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001627 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001628
showarda5288b42009-07-28 20:06:08 +00001629 def __unicode__(self):
1630 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001631
1632
showardc1a98d12010-01-15 00:22:22 +00001633class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1634 """Keyvals associated with jobs"""
Fang Dengff361592015-02-02 15:27:34 -08001635
1636 SERIALIZATION_LINKS_TO_KEEP = set(['job'])
1637 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
1638
showardc1a98d12010-01-15 00:22:22 +00001639 job = dbmodels.ForeignKey(Job)
1640 key = dbmodels.CharField(max_length=90)
1641 value = dbmodels.CharField(max_length=300)
1642
1643 objects = model_logic.ExtendedManager()
1644
Fang Dengff361592015-02-02 15:27:34 -08001645
1646 @classmethod
1647 def get_record(cls, data):
1648 """Check the database for an identical record.
1649
1650 Use job_id and key to search for a existing record.
1651
1652 @raises: DoesNotExist, if no record found
1653 @raises: MultipleObjectsReturned if multiple records found.
1654 """
1655 # TODO(fdeng): We should use job_id and key together as
1656 # a primary key in the db.
1657 return cls.objects.get(job_id=data['job_id'], key=data['key'])
1658
1659
1660 @classmethod
1661 def deserialize(cls, data):
1662 """Override deserialize in parent class.
1663
1664 Do not deserialize id as id is not kept consistent on master and shards.
1665
1666 @param data: A dictionary of data to deserialize.
1667
1668 @returns: A JobKeyval object.
1669 """
1670 if data:
1671 data.pop('id')
1672 return super(JobKeyval, cls).deserialize(data)
1673
1674
showardc1a98d12010-01-15 00:22:22 +00001675 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001676 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001677 db_table = 'afe_job_keyvals'
1678
1679
showard7c785282008-05-29 19:45:12 +00001680class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001681 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001682 job = dbmodels.ForeignKey(Job)
1683 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001684
jadmanski0afbb632008-06-06 21:10:57 +00001685 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001686
jadmanski0afbb632008-06-06 21:10:57 +00001687 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001688 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001689 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001690
mblighe8819cd2008-02-15 16:48:40 +00001691
showard7c785282008-05-29 19:45:12 +00001692class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001693 """Represents a host queue entry."""
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001694
1695 SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001696 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Jakob Juelichf865d332014-09-29 10:47:49 -07001697 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001698
Jakob Juelichf88fa932014-09-03 17:58:04 -07001699
1700 def custom_deserialize_relation(self, link, data):
1701 assert link == 'meta_host'
1702 self.meta_host = Label.deserialize(data)
1703
1704
Jakob Juelicha94efe62014-09-18 16:02:49 -07001705 def sanity_check_update_from_shard(self, shard, updated_serialized,
1706 job_ids_sent):
1707 if self.job_id not in job_ids_sent:
1708 raise error.UnallowedRecordsSentToMaster(
1709 'Sent HostQueueEntry without corresponding '
1710 'job entry: %s' % updated_serialized)
1711
1712
showardeaa408e2009-09-11 18:45:31 +00001713 Status = host_queue_entry_states.Status
1714 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001715 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001716
jadmanski0afbb632008-06-06 21:10:57 +00001717 job = dbmodels.ForeignKey(Job)
1718 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001719 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001720 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1721 db_column='meta_host')
1722 active = dbmodels.BooleanField(default=False)
1723 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001724 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001725 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1726 default='')
showard89f84db2009-03-12 20:39:13 +00001727 # If atomic_group is set, this is a virtual HostQueueEntry that will
1728 # be expanded into many actual hosts within the group at schedule time.
1729 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001730 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001731 started_on = dbmodels.DateTimeField(null=True, blank=True)
Fang Deng51599032014-06-23 17:24:27 -07001732 finished_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001733
jadmanski0afbb632008-06-06 21:10:57 +00001734 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001735
mblighe8819cd2008-02-15 16:48:40 +00001736
showard2bab8f42008-11-12 18:15:22 +00001737 def __init__(self, *args, **kwargs):
1738 super(HostQueueEntry, self).__init__(*args, **kwargs)
1739 self._record_attributes(['status'])
1740
1741
showard29f7cd22009-04-29 21:16:24 +00001742 @classmethod
1743 def create(cls, job, host=None, meta_host=None, atomic_group=None,
1744 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001745 """Creates a new host queue entry.
1746
1747 @param cls: Implicit class object.
1748 @param job: The associated job.
1749 @param host: The associated host.
1750 @param meta_host: The associated meta host.
1751 @param atomic_group: The associated atomic group.
1752 @param is_template: Whether the status should be "Template".
1753 """
showard29f7cd22009-04-29 21:16:24 +00001754 if is_template:
1755 status = cls.Status.TEMPLATE
1756 else:
1757 status = cls.Status.QUEUED
1758
1759 return cls(job=job, host=host, meta_host=meta_host,
1760 atomic_group=atomic_group, status=status)
1761
1762
showarda5288b42009-07-28 20:06:08 +00001763 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001764 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001765 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001766 self._check_for_updated_attributes()
1767
1768
showardc0ac3a72009-07-08 21:14:45 +00001769 def execution_path(self):
1770 """
1771 Path to this entry's results (relative to the base results directory).
1772 """
showardd1195652009-12-08 22:21:02 +00001773 return os.path.join(self.job.tag(), self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001774
1775
showard3f15eed2008-11-14 22:40:48 +00001776 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001777 """Returns the first non-None name found in priority order.
1778
1779 The priority order checked is: (1) host name; (2) meta host name; and
1780 (3) atomic group name.
1781 """
showard3f15eed2008-11-14 22:40:48 +00001782 if self.host:
1783 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001784 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +00001785 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +00001786 else:
1787 assert self.atomic_group, "no host, meta_host or atomic group!"
1788 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +00001789
1790
showard2bab8f42008-11-12 18:15:22 +00001791 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001792 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001793 self.active, self.complete = True, False
1794 elif self.status in self.COMPLETE_STATUSES:
1795 self.active, self.complete = False, True
1796 else:
1797 self.active, self.complete = False, False
1798
1799
1800 def on_attribute_changed(self, attribute, old_value):
1801 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001802 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1803 self.status)
showard2bab8f42008-11-12 18:15:22 +00001804
1805
jadmanski0afbb632008-06-06 21:10:57 +00001806 def is_meta_host_entry(self):
1807 'True if this is a entry has a meta_host instead of a host.'
1808 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001809
showarda3ab0d52008-11-03 19:03:47 +00001810
Simran Basic1b26762013-06-26 14:23:21 -07001811 # This code is shared between rpc_interface and models.HostQueueEntry.
1812 # Sadly due to circular imports between the 2 (crbug.com/230100) making it
1813 # a class method was the best way to refactor it. Attempting to put it in
1814 # rpc_utils or a new utils module failed as that would require us to import
1815 # models.py but to call it from here we would have to import the utils.py
1816 # thus creating a cycle.
1817 @classmethod
1818 def abort_host_queue_entries(cls, host_queue_entries):
1819 """Aborts a collection of host_queue_entries.
1820
1821 Abort these host queue entry and all host queue entries of jobs created
1822 by them.
1823
1824 @param host_queue_entries: List of host queue entries we want to abort.
1825 """
1826 # This isn't completely immune to race conditions since it's not atomic,
1827 # but it should be safe given the scheduler's behavior.
1828
1829 # TODO(milleral): crbug.com/230100
1830 # The |abort_host_queue_entries| rpc does nearly exactly this,
1831 # however, trying to re-use the code generates some horrible
1832 # circular import error. I'd be nice to refactor things around
1833 # sometime so the code could be reused.
1834
1835 # Fixpoint algorithm to find the whole tree of HQEs to abort to
1836 # minimize the total number of database queries:
1837 children = set()
1838 new_children = set(host_queue_entries)
1839 while new_children:
1840 children.update(new_children)
1841 new_child_ids = [hqe.job_id for hqe in new_children]
1842 new_children = HostQueueEntry.objects.filter(
1843 job__parent_job__in=new_child_ids,
1844 complete=False, aborted=False).all()
1845 # To handle circular parental relationships
1846 new_children = set(new_children) - children
1847
1848 # Associate a user with the host queue entries that we're about
1849 # to abort so that we can look up who to blame for the aborts.
1850 now = datetime.now()
1851 user = User.current_user()
1852 aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
1853 aborted_by=user, aborted_on=now) for hqe in children]
1854 AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
1855 # Bulk update all of the HQEs to set the abort bit.
1856 child_ids = [hqe.id for hqe in children]
1857 HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
1858
1859
Scott Zawalski23041432013-04-17 07:39:09 -07001860 def abort(self):
Alex Millerdea67042013-04-22 17:23:34 -07001861 """ Aborts this host queue entry.
Simran Basic1b26762013-06-26 14:23:21 -07001862
Alex Millerdea67042013-04-22 17:23:34 -07001863 Abort this host queue entry and all host queue entries of jobs created by
1864 this one.
1865
1866 """
showardd3dc1992009-04-22 21:01:40 +00001867 if not self.complete and not self.aborted:
Simran Basi97582a22013-06-27 12:03:21 -07001868 HostQueueEntry.abort_host_queue_entries([self])
mblighe8819cd2008-02-15 16:48:40 +00001869
showardd3dc1992009-04-22 21:01:40 +00001870
1871 @classmethod
1872 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001873 """Returns a modified status msg if the host queue entry was aborted.
1874
1875 @param cls: Implicit class object.
1876 @param status: The original status message.
1877 @param aborted: Whether the host queue entry was aborted.
1878 @param complete: Whether the host queue entry was completed.
1879 """
showardd3dc1992009-04-22 21:01:40 +00001880 if aborted and not complete:
1881 return 'Aborted (%s)' % status
1882 return status
1883
1884
1885 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001886 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00001887 return self.compute_full_status(self.status, self.aborted,
1888 self.complete)
1889
1890
1891 def _postprocess_object_dict(self, object_dict):
1892 object_dict['full_status'] = self.full_status()
1893
1894
jadmanski0afbb632008-06-06 21:10:57 +00001895 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001896 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001897 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00001898
showard12f3e322009-05-13 21:27:42 +00001899
showard4c119042008-09-29 19:16:18 +00001900
showarda5288b42009-07-28 20:06:08 +00001901 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00001902 hostname = None
1903 if self.host:
1904 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00001905 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00001906
1907
showard4c119042008-09-29 19:16:18 +00001908class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001909 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00001910 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
1911 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00001912 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00001913
1914 objects = model_logic.ExtendedManager()
1915
showard68c7aa02008-10-09 16:49:11 +00001916
showarda5288b42009-07-28 20:06:08 +00001917 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00001918 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00001919 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00001920
showard4c119042008-09-29 19:16:18 +00001921 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001922 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001923 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00001924
1925
1926class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
1927 """\
1928 job: job to use as a template
1929 owner: owner of the instantiated template
1930 start_date: Run the job at scheduled date
1931 loop_period: Re-run (loop) the job periodically
1932 (in every loop_period seconds)
1933 loop_count: Re-run (loop) count
1934 """
1935
1936 job = dbmodels.ForeignKey(Job)
1937 owner = dbmodels.ForeignKey(User)
1938 start_date = dbmodels.DateTimeField()
1939 loop_period = dbmodels.IntegerField(blank=True)
1940 loop_count = dbmodels.IntegerField(blank=True)
1941
1942 objects = model_logic.ExtendedManager()
1943
1944 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001945 """Metadata for class RecurringRun."""
showardeab66ce2009-12-23 00:03:56 +00001946 db_table = 'afe_recurring_run'
showard29f7cd22009-04-29 21:16:24 +00001947
showarda5288b42009-07-28 20:06:08 +00001948 def __unicode__(self):
1949 return u'RecurringRun(job %s, start %s, period %s, count %s)' % (
showard29f7cd22009-04-29 21:16:24 +00001950 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +00001951
1952
1953class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
1954 """\
1955 Tasks to run on hosts at the next time they are in the Ready state. Use this
1956 for high-priority tasks, such as forced repair or forced reinstall.
1957
1958 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00001959 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00001960 time_requested: date and time the request for this task was made
1961 is_active: task is currently running
1962 is_complete: task has finished running
beeps8bb1f7d2013-08-05 01:30:09 -07001963 is_aborted: task was aborted
showard2fe3f1d2009-07-06 20:19:11 +00001964 time_started: date and time the task started
Dan Shid0725542014-06-23 15:34:27 -07001965 time_finished: date and time the task finished
showard2fe3f1d2009-07-06 20:19:11 +00001966 queue_entry: Host queue entry waiting on this task (or None, if task was not
1967 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00001968 """
Alex Millerdfff2fd2013-05-28 13:05:06 -07001969 Task = enum.Enum('Verify', 'Cleanup', 'Repair', 'Reset', 'Provision',
Dan Shi07e09af2013-04-12 09:31:29 -07001970 string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00001971
1972 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00001973 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00001974 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00001975 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00001976 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
1977 null=False)
1978 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
1979 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
beeps8bb1f7d2013-08-05 01:30:09 -07001980 is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00001981 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00001982 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00001983 success = dbmodels.BooleanField(default=False, blank=False, null=False)
Dan Shid0725542014-06-23 15:34:27 -07001984 time_finished = dbmodels.DateTimeField(null=True, blank=True)
showard6d7b2ff2009-06-10 00:16:47 +00001985
1986 objects = model_logic.ExtendedManager()
1987
1988
showard9bb960b2009-11-19 01:02:11 +00001989 def save(self, **kwargs):
1990 if self.queue_entry:
1991 self.requested_by = User.objects.get(
1992 login=self.queue_entry.job.owner)
1993 super(SpecialTask, self).save(**kwargs)
1994
1995
showarded2afea2009-07-07 20:54:07 +00001996 def execution_path(self):
Prashanth Balasubramaniande87dea2014-11-09 17:47:10 -08001997 """Get the execution path of the SpecialTask.
1998
1999 This method returns different paths depending on where a
2000 the task ran:
2001 * Master: hosts/hostname/task_id-task_type
2002 * Shard: Master_path/time_created
2003 This is to work around the fact that a shard can fail independent
2004 of the master, and be replaced by another shard that has the same
2005 hosts. Without the time_created stamp the logs of the tasks running
2006 on the second shard will clobber the logs from the first in google
2007 storage, because task ids are not globally unique.
2008
2009 @return: An execution path for the task.
2010 """
2011 results_path = 'hosts/%s/%s-%s' % (self.host.hostname, self.id,
2012 self.task.lower())
2013
2014 # If we do this on the master it will break backward compatibility,
2015 # as there are tasks that currently don't have timestamps. If a host
2016 # or job has been sent to a shard, the rpc for that host/job will
2017 # be redirected to the shard, so this global_config check will happen
2018 # on the shard the logs are on.
2019 is_shard = global_config.global_config.get_config_value(
2020 'SHARD', 'shard_hostname', type=str, default='')
2021 if not is_shard:
2022 return results_path
2023
2024 # Generate a uid to disambiguate special task result directories
2025 # in case this shard fails. The simplest uid is the job_id, however
2026 # in rare cases tasks do not have jobs associated with them (eg:
2027 # frontend verify), so just use the creation timestamp. The clocks
2028 # between a shard and master should always be in sync. Any discrepancies
2029 # will be brought to our attention in the form of job timeouts.
2030 uid = self.time_requested.strftime('%Y%d%m%H%M%S')
2031
2032 # TODO: This is a hack, however it is the easiest way to achieve
2033 # correctness. There is currently some debate over the future of
2034 # tasks in our infrastructure and refactoring everything right
2035 # now isn't worth the time.
2036 return '%s/%s' % (results_path, uid)
showarded2afea2009-07-07 20:54:07 +00002037
2038
showardc0ac3a72009-07-08 21:14:45 +00002039 # property to emulate HostQueueEntry.status
2040 @property
2041 def status(self):
2042 """
2043 Return a host queue entry status appropriate for this task. Although
2044 SpecialTasks are not HostQueueEntries, it is helpful to the user to
2045 present similar statuses.
2046 """
2047 if self.is_complete:
showarde60e44e2009-11-13 20:45:38 +00002048 if self.success:
2049 return HostQueueEntry.Status.COMPLETED
2050 return HostQueueEntry.Status.FAILED
showardc0ac3a72009-07-08 21:14:45 +00002051 if self.is_active:
2052 return HostQueueEntry.Status.RUNNING
2053 return HostQueueEntry.Status.QUEUED
2054
2055
2056 # property to emulate HostQueueEntry.started_on
2057 @property
2058 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002059 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00002060 return self.time_started
2061
2062
showard6d7b2ff2009-06-10 00:16:47 +00002063 @classmethod
showardc5103442010-01-15 00:20:26 +00002064 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002065 """Schedules a special task on a host if not already scheduled.
2066
2067 @param cls: Implicit class object.
2068 @param host: The host to use.
2069 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00002070 """
showardc5103442010-01-15 00:20:26 +00002071 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
2072 is_active=False,
2073 is_complete=False)
2074 if existing_tasks:
2075 return existing_tasks[0]
2076
2077 special_task = SpecialTask(host=host, task=task,
2078 requested_by=User.current_user())
2079 special_task.save()
2080 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00002081
2082
beeps8bb1f7d2013-08-05 01:30:09 -07002083 def abort(self):
2084 """ Abort this special task."""
2085 self.is_aborted = True
2086 self.save()
2087
2088
showarded2afea2009-07-07 20:54:07 +00002089 def activate(self):
showard474d1362009-08-20 23:32:01 +00002090 """
2091 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00002092 """
showard97446882009-07-20 22:37:28 +00002093 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00002094 self.is_active = True
2095 self.time_started = datetime.now()
2096 self.save()
2097
2098
showarde60e44e2009-11-13 20:45:38 +00002099 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002100 """Sets a task as completed.
2101
2102 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00002103 """
showard97446882009-07-20 22:37:28 +00002104 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00002105 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00002106 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00002107 self.success = success
Dan Shid85d6112014-07-14 10:32:55 -07002108 if self.time_started:
2109 self.time_finished = datetime.now()
showard2fe3f1d2009-07-06 20:19:11 +00002110 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00002111
2112
2113 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002114 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00002115 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00002116
showard474d1362009-08-20 23:32:01 +00002117
showarda5288b42009-07-28 20:06:08 +00002118 def __unicode__(self):
2119 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00002120 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00002121 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00002122 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00002123 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00002124 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00002125
2126 return result
Dan Shi6964fa52014-12-18 11:04:27 -08002127
2128
2129class StableVersion(dbmodels.Model, model_logic.ModelExtensions):
2130
2131 board = dbmodels.CharField(max_length=255, unique=True)
2132 version = dbmodels.CharField(max_length=255)
2133
2134 class Meta:
2135 """Metadata for class StableVersion."""
Fang Deng86248502014-12-18 16:38:00 -08002136 db_table = 'afe_stable_versions'