blob: 8be942a6ddc48f35c8e1ffe5c540db91088fd253 [file] [log] [blame]
Shuqian Zhao54a5b672016-05-11 22:12:17 +00001# pylint: disable-msg=C0111
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08002
Fang Deng2b79fd72015-05-20 19:11:36 -07003import logging
showardfb2a7fa2008-07-17 17:04:12 +00004from datetime import datetime
Aviv Keshetfa199002013-05-09 13:31:46 -07005import django.core
6try:
7 from django.db import models as dbmodels, connection
8except django.core.exceptions.ImproperlyConfigured:
9 raise ImportError('Django database not yet configured. Import either '
10 'setup_django_environment or '
11 'setup_django_lite_environment from '
12 'autotest_lib.frontend before any imports that '
13 'depend on django models.')
jamesren35a70222010-02-16 19:30:46 +000014from xml.sax import saxutils
showardcafd16e2009-05-29 18:37:49 +000015import common
jamesrendd855242010-03-02 22:23:44 +000016from autotest_lib.frontend.afe import model_logic, model_attributes
Prashanth B489b91d2014-03-15 12:17:16 -070017from autotest_lib.frontend.afe import rdb_model_extensions
showardcafd16e2009-05-29 18:37:49 +000018from autotest_lib.frontend import settings, thread_local
Jakob Juelicha94efe62014-09-18 16:02:49 -070019from autotest_lib.client.common_lib import enum, error, host_protections
20from autotest_lib.client.common_lib import global_config
showardeaa408e2009-09-11 18:45:31 +000021from autotest_lib.client.common_lib import host_queue_entry_states
Jakob Juelicha94efe62014-09-18 16:02:49 -070022from autotest_lib.client.common_lib import control_data, priorities, decorators
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -080023from autotest_lib.client.common_lib import site_utils
Gabe Blackb72f4fb2015-01-20 16:47:13 -080024from autotest_lib.client.common_lib.cros.graphite import autotest_es
MK Ryu0c1a37d2015-04-30 12:00:55 -070025from autotest_lib.server import utils as server_utils
mblighe8819cd2008-02-15 16:48:40 +000026
showard0fc38302008-10-23 00:44:07 +000027# job options and user preferences
jamesrendd855242010-03-02 22:23:44 +000028DEFAULT_REBOOT_BEFORE = model_attributes.RebootBefore.IF_DIRTY
Dan Shi07e09af2013-04-12 09:31:29 -070029DEFAULT_REBOOT_AFTER = model_attributes.RebootBefore.NEVER
mblighe8819cd2008-02-15 16:48:40 +000030
showard89f84db2009-03-12 20:39:13 +000031
mblighe8819cd2008-02-15 16:48:40 +000032class AclAccessViolation(Exception):
jadmanski0afbb632008-06-06 21:10:57 +000033 """\
34 Raised when an operation is attempted with proper permissions as
35 dictated by ACLs.
36 """
mblighe8819cd2008-02-15 16:48:40 +000037
38
showard205fd602009-03-21 00:17:35 +000039class AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model):
showard89f84db2009-03-12 20:39:13 +000040 """\
41 An atomic group defines a collection of hosts which must only be scheduled
42 all at once. Any host with a label having an atomic group will only be
43 scheduled for a job at the same time as other hosts sharing that label.
44
45 Required:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080046 name: A name for this atomic group, e.g. 'rack23' or 'funky_net'.
showard89f84db2009-03-12 20:39:13 +000047 max_number_of_machines: The maximum number of machines that will be
48 scheduled at once when scheduling jobs to this atomic group.
49 The job.synch_count is considered the minimum.
50
51 Optional:
52 description: Arbitrary text description of this group's purpose.
53 """
showarda5288b42009-07-28 20:06:08 +000054 name = dbmodels.CharField(max_length=255, unique=True)
showard89f84db2009-03-12 20:39:13 +000055 description = dbmodels.TextField(blank=True)
showarde9450c92009-06-30 01:58:52 +000056 # This magic value is the default to simplify the scheduler logic.
57 # It must be "large". The common use of atomic groups is to want all
58 # machines in the group to be used, limits on which subset used are
59 # often chosen via dependency labels.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080060 # TODO(dennisjeffrey): Revisit this so we don't have to assume that
61 # "infinity" is around 3.3 million.
showarde9450c92009-06-30 01:58:52 +000062 INFINITE_MACHINES = 333333333
63 max_number_of_machines = dbmodels.IntegerField(default=INFINITE_MACHINES)
showard205fd602009-03-21 00:17:35 +000064 invalid = dbmodels.BooleanField(default=False,
showarda5288b42009-07-28 20:06:08 +000065 editable=settings.FULL_ADMIN)
showard89f84db2009-03-12 20:39:13 +000066
showard89f84db2009-03-12 20:39:13 +000067 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +000068 objects = model_logic.ModelWithInvalidManager()
showard205fd602009-03-21 00:17:35 +000069 valid_objects = model_logic.ValidObjectsManager()
70
71
showard29f7cd22009-04-29 21:16:24 +000072 def enqueue_job(self, job, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080073 """Enqueue a job on an associated atomic group of hosts.
74
75 @param job: A job to enqueue.
76 @param is_template: Whether the status should be "Template".
77 """
showard29f7cd22009-04-29 21:16:24 +000078 queue_entry = HostQueueEntry.create(atomic_group=self, job=job,
79 is_template=is_template)
showardc92da832009-04-07 18:14:34 +000080 queue_entry.save()
81
82
showard205fd602009-03-21 00:17:35 +000083 def clean_object(self):
84 self.label_set.clear()
showard89f84db2009-03-12 20:39:13 +000085
86
87 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080088 """Metadata for class AtomicGroup."""
showardeab66ce2009-12-23 00:03:56 +000089 db_table = 'afe_atomic_groups'
showard89f84db2009-03-12 20:39:13 +000090
showard205fd602009-03-21 00:17:35 +000091
showarda5288b42009-07-28 20:06:08 +000092 def __unicode__(self):
93 return unicode(self.name)
showard89f84db2009-03-12 20:39:13 +000094
95
showard7c785282008-05-29 19:45:12 +000096class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000097 """\
98 Required:
showard89f84db2009-03-12 20:39:13 +000099 name: label name
mblighe8819cd2008-02-15 16:48:40 +0000100
jadmanski0afbb632008-06-06 21:10:57 +0000101 Optional:
showard89f84db2009-03-12 20:39:13 +0000102 kernel_config: URL/path to kernel config for jobs run on this label.
103 platform: If True, this is a platform label (defaults to False).
104 only_if_needed: If True, a Host with this label can only be used if that
105 label is requested by the job/test (either as the meta_host or
106 in the job_dependencies).
107 atomic_group: The atomic group associated with this label.
jadmanski0afbb632008-06-06 21:10:57 +0000108 """
showarda5288b42009-07-28 20:06:08 +0000109 name = dbmodels.CharField(max_length=255, unique=True)
110 kernel_config = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000111 platform = dbmodels.BooleanField(default=False)
112 invalid = dbmodels.BooleanField(default=False,
113 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +0000114 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +0000115
jadmanski0afbb632008-06-06 21:10:57 +0000116 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +0000117 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000118 valid_objects = model_logic.ValidObjectsManager()
showard89f84db2009-03-12 20:39:13 +0000119 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
120
mbligh5244cbb2008-04-24 20:39:52 +0000121
jadmanski0afbb632008-06-06 21:10:57 +0000122 def clean_object(self):
123 self.host_set.clear()
showard01a51672009-05-29 18:42:37 +0000124 self.test_set.clear()
mblighe8819cd2008-02-15 16:48:40 +0000125
126
showard29f7cd22009-04-29 21:16:24 +0000127 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800128 """Enqueue a job on any host of this label.
129
130 @param job: A job to enqueue.
131 @param atomic_group: The associated atomic group.
132 @param is_template: Whether the status should be "Template".
133 """
showard29f7cd22009-04-29 21:16:24 +0000134 queue_entry = HostQueueEntry.create(meta_host=self, job=job,
135 is_template=is_template,
136 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000137 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000138
139
Fang Dengff361592015-02-02 15:27:34 -0800140
jadmanski0afbb632008-06-06 21:10:57 +0000141 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800142 """Metadata for class Label."""
showardeab66ce2009-12-23 00:03:56 +0000143 db_table = 'afe_labels'
mblighe8819cd2008-02-15 16:48:40 +0000144
Fang Dengff361592015-02-02 15:27:34 -0800145
showarda5288b42009-07-28 20:06:08 +0000146 def __unicode__(self):
147 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000148
149
Jakob Jülich92c06332014-08-25 19:06:57 +0000150class Shard(dbmodels.Model, model_logic.ModelExtensions):
151
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700152 hostname = dbmodels.CharField(max_length=255, unique=True)
153
154 name_field = 'hostname'
155
Jakob Jülich92c06332014-08-25 19:06:57 +0000156 labels = dbmodels.ManyToManyField(Label, blank=True,
157 db_table='afe_shards_labels')
158
159 class Meta:
160 """Metadata for class ParameterizedJob."""
161 db_table = 'afe_shards'
162
163
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -0800164 def rpc_hostname(self):
165 """Get the rpc hostname of the shard.
166
167 @return: Just the shard hostname for all non-testing environments.
168 The address of the default gateway for vm testing environments.
169 """
170 # TODO: Figure out a better solution for testing. Since no 2 shards
171 # can run on the same host, if the shard hostname is localhost we
172 # conclude that it must be a vm in a test cluster. In such situations
173 # a name of localhost:<port> is necessary to achieve the correct
174 # afe links/redirection from the frontend (this happens through the
175 # host), but for rpcs that are performed *on* the shard, they need to
176 # use the address of the gateway.
MK Ryu8f8cdb42015-05-11 17:41:14 -0700177 # In the virtual machine testing environment (i.e., puppylab), each
178 # shard VM has a hostname like localhost:<port>. In the real cluster
179 # environment, a shard node does not have 'localhost' for its hostname.
180 # The following hostname substitution is needed only for the VM
181 # in puppylab.
182 # The 'hostname' should not be replaced in the case of real cluster.
183 if site_utils.is_puppylab_vm(self.hostname):
184 hostname = self.hostname.split(':')[0]
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -0800185 return self.hostname.replace(
186 hostname, site_utils.DEFAULT_VM_GATEWAY)
187 return self.hostname
188
189
jamesren76fcf192010-04-21 20:39:50 +0000190class Drone(dbmodels.Model, model_logic.ModelExtensions):
191 """
192 A scheduler drone
193
194 hostname: the drone's hostname
195 """
196 hostname = dbmodels.CharField(max_length=255, unique=True)
197
198 name_field = 'hostname'
199 objects = model_logic.ExtendedManager()
200
201
202 def save(self, *args, **kwargs):
203 if not User.current_user().is_superuser():
204 raise Exception('Only superusers may edit drones')
205 super(Drone, self).save(*args, **kwargs)
206
207
208 def delete(self):
209 if not User.current_user().is_superuser():
210 raise Exception('Only superusers may delete drones')
211 super(Drone, self).delete()
212
213
214 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800215 """Metadata for class Drone."""
jamesren76fcf192010-04-21 20:39:50 +0000216 db_table = 'afe_drones'
217
218 def __unicode__(self):
219 return unicode(self.hostname)
220
221
222class DroneSet(dbmodels.Model, model_logic.ModelExtensions):
223 """
224 A set of scheduler drones
225
226 These will be used by the scheduler to decide what drones a job is allowed
227 to run on.
228
229 name: the drone set's name
230 drones: the drones that are part of the set
231 """
232 DRONE_SETS_ENABLED = global_config.global_config.get_config_value(
233 'SCHEDULER', 'drone_sets_enabled', type=bool, default=False)
234 DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value(
235 'SCHEDULER', 'default_drone_set_name', default=None)
236
237 name = dbmodels.CharField(max_length=255, unique=True)
238 drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones')
239
240 name_field = 'name'
241 objects = model_logic.ExtendedManager()
242
243
244 def save(self, *args, **kwargs):
245 if not User.current_user().is_superuser():
246 raise Exception('Only superusers may edit drone sets')
247 super(DroneSet, self).save(*args, **kwargs)
248
249
250 def delete(self):
251 if not User.current_user().is_superuser():
252 raise Exception('Only superusers may delete drone sets')
253 super(DroneSet, self).delete()
254
255
256 @classmethod
257 def drone_sets_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800258 """Returns whether drone sets are enabled.
259
260 @param cls: Implicit class object.
261 """
jamesren76fcf192010-04-21 20:39:50 +0000262 return cls.DRONE_SETS_ENABLED
263
264
265 @classmethod
266 def default_drone_set_name(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800267 """Returns the default drone set name.
268
269 @param cls: Implicit class object.
270 """
jamesren76fcf192010-04-21 20:39:50 +0000271 return cls.DEFAULT_DRONE_SET_NAME
272
273
274 @classmethod
275 def get_default(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800276 """Gets the default drone set name, compatible with Job.add_object.
277
278 @param cls: Implicit class object.
279 """
jamesren76fcf192010-04-21 20:39:50 +0000280 return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME)
281
282
283 @classmethod
284 def resolve_name(cls, drone_set_name):
285 """
286 Returns the name of one of these, if not None, in order of preference:
287 1) the drone set given,
288 2) the current user's default drone set, or
289 3) the global default drone set
290
291 or returns None if drone sets are disabled
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800292
293 @param cls: Implicit class object.
294 @param drone_set_name: A drone set name.
jamesren76fcf192010-04-21 20:39:50 +0000295 """
296 if not cls.drone_sets_enabled():
297 return None
298
299 user = User.current_user()
300 user_drone_set_name = user.drone_set and user.drone_set.name
301
302 return drone_set_name or user_drone_set_name or cls.get_default().name
303
304
305 def get_drone_hostnames(self):
306 """
307 Gets the hostnames of all drones in this drone set
308 """
309 return set(self.drones.all().values_list('hostname', flat=True))
310
311
312 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800313 """Metadata for class DroneSet."""
jamesren76fcf192010-04-21 20:39:50 +0000314 db_table = 'afe_drone_sets'
315
316 def __unicode__(self):
317 return unicode(self.name)
318
319
showardfb2a7fa2008-07-17 17:04:12 +0000320class User(dbmodels.Model, model_logic.ModelExtensions):
321 """\
322 Required:
323 login :user login name
324
325 Optional:
326 access_level: 0=User (default), 1=Admin, 100=Root
327 """
328 ACCESS_ROOT = 100
329 ACCESS_ADMIN = 1
330 ACCESS_USER = 0
331
showard64a95952010-01-13 21:27:16 +0000332 AUTOTEST_SYSTEM = 'autotest_system'
333
showarda5288b42009-07-28 20:06:08 +0000334 login = dbmodels.CharField(max_length=255, unique=True)
showardfb2a7fa2008-07-17 17:04:12 +0000335 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
336
showard0fc38302008-10-23 00:44:07 +0000337 # user preferences
jamesrendd855242010-03-02 22:23:44 +0000338 reboot_before = dbmodels.SmallIntegerField(
339 choices=model_attributes.RebootBefore.choices(), blank=True,
340 default=DEFAULT_REBOOT_BEFORE)
341 reboot_after = dbmodels.SmallIntegerField(
342 choices=model_attributes.RebootAfter.choices(), blank=True,
343 default=DEFAULT_REBOOT_AFTER)
jamesren76fcf192010-04-21 20:39:50 +0000344 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
showard97db5ba2008-11-12 18:18:02 +0000345 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000346
showardfb2a7fa2008-07-17 17:04:12 +0000347 name_field = 'login'
348 objects = model_logic.ExtendedManager()
349
350
showarda5288b42009-07-28 20:06:08 +0000351 def save(self, *args, **kwargs):
showardfb2a7fa2008-07-17 17:04:12 +0000352 # is this a new object being saved for the first time?
353 first_time = (self.id is None)
354 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000355 if user and not user.is_superuser() and user.login != self.login:
356 raise AclAccessViolation("You cannot modify user " + self.login)
showarda5288b42009-07-28 20:06:08 +0000357 super(User, self).save(*args, **kwargs)
showardfb2a7fa2008-07-17 17:04:12 +0000358 if first_time:
359 everyone = AclGroup.objects.get(name='Everyone')
360 everyone.users.add(self)
361
362
363 def is_superuser(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800364 """Returns whether the user has superuser access."""
showardfb2a7fa2008-07-17 17:04:12 +0000365 return self.access_level >= self.ACCESS_ROOT
366
367
showard64a95952010-01-13 21:27:16 +0000368 @classmethod
369 def current_user(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800370 """Returns the current user.
371
372 @param cls: Implicit class object.
373 """
showard64a95952010-01-13 21:27:16 +0000374 user = thread_local.get_user()
375 if user is None:
showardcfcdd802010-01-15 00:16:33 +0000376 user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM)
showard64a95952010-01-13 21:27:16 +0000377 user.access_level = cls.ACCESS_ROOT
378 user.save()
379 return user
380
381
Prashanth Balasubramanianaf516642014-12-12 18:16:32 -0800382 @classmethod
383 def get_record(cls, data):
384 """Check the database for an identical record.
385
386 Check for a record with matching id and login. If one exists,
387 return it. If one does not exist there is a possibility that
388 the following cases have happened:
389 1. Same id, different login
390 We received: "1 chromeos-test"
391 And we have: "1 debug-user"
392 In this case we need to delete "1 debug_user" and insert
393 "1 chromeos-test".
394
395 2. Same login, different id:
396 We received: "1 chromeos-test"
397 And we have: "2 chromeos-test"
398 In this case we need to delete "2 chromeos-test" and insert
399 "1 chromeos-test".
400
401 As long as this method deletes bad records and raises the
402 DoesNotExist exception the caller will handle creating the
403 new record.
404
405 @raises: DoesNotExist, if a record with the matching login and id
406 does not exist.
407 """
408
409 # Both the id and login should be uniqe but there are cases when
410 # we might already have a user with the same login/id because
411 # current_user will proactively create a user record if it doesn't
412 # exist. Since we want to avoid conflict between the master and
413 # shard, just delete any existing user records that don't match
414 # what we're about to deserialize from the master.
415 try:
416 return cls.objects.get(login=data['login'], id=data['id'])
417 except cls.DoesNotExist:
418 cls.delete_matching_record(login=data['login'])
419 cls.delete_matching_record(id=data['id'])
420 raise
421
422
showardfb2a7fa2008-07-17 17:04:12 +0000423 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800424 """Metadata for class User."""
showardeab66ce2009-12-23 00:03:56 +0000425 db_table = 'afe_users'
showardfb2a7fa2008-07-17 17:04:12 +0000426
showarda5288b42009-07-28 20:06:08 +0000427 def __unicode__(self):
428 return unicode(self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000429
430
Prashanth B489b91d2014-03-15 12:17:16 -0700431class Host(model_logic.ModelWithInvalid, rdb_model_extensions.AbstractHostModel,
showardf8b19042009-05-12 17:22:49 +0000432 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000433 """\
434 Required:
435 hostname
mblighe8819cd2008-02-15 16:48:40 +0000436
jadmanski0afbb632008-06-06 21:10:57 +0000437 optional:
showard21baa452008-10-21 00:08:39 +0000438 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000439
jadmanski0afbb632008-06-06 21:10:57 +0000440 Internal:
Prashanth B489b91d2014-03-15 12:17:16 -0700441 From AbstractHostModel:
442 synch_id: currently unused
443 status: string describing status of host
444 invalid: true if the host has been deleted
445 protection: indicates what can be done to this host during repair
446 lock_time: DateTime at which the host was locked
447 dirty: true if the host has been used without being rebooted
448 Local:
449 locked_by: user that locked the host, or null if the host is unlocked
jadmanski0afbb632008-06-06 21:10:57 +0000450 """
mblighe8819cd2008-02-15 16:48:40 +0000451
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700452 SERIALIZATION_LINKS_TO_FOLLOW = set(['aclgroup_set',
453 'hostattribute_set',
454 'labels',
455 'shard'])
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800456 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['invalid'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700457
Jakob Juelichf88fa932014-09-03 17:58:04 -0700458
459 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700460 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -0700461 self.shard = Shard.deserialize(data)
462
463
Prashanth B489b91d2014-03-15 12:17:16 -0700464 # Note: Only specify foreign keys here, specify all native host columns in
465 # rdb_model_extensions instead.
466 Protection = host_protections.Protection
showardeab66ce2009-12-23 00:03:56 +0000467 labels = dbmodels.ManyToManyField(Label, blank=True,
468 db_table='afe_hosts_labels')
showardfb2a7fa2008-07-17 17:04:12 +0000469 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
jadmanski0afbb632008-06-06 21:10:57 +0000470 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000471 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000472 valid_objects = model_logic.ValidObjectsManager()
beepscc9fc702013-12-02 12:45:38 -0800473 leased_objects = model_logic.LeasedHostManager()
mbligh5244cbb2008-04-24 20:39:52 +0000474
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700475 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
showard2bab8f42008-11-12 18:15:22 +0000476
477 def __init__(self, *args, **kwargs):
478 super(Host, self).__init__(*args, **kwargs)
479 self._record_attributes(['status'])
480
481
showardb8471e32008-07-03 19:51:08 +0000482 @staticmethod
483 def create_one_time_host(hostname):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800484 """Creates a one-time host.
485
486 @param hostname: The name for the host.
487 """
showardb8471e32008-07-03 19:51:08 +0000488 query = Host.objects.filter(hostname=hostname)
489 if query.count() == 0:
490 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000491 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000492 else:
493 host = query[0]
494 if not host.invalid:
495 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000496 'hostname' : '%s already exists in the autotest DB. '
497 'Select it rather than entering it as a one time '
498 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000499 })
showard1ab512b2008-07-30 23:39:04 +0000500 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000501 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000502 host.save()
showard2924b0a2009-06-18 23:16:15 +0000503 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000504 return host
mbligh5244cbb2008-04-24 20:39:52 +0000505
showard1ff7b2e2009-05-15 23:17:18 +0000506
Jakob Juelich59cfe542014-09-02 16:37:46 -0700507 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -0700508 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -0700509 """Assigns hosts to a shard.
510
MK Ryu638a6cf2015-05-08 14:49:43 -0700511 For all labels that have been assigned to a shard, all hosts that
512 have at least one of the shard's labels are assigned to the shard.
Jakob Juelich1b525742014-09-30 13:08:07 -0700513 Hosts that are assigned to the shard but aren't already present on the
514 shard are returned.
Jakob Juelich59cfe542014-09-02 16:37:46 -0700515
MK Ryu638a6cf2015-05-08 14:49:43 -0700516 Board to shard mapping is many-to-one. Many different boards can be
517 hosted in a shard. However, DUTs of a single board cannot be distributed
518 into more than one shard.
519
Jakob Juelich59cfe542014-09-02 16:37:46 -0700520 @param shard: The shard object to assign labels/hosts for.
Jakob Juelich1b525742014-09-30 13:08:07 -0700521 @param known_ids: List of all host-ids the shard already knows.
522 This is used to figure out which hosts should be sent
523 to the shard. If shard_ids were used instead, hosts
524 would only be transferred once, even if the client
525 failed persisting them.
526 The number of hosts usually lies in O(100), so the
527 overhead is acceptable.
528
Jakob Juelich59cfe542014-09-02 16:37:46 -0700529 @returns the hosts objects that should be sent to the shard.
530 """
531
532 # Disclaimer: concurrent heartbeats should theoretically not occur in
533 # the current setup. As they may be introduced in the near future,
534 # this comment will be left here.
535
536 # Sending stuff twice is acceptable, but forgetting something isn't.
537 # Detecting duplicates on the client is easy, but here it's harder. The
538 # following options were considered:
539 # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
540 # than select returned, as concurrently more hosts might have been
541 # inserted
542 # - UPDATE and then SELECT WHERE shard=shard: select always returns all
543 # hosts for the shard, this is overhead
544 # - SELECT and then UPDATE only selected without requerying afterwards:
545 # returns the old state of the records.
MK Ryu638a6cf2015-05-08 14:49:43 -0700546 host_ids = set(Host.objects.filter(
547 labels__in=shard.labels.all(),
Jakob Juelich59cfe542014-09-02 16:37:46 -0700548 leased=False
Jakob Juelich1b525742014-09-30 13:08:07 -0700549 ).exclude(
550 id__in=known_ids,
Jakob Juelich59cfe542014-09-02 16:37:46 -0700551 ).values_list('pk', flat=True))
552
553 if host_ids:
554 Host.objects.filter(pk__in=host_ids).update(shard=shard)
555 return list(Host.objects.filter(pk__in=host_ids).all())
556 return []
557
showardafd97de2009-10-01 18:45:09 +0000558 def resurrect_object(self, old_object):
559 super(Host, self).resurrect_object(old_object)
560 # invalid hosts can be in use by the scheduler (as one-time hosts), so
561 # don't change the status
562 self.status = old_object.status
563
564
jadmanski0afbb632008-06-06 21:10:57 +0000565 def clean_object(self):
566 self.aclgroup_set.clear()
567 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000568
569
Dan Shia0acfbc2014-10-14 15:56:23 -0700570 def record_state(self, type_str, state, value, other_metadata=None):
571 """Record metadata in elasticsearch.
572
573 @param type_str: sets the _type field in elasticsearch db.
574 @param state: string representing what state we are recording,
575 e.g. 'locked'
576 @param value: value of the state, e.g. True
577 @param other_metadata: Other metadata to store in metaDB.
578 """
579 metadata = {
580 state: value,
581 'hostname': self.hostname,
582 }
583 if other_metadata:
584 metadata = dict(metadata.items() + other_metadata.items())
Matthew Sartori68186332015-04-27 17:19:53 -0700585 autotest_es.post(use_http=True, type_str=type_str, metadata=metadata)
Dan Shia0acfbc2014-10-14 15:56:23 -0700586
587
showarda5288b42009-07-28 20:06:08 +0000588 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000589 # extra spaces in the hostname can be a sneaky source of errors
590 self.hostname = self.hostname.strip()
591 # is this a new object being saved for the first time?
592 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000593 if not first_time:
594 AclGroup.check_for_acl_violation_hosts([self])
Dan Shia0acfbc2014-10-14 15:56:23 -0700595 # If locked is changed, send its status and user made the change to
596 # metaDB. Locks are important in host history because if a device is
597 # locked then we don't really care what state it is in.
showardfb2a7fa2008-07-17 17:04:12 +0000598 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000599 self.locked_by = User.current_user()
MK Ryud53e1492015-12-15 12:09:03 -0800600 if not self.lock_time:
601 self.lock_time = datetime.now()
Dan Shia0acfbc2014-10-14 15:56:23 -0700602 self.record_state('lock_history', 'locked', self.locked,
Matthew Sartori68186332015-04-27 17:19:53 -0700603 {'changed_by': self.locked_by.login,
604 'lock_reason': self.lock_reason})
showard21baa452008-10-21 00:08:39 +0000605 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000606 elif not self.locked and self.locked_by:
Dan Shia0acfbc2014-10-14 15:56:23 -0700607 self.record_state('lock_history', 'locked', self.locked,
608 {'changed_by': self.locked_by.login})
showardfb2a7fa2008-07-17 17:04:12 +0000609 self.locked_by = None
610 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000611 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000612 if first_time:
613 everyone = AclGroup.objects.get(name='Everyone')
614 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000615 self._check_for_updated_attributes()
616
mblighe8819cd2008-02-15 16:48:40 +0000617
showardb8471e32008-07-03 19:51:08 +0000618 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000619 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000620 for queue_entry in self.hostqueueentry_set.all():
621 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000622 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000623 super(Host, self).delete()
624
mblighe8819cd2008-02-15 16:48:40 +0000625
showard2bab8f42008-11-12 18:15:22 +0000626 def on_attribute_changed(self, attribute, old_value):
627 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000628 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000629
630
showard29f7cd22009-04-29 21:16:24 +0000631 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800632 """Enqueue a job on this host.
633
634 @param job: A job to enqueue.
635 @param atomic_group: The associated atomic group.
636 @param is_template: Whther the status should be "Template".
637 """
showard29f7cd22009-04-29 21:16:24 +0000638 queue_entry = HostQueueEntry.create(host=self, job=job,
639 is_template=is_template,
640 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000641 # allow recovery of dead hosts from the frontend
642 if not self.active_queue_entry() and self.is_dead():
643 self.status = Host.Status.READY
644 self.save()
645 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000646
showard08f981b2008-06-24 21:59:03 +0000647 block = IneligibleHostQueue(job=job, host=self)
648 block.save()
649
mblighe8819cd2008-02-15 16:48:40 +0000650
jadmanski0afbb632008-06-06 21:10:57 +0000651 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800652 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000653 # TODO(showard): slighly hacky?
654 platforms = self.labels.filter(platform=True)
655 if len(platforms) == 0:
656 return None
657 return platforms[0]
658 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000659
660
showardcafd16e2009-05-29 18:37:49 +0000661 @classmethod
662 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800663 """Verify the specified hosts have no associated platforms.
664
665 @param cls: Implicit class object.
666 @param hosts: The hosts to verify.
667 @raises model_logic.ValidationError if any hosts already have a
668 platform.
669 """
showardcafd16e2009-05-29 18:37:49 +0000670 Host.objects.populate_relationships(hosts, Label, 'label_list')
671 errors = []
672 for host in hosts:
673 platforms = [label.name for label in host.label_list
674 if label.platform]
675 if platforms:
676 # do a join, just in case this host has multiple platforms,
677 # we'll be able to see it
678 errors.append('Host %s already has a platform: %s' % (
679 host.hostname, ', '.join(platforms)))
680 if errors:
681 raise model_logic.ValidationError({'labels': '; '.join(errors)})
682
683
jadmanski0afbb632008-06-06 21:10:57 +0000684 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800685 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000686 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000687
688
jadmanski0afbb632008-06-06 21:10:57 +0000689 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800690 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000691 active = list(self.hostqueueentry_set.filter(active=True))
692 if not active:
693 return None
694 assert len(active) == 1, ('More than one active entry for '
695 'host ' + self.hostname)
696 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000697
698
showardf8b19042009-05-12 17:22:49 +0000699 def _get_attribute_model_and_args(self, attribute):
700 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000701
702
Fang Dengff361592015-02-02 15:27:34 -0800703 @classmethod
704 def get_attribute_model(cls):
705 """Return the attribute model.
706
707 Override method in parent class. See ModelExtensions for details.
708 @returns: The attribute model of Host.
709 """
710 return HostAttribute
711
712
jadmanski0afbb632008-06-06 21:10:57 +0000713 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800714 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000715 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000716
Fang Dengff361592015-02-02 15:27:34 -0800717
showarda5288b42009-07-28 20:06:08 +0000718 def __unicode__(self):
719 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000720
721
MK Ryuacf35922014-10-03 14:56:49 -0700722class HostAttribute(dbmodels.Model, model_logic.ModelExtensions):
showard0957a842009-05-11 19:25:08 +0000723 """Arbitrary keyvals associated with hosts."""
Fang Deng86248502014-12-18 16:38:00 -0800724
725 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Fang Dengff361592015-02-02 15:27:34 -0800726 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
showard0957a842009-05-11 19:25:08 +0000727 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000728 attribute = dbmodels.CharField(max_length=90)
729 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000730
731 objects = model_logic.ExtendedManager()
732
733 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800734 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000735 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000736
737
Fang Dengff361592015-02-02 15:27:34 -0800738 @classmethod
739 def get_record(cls, data):
740 """Check the database for an identical record.
741
742 Use host_id and attribute to search for a existing record.
743
744 @raises: DoesNotExist, if no record found
745 @raises: MultipleObjectsReturned if multiple records found.
746 """
747 # TODO(fdeng): We should use host_id and attribute together as
748 # a primary key in the db.
749 return cls.objects.get(host_id=data['host_id'],
750 attribute=data['attribute'])
751
752
753 @classmethod
754 def deserialize(cls, data):
755 """Override deserialize in parent class.
756
757 Do not deserialize id as id is not kept consistent on master and shards.
758
759 @param data: A dictionary of data to deserialize.
760
761 @returns: A HostAttribute object.
762 """
763 if data:
764 data.pop('id')
765 return super(HostAttribute, cls).deserialize(data)
766
767
showard7c785282008-05-29 19:45:12 +0000768class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000769 """\
770 Required:
showard909c7a62008-07-15 21:52:38 +0000771 author: author name
772 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000773 name: test name
showard909c7a62008-07-15 21:52:38 +0000774 time: short, medium, long
775 test_class: This describes the class for your the test belongs in.
776 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000777 test_type: Client or Server
778 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000779 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
780 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000781 i.e. if sync_count = 2 it is a sync job that requires two
782 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000783 Optional:
showard909c7a62008-07-15 21:52:38 +0000784 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000785 dependency_labels: many-to-many relationship with labels corresponding to
786 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000787 experimental: If this is set to True production servers will ignore the test
788 run_verify: Whether or not the scheduler should run the verify stage
Dan Shi07e09af2013-04-12 09:31:29 -0700789 run_reset: Whether or not the scheduler should run the reset stage
Aviv Keshet9af96d32013-03-05 12:56:24 -0800790 test_retry: Number of times to retry test if the test did not complete
791 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +0000792 """
showard909c7a62008-07-15 21:52:38 +0000793 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000794
showarda5288b42009-07-28 20:06:08 +0000795 name = dbmodels.CharField(max_length=255, unique=True)
796 author = dbmodels.CharField(max_length=255)
797 test_class = dbmodels.CharField(max_length=255)
798 test_category = dbmodels.CharField(max_length=255)
799 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000800 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000801 experimental = dbmodels.BooleanField(default=True)
Dan Shi07e09af2013-04-12 09:31:29 -0700802 run_verify = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +0000803 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
804 default=TestTime.MEDIUM)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700805 test_type = dbmodels.SmallIntegerField(
806 choices=control_data.CONTROL_TYPE.choices())
showard909c7a62008-07-15 21:52:38 +0000807 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000808 path = dbmodels.CharField(max_length=255, unique=True)
Aviv Keshet9af96d32013-03-05 12:56:24 -0800809 test_retry = dbmodels.IntegerField(blank=True, default=0)
Dan Shi07e09af2013-04-12 09:31:29 -0700810 run_reset = dbmodels.BooleanField(default=True)
mblighe8819cd2008-02-15 16:48:40 +0000811
showardeab66ce2009-12-23 00:03:56 +0000812 dependency_labels = (
813 dbmodels.ManyToManyField(Label, blank=True,
814 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000815 name_field = 'name'
816 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000817
818
jamesren35a70222010-02-16 19:30:46 +0000819 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800820 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +0000821 escaped_description = saxutils.escape(self.description)
822 return '<span style="white-space:pre">%s</span>' % escaped_description
823 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000824 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000825
826
jadmanski0afbb632008-06-06 21:10:57 +0000827 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800828 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +0000829 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000830
showarda5288b42009-07-28 20:06:08 +0000831 def __unicode__(self):
832 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000833
834
jamesren4a41e012010-07-16 22:33:48 +0000835class TestParameter(dbmodels.Model):
836 """
837 A declared parameter of a test
838 """
839 test = dbmodels.ForeignKey(Test)
840 name = dbmodels.CharField(max_length=255)
841
842 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800843 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000844 db_table = 'afe_test_parameters'
845 unique_together = ('test', 'name')
846
847 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -0700848 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +0000849
850
showard2b9a88b2008-06-13 20:55:03 +0000851class Profiler(dbmodels.Model, model_logic.ModelExtensions):
852 """\
853 Required:
854 name: profiler name
855 test_type: Client or Server
856
857 Optional:
858 description: arbirary text description
859 """
showarda5288b42009-07-28 20:06:08 +0000860 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000861 description = dbmodels.TextField(blank=True)
862
863 name_field = 'name'
864 objects = model_logic.ExtendedManager()
865
866
867 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800868 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +0000869 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000870
showarda5288b42009-07-28 20:06:08 +0000871 def __unicode__(self):
872 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000873
874
showard7c785282008-05-29 19:45:12 +0000875class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000876 """\
877 Required:
878 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000879
jadmanski0afbb632008-06-06 21:10:57 +0000880 Optional:
881 description: arbitrary description of group
882 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700883
884 SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
885
showarda5288b42009-07-28 20:06:08 +0000886 name = dbmodels.CharField(max_length=255, unique=True)
887 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000888 users = dbmodels.ManyToManyField(User, blank=False,
889 db_table='afe_acl_groups_users')
890 hosts = dbmodels.ManyToManyField(Host, blank=True,
891 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000892
jadmanski0afbb632008-06-06 21:10:57 +0000893 name_field = 'name'
894 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000895
showard08f981b2008-06-24 21:59:03 +0000896 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000897 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800898 """Verify the current user has access to the specified hosts.
899
900 @param hosts: The hosts to verify against.
901 @raises AclAccessViolation if the current user doesn't have access
902 to a host.
903 """
showard64a95952010-01-13 21:27:16 +0000904 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000905 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000906 return
showard3dd47c22008-07-10 00:41:36 +0000907 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000908 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000909 for host in hosts:
910 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800911 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +0000912 no_access = (isinstance(host, Host)
913 and not host.invalid
914 and int(host.id) not in accessible_host_ids)
915 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000916 raise AclAccessViolation("%s does not have access to %s" %
917 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000918
showard9dbdcda2008-10-14 17:34:36 +0000919
920 @staticmethod
showarddc817512008-11-12 18:16:41 +0000921 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800922 """Look for queue entries that aren't abortable by the current user.
923
924 An entry is not abortable if:
925 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +0000926 * the machine isn't ACL-accessible, or
927 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800928
929 @param queue_entries: The queue entries to check.
930 @raises AclAccessViolation if a queue entry is not abortable by the
931 current user.
showarddc817512008-11-12 18:16:41 +0000932 """
showard64a95952010-01-13 21:27:16 +0000933 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000934 if user.is_superuser():
935 return
showarddc817512008-11-12 18:16:41 +0000936 not_owned = queue_entries.exclude(job__owner=user.login)
937 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000938 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
939 # 1.0.
940 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000941 accessible_ids = set(
942 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000943 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000944 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000945 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000946 cannot_abort = [entry for entry in not_owned.select_related()
947 if entry.id not in accessible_ids
948 or entry.id in public_ids]
949 if len(cannot_abort) == 0:
950 return
951 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000952 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000953 for entry in cannot_abort)
954 raise AclAccessViolation('You cannot abort the following job entries: '
955 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000956
957
showard3dd47c22008-07-10 00:41:36 +0000958 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800959 """Verifies the current user has acces to this ACL group.
960
961 @raises AclAccessViolation if the current user doesn't have access to
962 this ACL group.
963 """
showard64a95952010-01-13 21:27:16 +0000964 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000965 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000966 return
967 if self.name == 'Everyone':
968 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000969 if not user in self.users.all():
970 raise AclAccessViolation("You do not have access to %s"
971 % self.name)
972
973 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000974 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800975 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +0000976 everyone = AclGroup.objects.get(name='Everyone')
977
showard3dd47c22008-07-10 00:41:36 +0000978 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000979 # TODO(showard): this is a bit of a hack, since the fact that this query
980 # works is kind of a coincidence of Django internals. This trick
981 # doesn't work in general (on all foreign key relationships). I'll
982 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000983 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000984 everyone.hosts.add(*orphaned_hosts.distinct())
985
986 # find hosts in both Everyone and another ACL group, and remove them
987 # from Everyone
showarda5288b42009-07-28 20:06:08 +0000988 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
989 acled_hosts = set()
990 for host in hosts_in_everyone:
991 # Has an ACL group other than Everyone
992 if host.aclgroup_set.count() > 1:
993 acled_hosts.add(host)
994 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +0000995
996
997 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000998 if (self.name == 'Everyone'):
999 raise AclAccessViolation("You cannot delete 'Everyone'!")
1000 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +00001001 super(AclGroup, self).delete()
1002 self.on_host_membership_change()
1003
1004
showard04f2cd82008-07-25 20:53:31 +00001005 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001006 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +00001007 if not self.users.count():
showard64a95952010-01-13 21:27:16 +00001008 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +00001009
1010
showard8cbaf1e2009-09-08 16:27:04 +00001011 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001012 """Called after a save.
1013
1014 @param change: Whether there was a change.
1015 """
showard8cbaf1e2009-09-08 16:27:04 +00001016 if not change:
showard64a95952010-01-13 21:27:16 +00001017 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +00001018 self.add_current_user_if_empty()
1019 self.on_host_membership_change()
1020
1021
1022 def save(self, *args, **kwargs):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001023 change = bool(self.id)
1024 if change:
showard8cbaf1e2009-09-08 16:27:04 +00001025 # Check the original object for an ACL violation
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001026 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
showard8cbaf1e2009-09-08 16:27:04 +00001027 super(AclGroup, self).save(*args, **kwargs)
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001028 self.perform_after_save(change)
showard8cbaf1e2009-09-08 16:27:04 +00001029
showardeb3be4d2008-04-21 20:59:26 +00001030
jadmanski0afbb632008-06-06 21:10:57 +00001031 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001032 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +00001033 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +00001034
showarda5288b42009-07-28 20:06:08 +00001035 def __unicode__(self):
1036 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +00001037
mblighe8819cd2008-02-15 16:48:40 +00001038
Shuqian Zhao54a5b672016-05-11 22:12:17 +00001039class Kernel(dbmodels.Model):
1040 """
1041 A kernel configuration for a parameterized job
1042 """
1043 version = dbmodels.CharField(max_length=255)
1044 cmdline = dbmodels.CharField(max_length=255, blank=True)
1045
1046 @classmethod
1047 def create_kernels(cls, kernel_list):
1048 """Creates all kernels in the kernel list.
1049
1050 @param cls: Implicit class object.
1051 @param kernel_list: A list of dictionaries that describe the kernels,
1052 in the same format as the 'kernel' argument to
1053 rpc_interface.generate_control_file.
1054 @return A list of the created kernels.
1055 """
1056 if not kernel_list:
1057 return None
1058 return [cls._create(kernel) for kernel in kernel_list]
1059
1060
1061 @classmethod
1062 def _create(cls, kernel_dict):
1063 version = kernel_dict.pop('version')
1064 cmdline = kernel_dict.pop('cmdline', '')
1065
1066 if kernel_dict:
1067 raise Exception('Extraneous kernel arguments remain: %r'
1068 % kernel_dict)
1069
1070 kernel, _ = cls.objects.get_or_create(version=version,
1071 cmdline=cmdline)
1072 return kernel
1073
1074
1075 class Meta:
1076 """Metadata for class Kernel."""
1077 db_table = 'afe_kernels'
1078 unique_together = ('version', 'cmdline')
1079
1080 def __unicode__(self):
1081 return u'%s %s' % (self.version, self.cmdline)
1082
1083
jamesren4a41e012010-07-16 22:33:48 +00001084class ParameterizedJob(dbmodels.Model):
1085 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001086 Auxiliary configuration for a parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001087 """
1088 test = dbmodels.ForeignKey(Test)
1089 label = dbmodels.ForeignKey(Label, null=True)
1090 use_container = dbmodels.BooleanField(default=False)
1091 profile_only = dbmodels.BooleanField(default=False)
1092 upload_kernel_config = dbmodels.BooleanField(default=False)
1093
Shuqian Zhao54a5b672016-05-11 22:12:17 +00001094 kernels = dbmodels.ManyToManyField(
1095 Kernel, db_table='afe_parameterized_job_kernels')
jamesren4a41e012010-07-16 22:33:48 +00001096 profilers = dbmodels.ManyToManyField(
1097 Profiler, through='ParameterizedJobProfiler')
1098
1099
1100 @classmethod
1101 def smart_get(cls, id_or_name, *args, **kwargs):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001102 """For compatibility with Job.add_object.
1103
1104 @param cls: Implicit class object.
1105 @param id_or_name: The ID or name to get.
1106 @param args: Non-keyword arguments.
1107 @param kwargs: Keyword arguments.
1108 """
jamesren4a41e012010-07-16 22:33:48 +00001109 return cls.objects.get(pk=id_or_name)
1110
1111
1112 def job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001113 """Returns the job if it exists, or else None."""
jamesren4a41e012010-07-16 22:33:48 +00001114 jobs = self.job_set.all()
1115 assert jobs.count() <= 1
1116 return jobs and jobs[0] or None
1117
1118
1119 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001120 """Metadata for class ParameterizedJob."""
jamesren4a41e012010-07-16 22:33:48 +00001121 db_table = 'afe_parameterized_jobs'
1122
1123 def __unicode__(self):
1124 return u'%s (parameterized) - %s' % (self.test.name, self.job())
1125
1126
1127class ParameterizedJobProfiler(dbmodels.Model):
1128 """
1129 A profiler to run on a parameterized job
1130 """
1131 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1132 profiler = dbmodels.ForeignKey(Profiler)
1133
1134 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001135 """Metedata for class ParameterizedJobProfiler."""
jamesren4a41e012010-07-16 22:33:48 +00001136 db_table = 'afe_parameterized_jobs_profilers'
1137 unique_together = ('parameterized_job', 'profiler')
1138
1139
Shuqian Zhao54a5b672016-05-11 22:12:17 +00001140class ParameterizedJobProfilerParameter(dbmodels.Model):
1141 """
1142 A parameter for a profiler in a parameterized job
1143 """
1144 parameterized_job_profiler = dbmodels.ForeignKey(ParameterizedJobProfiler)
1145 parameter_name = dbmodels.CharField(max_length=255)
1146 parameter_value = dbmodels.TextField()
1147 parameter_type = dbmodels.CharField(
1148 max_length=8, choices=model_attributes.ParameterTypes.choices())
1149
1150 class Meta:
1151 """Metadata for class ParameterizedJobProfilerParameter."""
1152 db_table = 'afe_parameterized_job_profiler_parameters'
1153 unique_together = ('parameterized_job_profiler', 'parameter_name')
1154
1155 def __unicode__(self):
1156 return u'%s - %s' % (self.parameterized_job_profiler.profiler.name,
1157 self.parameter_name)
1158
1159
1160class ParameterizedJobParameter(dbmodels.Model):
1161 """
1162 Parameters for a parameterized job
1163 """
1164 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1165 test_parameter = dbmodels.ForeignKey(TestParameter)
1166 parameter_value = dbmodels.TextField()
1167 parameter_type = dbmodels.CharField(
1168 max_length=8, choices=model_attributes.ParameterTypes.choices())
1169
1170 class Meta:
1171 """Metadata for class ParameterizedJobParameter."""
1172 db_table = 'afe_parameterized_job_parameters'
1173 unique_together = ('parameterized_job', 'test_parameter')
1174
1175 def __unicode__(self):
1176 return u'%s - %s' % (self.parameterized_job.job().name,
1177 self.test_parameter.name)
1178
1179
showard7c785282008-05-29 19:45:12 +00001180class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +00001181 'Custom manager to provide efficient status counts querying.'
1182 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001183 """Returns a dict mapping the given job IDs to their status count dicts.
1184
1185 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +00001186 """
1187 if not job_ids:
1188 return {}
1189 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1190 cursor = connection.cursor()
1191 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +00001192 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +00001193 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +00001194 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +00001195 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +00001196 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +00001197 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +00001198 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +00001199 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +00001200 full_status = HostQueueEntry.compute_full_status(status, aborted,
1201 complete)
showardb6d16622009-05-26 19:35:29 +00001202 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +00001203 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +00001204 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +00001205
1206
showard7c785282008-05-29 19:45:12 +00001207class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001208 """\
1209 owner: username of job owner
1210 name: job name (does not have to be unique)
Alex Miller7d658cf2013-09-04 16:00:35 -07001211 priority: Integer priority value. Higher is more important.
jadmanski0afbb632008-06-06 21:10:57 +00001212 control_file: contents of control file
1213 control_type: Client or Server
1214 created_on: date of job creation
1215 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +00001216 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +00001217 run_verify: Whether or not to run the verify phase
Dan Shi07e09af2013-04-12 09:31:29 -07001218 run_reset: Whether or not to run the reset phase
Simran Basi94dc0032013-11-12 14:09:46 -08001219 timeout: DEPRECATED - hours from queuing time until job times out
1220 timeout_mins: minutes from job queuing time until the job times out
Simran Basi34217022012-11-06 13:43:15 -08001221 max_runtime_hrs: DEPRECATED - hours from job starting time until job
1222 times out
1223 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +00001224 email_list: list of people to email on completion delimited by any of:
1225 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +00001226 dependency_labels: many-to-many relationship with labels corresponding to
1227 job dependencies
showard21baa452008-10-21 00:08:39 +00001228 reboot_before: Never, If dirty, or Always
1229 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001230 parse_failed_repair: if True, a failed repair launched by this job will have
1231 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001232 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001233 parent_job: Parent job (optional)
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001234 test_retry: Number of times to retry test if the test did not complete
1235 successfully. (optional, default: 0)
Dan Shic9e17142015-02-19 11:50:55 -08001236 require_ssp: Require server-side packaging unless require_ssp is set to
1237 False. (optional, default: None)
jadmanski0afbb632008-06-06 21:10:57 +00001238 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001239
1240 # TODO: Investigate, if jobkeyval_set is really needed.
1241 # dynamic_suite will write them into an attached file for the drone, but
1242 # it doesn't seem like they are actually used. If they aren't used, remove
1243 # jobkeyval_set here.
1244 SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1245 'hostqueueentry_set',
1246 'jobkeyval_set',
1247 'shard'])
1248
Fang Deng2b79fd72015-05-20 19:11:36 -07001249 # SQL for selecting jobs that should be sent to shard.
1250 # We use raw sql as django filters were not optimized.
1251 # The following jobs are excluded by the SQL.
1252 # - Non-aborted jobs known to shard as specified in |known_ids|.
1253 # Note for jobs aborted on master, even if already known to shard,
1254 # will be sent to shard again so that shard can abort them.
1255 # - Completed jobs
1256 # - Active jobs
1257 # - Jobs without host_queue_entries
1258 NON_ABORTED_KNOWN_JOBS = '(t2.aborted = 0 AND t1.id IN (%(known_ids)s))'
1259
1260 SQL_SHARD_JOBS = (
MK Ryu5c7ef302015-07-10 17:23:08 -07001261 'SELECT DISTINCT(t1.id) FROM afe_jobs t1 '
Fang Deng2b79fd72015-05-20 19:11:36 -07001262 'INNER JOIN afe_host_queue_entries t2 ON '
MK Ryu5c7ef302015-07-10 17:23:08 -07001263 ' (t1.id = t2.job_id AND t2.complete != 1 AND t2.active != 1 '
Fang Deng2b79fd72015-05-20 19:11:36 -07001264 ' %(check_known_jobs)s) '
MK Ryu5c7ef302015-07-10 17:23:08 -07001265 'LEFT OUTER JOIN afe_jobs_dependency_labels t3 ON (t1.id = t3.job_id) '
Dan Shi6b89f1c2016-09-17 22:25:53 -07001266 'JOIN afe_shards_labels t4 '
1267 ' ON (t4.label_id = t3.label_id OR t4.label_id = t2.meta_host) '
1268 'WHERE t4.shard_id = %(shard_id)s'
Fang Deng2b79fd72015-05-20 19:11:36 -07001269 )
1270
MK Ryu5c7ef302015-07-10 17:23:08 -07001271 # Jobs can be created with assigned hosts and have no dependency
1272 # labels nor meta_host.
Fang Deng2b79fd72015-05-20 19:11:36 -07001273 # We are looking for:
MK Ryu5c7ef302015-07-10 17:23:08 -07001274 # - a job whose hqe's meta_host is null
Fang Deng2b79fd72015-05-20 19:11:36 -07001275 # - a job whose hqe has a host
1276 # - one of the host's labels matches the shard's label.
1277 # Non-aborted known jobs, completed jobs, active jobs, jobs
MK Ryu5c7ef302015-07-10 17:23:08 -07001278 # without hqe are exluded as we do with SQL_SHARD_JOBS.
1279 SQL_SHARD_JOBS_WITH_HOSTS = (
1280 'SELECT DISTINCT(t1.id) FROM afe_jobs t1 '
Fang Deng2b79fd72015-05-20 19:11:36 -07001281 'INNER JOIN afe_host_queue_entries t2 ON '
1282 ' (t1.id = t2.job_id AND t2.complete != 1 AND t2.active != 1 '
1283 ' AND t2.meta_host IS NULL AND t2.host_id IS NOT NULL '
1284 ' %(check_known_jobs)s) '
1285 'LEFT OUTER JOIN afe_hosts_labels t3 ON (t2.host_id = t3.host_id) '
1286 'WHERE (t3.label_id IN '
1287 ' (SELECT label_id FROM afe_shards_labels '
1288 ' WHERE shard_id = %(shard_id)s))'
1289 )
MK Ryu5c7ef302015-07-10 17:23:08 -07001290
Fang Deng2b79fd72015-05-20 19:11:36 -07001291 # Even if we had filters about complete, active and aborted
MK Ryu5c7ef302015-07-10 17:23:08 -07001292 # bits in the above two SQLs, there is a chance that
Fang Deng2b79fd72015-05-20 19:11:36 -07001293 # the result may still contain a job with an hqe with 'complete=1'
1294 # or 'active=1' or 'aborted=0 and afe_job.id in known jobs.'
1295 # This happens when a job has two (or more) hqes and at least
1296 # one hqe has different bits than others.
1297 # We use a second sql to ensure we exclude all un-desired jobs.
1298 SQL_JOBS_TO_EXCLUDE =(
1299 'SELECT t1.id FROM afe_jobs t1 '
1300 'INNER JOIN afe_host_queue_entries t2 ON '
1301 ' (t1.id = t2.job_id) '
1302 'WHERE (t1.id in (%(candidates)s) '
1303 ' AND (t2.complete=1 OR t2.active=1 '
1304 ' %(check_known_jobs)s))'
1305 )
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001306
Jakob Juelichf88fa932014-09-03 17:58:04 -07001307 def _deserialize_relation(self, link, data):
1308 if link in ['hostqueueentry_set', 'jobkeyval_set']:
1309 for obj in data:
1310 obj['job_id'] = self.id
1311
1312 super(Job, self)._deserialize_relation(link, data)
1313
1314
1315 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001316 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -07001317 self.shard = Shard.deserialize(data)
1318
1319
Jakob Juelicha94efe62014-09-18 16:02:49 -07001320 def sanity_check_update_from_shard(self, shard, updated_serialized):
Jakob Juelich02e61292014-10-17 12:36:55 -07001321 # If the job got aborted on the master after the client fetched it
1322 # no shard_id will be set. The shard might still push updates though,
1323 # as the job might complete before the abort bit syncs to the shard.
1324 # Alternative considered: The master scheduler could be changed to not
1325 # set aborted jobs to completed that are sharded out. But that would
1326 # require database queries and seemed more complicated to implement.
1327 # This seems safe to do, as there won't be updates pushed from the wrong
1328 # shards should be powered off and wiped hen they are removed from the
1329 # master.
1330 if self.shard_id and self.shard_id != shard.id:
Jakob Juelicha94efe62014-09-18 16:02:49 -07001331 raise error.UnallowedRecordsSentToMaster(
1332 'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1333 'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1334 shard.id))
1335
1336
Simran Basi94dc0032013-11-12 14:09:46 -08001337 # TIMEOUT is deprecated.
showardb1e51872008-10-07 11:08:18 +00001338 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
Simran Basi94dc0032013-11-12 14:09:46 -08001339 'AUTOTEST_WEB', 'job_timeout_default', default=24)
1340 DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1341 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
Simran Basi34217022012-11-06 13:43:15 -08001342 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1343 # completed.
showard12f3e322009-05-13 21:27:42 +00001344 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1345 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001346 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1347 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001348 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1349 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1350 default=False)
showardb1e51872008-10-07 11:08:18 +00001351
showarda5288b42009-07-28 20:06:08 +00001352 owner = dbmodels.CharField(max_length=255)
1353 name = dbmodels.CharField(max_length=255)
Alex Miller7d658cf2013-09-04 16:00:35 -07001354 priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
jamesren4a41e012010-07-16 22:33:48 +00001355 control_file = dbmodels.TextField(null=True, blank=True)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001356 control_type = dbmodels.SmallIntegerField(
1357 choices=control_data.CONTROL_TYPE.choices(),
1358 blank=True, # to allow 0
1359 default=control_data.CONTROL_TYPE.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001360 created_on = dbmodels.DateTimeField()
Simran Basi8d89b642014-05-02 17:33:20 -07001361 synch_count = dbmodels.IntegerField(blank=True, default=0)
showardb1e51872008-10-07 11:08:18 +00001362 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
Dan Shi07e09af2013-04-12 09:31:29 -07001363 run_verify = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001364 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001365 dependency_labels = (
1366 dbmodels.ManyToManyField(Label, blank=True,
1367 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001368 reboot_before = dbmodels.SmallIntegerField(
1369 choices=model_attributes.RebootBefore.choices(), blank=True,
1370 default=DEFAULT_REBOOT_BEFORE)
1371 reboot_after = dbmodels.SmallIntegerField(
1372 choices=model_attributes.RebootAfter.choices(), blank=True,
1373 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001374 parse_failed_repair = dbmodels.BooleanField(
1375 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001376 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1377 # completed.
showard12f3e322009-05-13 21:27:42 +00001378 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001379 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001380 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001381
jamesren4a41e012010-07-16 22:33:48 +00001382 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1383 blank=True)
1384
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001385 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001386
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001387 test_retry = dbmodels.IntegerField(blank=True, default=0)
1388
Dan Shi07e09af2013-04-12 09:31:29 -07001389 run_reset = dbmodels.BooleanField(default=True)
1390
Simran Basi94dc0032013-11-12 14:09:46 -08001391 timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1392
Jakob Juelich8421d592014-09-17 15:27:06 -07001393 # If this is None on the master, a slave should be found.
1394 # If this is None on a slave, it should be synced back to the master
Jakob Jülich92c06332014-08-25 19:06:57 +00001395 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1396
Dan Shic9e17142015-02-19 11:50:55 -08001397 # If this is None, server-side packaging will be used for server side test,
1398 # unless it's disabled in global config AUTOSERV/enable_ssp_container.
1399 require_ssp = dbmodels.NullBooleanField(default=None, blank=True, null=True)
1400
jadmanski0afbb632008-06-06 21:10:57 +00001401 # custom manager
1402 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001403
1404
Alex Millerec212252014-02-28 16:48:34 -08001405 @decorators.cached_property
1406 def labels(self):
1407 """All the labels of this job"""
1408 # We need to convert dependency_labels to a list, because all() gives us
1409 # back an iterator, and storing/caching an iterator means we'd only be
1410 # able to read from it once.
1411 return list(self.dependency_labels.all())
1412
1413
jadmanski0afbb632008-06-06 21:10:57 +00001414 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001415 """Returns whether this job is of type server."""
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001416 return self.control_type == control_data.CONTROL_TYPE.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001417
1418
jadmanski0afbb632008-06-06 21:10:57 +00001419 @classmethod
jamesren4a41e012010-07-16 22:33:48 +00001420 def parameterized_jobs_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001421 """Returns whether parameterized jobs are enabled.
1422
1423 @param cls: Implicit class object.
1424 """
jamesren4a41e012010-07-16 22:33:48 +00001425 return global_config.global_config.get_config_value(
1426 'AUTOTEST_WEB', 'parameterized_jobs', type=bool)
1427
1428
1429 @classmethod
1430 def check_parameterized_job(cls, control_file, parameterized_job):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001431 """Checks that the job is valid given the global config settings.
jamesren4a41e012010-07-16 22:33:48 +00001432
1433 First, either control_file must be set, or parameterized_job must be
1434 set, but not both. Second, parameterized_job must be set if and only if
1435 the parameterized_jobs option in the global config is set to True.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001436
1437 @param cls: Implict class object.
1438 @param control_file: A control file.
1439 @param parameterized_job: A parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001440 """
1441 if not (bool(control_file) ^ bool(parameterized_job)):
1442 raise Exception('Job must have either control file or '
1443 'parameterization, but not both')
1444
1445 parameterized_jobs_enabled = cls.parameterized_jobs_enabled()
1446 if control_file and parameterized_jobs_enabled:
1447 raise Exception('Control file specified, but parameterized jobs '
1448 'are enabled')
1449 if parameterized_job and not parameterized_jobs_enabled:
1450 raise Exception('Parameterized job specified, but parameterized '
1451 'jobs are not enabled')
1452
1453
1454 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001455 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001456 """Creates a job.
1457
1458 The job is created by taking some information (the listed args) and
1459 filling in the rest of the necessary information.
1460
1461 @param cls: Implicit class object.
1462 @param owner: The owner for the job.
1463 @param options: An options object.
1464 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001465 """
showard3dd47c22008-07-10 00:41:36 +00001466 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001467
jamesren4a41e012010-07-16 22:33:48 +00001468 control_file = options.get('control_file')
1469 parameterized_job = options.get('parameterized_job')
jamesren4a41e012010-07-16 22:33:48 +00001470
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001471 # The current implementation of parameterized jobs requires that only
1472 # control files or parameterized jobs are used. Using the image
1473 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1474 # parameterized jobs and control files jobs, it does muck enough with
1475 # normal jobs by adding a parameterized id to them that this check will
1476 # fail. So for now we just skip this check.
1477 # cls.check_parameterized_job(control_file=control_file,
1478 # parameterized_job=parameterized_job)
showard4d233752010-01-20 19:06:40 +00001479 user = User.current_user()
1480 if options.get('reboot_before') is None:
1481 options['reboot_before'] = user.get_reboot_before_display()
1482 if options.get('reboot_after') is None:
1483 options['reboot_after'] = user.get_reboot_after_display()
1484
jamesren76fcf192010-04-21 20:39:50 +00001485 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1486
Simran Basi94dc0032013-11-12 14:09:46 -08001487 if options.get('timeout_mins') is None and options.get('timeout'):
1488 options['timeout_mins'] = options['timeout'] * 60
1489
jadmanski0afbb632008-06-06 21:10:57 +00001490 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001491 owner=owner,
1492 name=options['name'],
1493 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001494 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001495 control_type=options['control_type'],
1496 synch_count=options.get('synch_count'),
Simran Basi94dc0032013-11-12 14:09:46 -08001497 # timeout needs to be deleted in the future.
showarda1e74b32009-05-12 17:32:04 +00001498 timeout=options.get('timeout'),
Simran Basi94dc0032013-11-12 14:09:46 -08001499 timeout_mins=options.get('timeout_mins'),
Simran Basi34217022012-11-06 13:43:15 -08001500 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001501 run_verify=options.get('run_verify'),
1502 email_list=options.get('email_list'),
1503 reboot_before=options.get('reboot_before'),
1504 reboot_after=options.get('reboot_after'),
1505 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001506 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001507 drone_set=drone_set,
Aviv Keshet18308922013-02-19 17:49:49 -08001508 parameterized_job=parameterized_job,
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001509 parent_job=options.get('parent_job_id'),
Dan Shi07e09af2013-04-12 09:31:29 -07001510 test_retry=options.get('test_retry'),
Dan Shic9e17142015-02-19 11:50:55 -08001511 run_reset=options.get('run_reset'),
1512 require_ssp=options.get('require_ssp'))
mblighe8819cd2008-02-15 16:48:40 +00001513
showarda1e74b32009-05-12 17:32:04 +00001514 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001515
jamesrend8b6e172010-04-16 23:45:00 +00001516 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001517 for key, value in options['keyvals'].iteritems():
1518 JobKeyval.objects.create(job=job, key=key, value=value)
1519
jadmanski0afbb632008-06-06 21:10:57 +00001520 return job
mblighe8819cd2008-02-15 16:48:40 +00001521
1522
Jakob Juelich59cfe542014-09-02 16:37:46 -07001523 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -07001524 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -07001525 """Assigns unassigned jobs to a shard.
1526
Jakob Juelich1b525742014-09-30 13:08:07 -07001527 For all labels that have been assigned to this shard, all jobs that
1528 have this label, are assigned to this shard.
1529
1530 Jobs that are assigned to the shard but aren't already present on the
1531 shard are returned.
1532
Jakob Juelich59cfe542014-09-02 16:37:46 -07001533 @param shard: The shard to assign jobs to.
Jakob Juelich1b525742014-09-30 13:08:07 -07001534 @param known_ids: List of all ids of incomplete jobs, the shard already
1535 knows about.
1536 This is used to figure out which jobs should be sent
1537 to the shard. If shard_ids were used instead, jobs
1538 would only be transferred once, even if the client
1539 failed persisting them.
1540 The number of unfinished jobs usually lies in O(1000).
1541 Assuming one id takes 8 chars in the json, this means
1542 overhead that lies in the lower kilobyte range.
1543 A not in query with 5000 id's takes about 30ms.
1544
Jakob Juelich59cfe542014-09-02 16:37:46 -07001545 @returns The job objects that should be sent to the shard.
1546 """
1547 # Disclaimer: Concurrent heartbeats should not occur in today's setup.
1548 # If this changes or they are triggered manually, this applies:
1549 # Jobs may be returned more than once by concurrent calls of this
1550 # function, as there is a race condition between SELECT and UPDATE.
Fang Deng2b79fd72015-05-20 19:11:36 -07001551 job_ids = set([])
1552 check_known_jobs_exclude = ''
1553 check_known_jobs_include = ''
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001554
Fang Deng2b79fd72015-05-20 19:11:36 -07001555 if known_ids:
1556 check_known_jobs = (
1557 cls.NON_ABORTED_KNOWN_JOBS %
1558 {'known_ids': ','.join([str(i) for i in known_ids])})
1559 check_known_jobs_exclude = 'AND NOT ' + check_known_jobs
1560 check_known_jobs_include = 'OR ' + check_known_jobs
1561
MK Ryu5c7ef302015-07-10 17:23:08 -07001562 for sql in [cls.SQL_SHARD_JOBS, cls.SQL_SHARD_JOBS_WITH_HOSTS]:
MK Ryu2e3e6052015-07-09 16:15:18 -07001563 query = Job.objects.raw(sql % {
1564 'check_known_jobs': check_known_jobs_exclude,
1565 'shard_id': shard.id})
Fang Deng2b79fd72015-05-20 19:11:36 -07001566 job_ids |= set([j.id for j in query])
1567
1568 if job_ids:
1569 query = Job.objects.raw(
1570 cls.SQL_JOBS_TO_EXCLUDE %
1571 {'check_known_jobs': check_known_jobs_include,
1572 'candidates': ','.join([str(i) for i in job_ids])})
1573 job_ids -= set([j.id for j in query])
1574
Jakob Juelich59cfe542014-09-02 16:37:46 -07001575 if job_ids:
1576 Job.objects.filter(pk__in=job_ids).update(shard=shard)
1577 return list(Job.objects.filter(pk__in=job_ids).all())
1578 return []
1579
1580
jamesren4a41e012010-07-16 22:33:48 +00001581 def save(self, *args, **kwargs):
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001582 # The current implementation of parameterized jobs requires that only
1583 # control files or parameterized jobs are used. Using the image
1584 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1585 # parameterized jobs and control files jobs, it does muck enough with
1586 # normal jobs by adding a parameterized id to them that this check will
1587 # fail. So for now we just skip this check.
1588 # cls.check_parameterized_job(control_file=self.control_file,
1589 # parameterized_job=self.parameterized_job)
jamesren4a41e012010-07-16 22:33:48 +00001590 super(Job, self).save(*args, **kwargs)
1591
1592
showard29f7cd22009-04-29 21:16:24 +00001593 def queue(self, hosts, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001594 """Enqueue a job on the given hosts.
1595
1596 @param hosts: The hosts to use.
1597 @param atomic_group: The associated atomic group.
1598 @param is_template: Whether the status should be "Template".
1599 """
showarda9545c02009-12-18 22:44:26 +00001600 if not hosts:
1601 if atomic_group:
1602 # No hosts or labels are required to queue an atomic group
1603 # Job. However, if they are given, we respect them below.
1604 atomic_group.enqueue_job(self, is_template=is_template)
1605 else:
1606 # hostless job
1607 entry = HostQueueEntry.create(job=self, is_template=is_template)
1608 entry.save()
1609 return
1610
jadmanski0afbb632008-06-06 21:10:57 +00001611 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +00001612 host.enqueue_job(self, atomic_group=atomic_group,
1613 is_template=is_template)
1614
1615
1616 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001617 """Creates a recurring job.
1618
1619 @param start_date: The starting date of the job.
1620 @param loop_period: How often to re-run the job, in seconds.
1621 @param loop_count: The re-run count.
1622 @param owner: The owner of the job.
1623 """
showard29f7cd22009-04-29 21:16:24 +00001624 rec = RecurringRun(job=self, start_date=start_date,
1625 loop_period=loop_period,
1626 loop_count=loop_count,
1627 owner=User.objects.get(login=owner))
1628 rec.save()
1629 return rec.id
mblighe8819cd2008-02-15 16:48:40 +00001630
1631
jadmanski0afbb632008-06-06 21:10:57 +00001632 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001633 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001634 try:
1635 return User.objects.get(login=self.owner)
1636 except self.DoesNotExist:
1637 return None
mblighe8819cd2008-02-15 16:48:40 +00001638
1639
showard64a95952010-01-13 21:27:16 +00001640 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001641 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001642 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001643 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001644
1645
showardd1195652009-12-08 22:21:02 +00001646 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001647 """Returns a string tag for this job."""
MK Ryu0c1a37d2015-04-30 12:00:55 -07001648 return server_utils.get_job_tag(self.id, self.owner)
showardd1195652009-12-08 22:21:02 +00001649
1650
showardc1a98d12010-01-15 00:22:22 +00001651 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001652 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001653 return dict((keyval.key, keyval.value)
1654 for keyval in self.jobkeyval_set.all())
1655
1656
Fang Dengff361592015-02-02 15:27:34 -08001657 @classmethod
1658 def get_attribute_model(cls):
1659 """Return the attribute model.
1660
1661 Override method in parent class. This class is called when
1662 deserializing the one-to-many relationship betwen Job and JobKeyval.
1663 On deserialization, we will try to clear any existing job keyvals
1664 associated with a job to avoid any inconsistency.
1665 Though Job doesn't implement ModelWithAttribute, we still treat
1666 it as an attribute model for this purpose.
1667
1668 @returns: The attribute model of Job.
1669 """
1670 return JobKeyval
1671
1672
jadmanski0afbb632008-06-06 21:10:57 +00001673 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001674 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001675 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001676
showarda5288b42009-07-28 20:06:08 +00001677 def __unicode__(self):
1678 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001679
1680
showardc1a98d12010-01-15 00:22:22 +00001681class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1682 """Keyvals associated with jobs"""
Fang Dengff361592015-02-02 15:27:34 -08001683
1684 SERIALIZATION_LINKS_TO_KEEP = set(['job'])
1685 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
1686
showardc1a98d12010-01-15 00:22:22 +00001687 job = dbmodels.ForeignKey(Job)
1688 key = dbmodels.CharField(max_length=90)
1689 value = dbmodels.CharField(max_length=300)
1690
1691 objects = model_logic.ExtendedManager()
1692
Fang Dengff361592015-02-02 15:27:34 -08001693
1694 @classmethod
1695 def get_record(cls, data):
1696 """Check the database for an identical record.
1697
1698 Use job_id and key to search for a existing record.
1699
1700 @raises: DoesNotExist, if no record found
1701 @raises: MultipleObjectsReturned if multiple records found.
1702 """
1703 # TODO(fdeng): We should use job_id and key together as
1704 # a primary key in the db.
1705 return cls.objects.get(job_id=data['job_id'], key=data['key'])
1706
1707
1708 @classmethod
1709 def deserialize(cls, data):
1710 """Override deserialize in parent class.
1711
1712 Do not deserialize id as id is not kept consistent on master and shards.
1713
1714 @param data: A dictionary of data to deserialize.
1715
1716 @returns: A JobKeyval object.
1717 """
1718 if data:
1719 data.pop('id')
1720 return super(JobKeyval, cls).deserialize(data)
1721
1722
showardc1a98d12010-01-15 00:22:22 +00001723 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001724 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001725 db_table = 'afe_job_keyvals'
1726
1727
showard7c785282008-05-29 19:45:12 +00001728class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001729 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001730 job = dbmodels.ForeignKey(Job)
1731 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001732
jadmanski0afbb632008-06-06 21:10:57 +00001733 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001734
jadmanski0afbb632008-06-06 21:10:57 +00001735 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001736 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001737 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001738
mblighe8819cd2008-02-15 16:48:40 +00001739
showard7c785282008-05-29 19:45:12 +00001740class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001741 """Represents a host queue entry."""
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001742
1743 SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001744 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Jakob Juelichf865d332014-09-29 10:47:49 -07001745 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001746
Jakob Juelichf88fa932014-09-03 17:58:04 -07001747
1748 def custom_deserialize_relation(self, link, data):
1749 assert link == 'meta_host'
1750 self.meta_host = Label.deserialize(data)
1751
1752
Jakob Juelicha94efe62014-09-18 16:02:49 -07001753 def sanity_check_update_from_shard(self, shard, updated_serialized,
1754 job_ids_sent):
1755 if self.job_id not in job_ids_sent:
1756 raise error.UnallowedRecordsSentToMaster(
1757 'Sent HostQueueEntry without corresponding '
1758 'job entry: %s' % updated_serialized)
1759
1760
showardeaa408e2009-09-11 18:45:31 +00001761 Status = host_queue_entry_states.Status
1762 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001763 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
Laurence Goodby303d2662016-07-01 15:57:04 -07001764 PRE_JOB_STATUSES = host_queue_entry_states.PRE_JOB_STATUSES
1765 IDLE_PRE_JOB_STATUSES = host_queue_entry_states.IDLE_PRE_JOB_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001766
jadmanski0afbb632008-06-06 21:10:57 +00001767 job = dbmodels.ForeignKey(Job)
1768 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001769 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001770 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1771 db_column='meta_host')
1772 active = dbmodels.BooleanField(default=False)
1773 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001774 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001775 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1776 default='')
showard89f84db2009-03-12 20:39:13 +00001777 # If atomic_group is set, this is a virtual HostQueueEntry that will
1778 # be expanded into many actual hosts within the group at schedule time.
1779 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001780 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001781 started_on = dbmodels.DateTimeField(null=True, blank=True)
Fang Deng51599032014-06-23 17:24:27 -07001782 finished_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001783
jadmanski0afbb632008-06-06 21:10:57 +00001784 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001785
mblighe8819cd2008-02-15 16:48:40 +00001786
showard2bab8f42008-11-12 18:15:22 +00001787 def __init__(self, *args, **kwargs):
1788 super(HostQueueEntry, self).__init__(*args, **kwargs)
1789 self._record_attributes(['status'])
1790
1791
showard29f7cd22009-04-29 21:16:24 +00001792 @classmethod
1793 def create(cls, job, host=None, meta_host=None, atomic_group=None,
1794 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001795 """Creates a new host queue entry.
1796
1797 @param cls: Implicit class object.
1798 @param job: The associated job.
1799 @param host: The associated host.
1800 @param meta_host: The associated meta host.
1801 @param atomic_group: The associated atomic group.
1802 @param is_template: Whether the status should be "Template".
1803 """
showard29f7cd22009-04-29 21:16:24 +00001804 if is_template:
1805 status = cls.Status.TEMPLATE
1806 else:
1807 status = cls.Status.QUEUED
1808
1809 return cls(job=job, host=host, meta_host=meta_host,
1810 atomic_group=atomic_group, status=status)
1811
1812
showarda5288b42009-07-28 20:06:08 +00001813 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001814 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001815 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001816 self._check_for_updated_attributes()
1817
1818
showardc0ac3a72009-07-08 21:14:45 +00001819 def execution_path(self):
1820 """
1821 Path to this entry's results (relative to the base results directory).
1822 """
MK Ryu0c1a37d2015-04-30 12:00:55 -07001823 return server_utils.get_hqe_exec_path(self.job.tag(),
1824 self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001825
1826
showard3f15eed2008-11-14 22:40:48 +00001827 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001828 """Returns the first non-None name found in priority order.
1829
1830 The priority order checked is: (1) host name; (2) meta host name; and
1831 (3) atomic group name.
1832 """
showard3f15eed2008-11-14 22:40:48 +00001833 if self.host:
1834 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001835 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +00001836 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +00001837 else:
1838 assert self.atomic_group, "no host, meta_host or atomic group!"
1839 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +00001840
1841
showard2bab8f42008-11-12 18:15:22 +00001842 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001843 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001844 self.active, self.complete = True, False
1845 elif self.status in self.COMPLETE_STATUSES:
1846 self.active, self.complete = False, True
1847 else:
1848 self.active, self.complete = False, False
1849
1850
1851 def on_attribute_changed(self, attribute, old_value):
1852 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001853 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1854 self.status)
showard2bab8f42008-11-12 18:15:22 +00001855
1856
jadmanski0afbb632008-06-06 21:10:57 +00001857 def is_meta_host_entry(self):
1858 'True if this is a entry has a meta_host instead of a host.'
1859 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001860
showarda3ab0d52008-11-03 19:03:47 +00001861
Simran Basic1b26762013-06-26 14:23:21 -07001862 # This code is shared between rpc_interface and models.HostQueueEntry.
1863 # Sadly due to circular imports between the 2 (crbug.com/230100) making it
1864 # a class method was the best way to refactor it. Attempting to put it in
1865 # rpc_utils or a new utils module failed as that would require us to import
1866 # models.py but to call it from here we would have to import the utils.py
1867 # thus creating a cycle.
1868 @classmethod
1869 def abort_host_queue_entries(cls, host_queue_entries):
1870 """Aborts a collection of host_queue_entries.
1871
1872 Abort these host queue entry and all host queue entries of jobs created
1873 by them.
1874
1875 @param host_queue_entries: List of host queue entries we want to abort.
1876 """
1877 # This isn't completely immune to race conditions since it's not atomic,
1878 # but it should be safe given the scheduler's behavior.
1879
1880 # TODO(milleral): crbug.com/230100
1881 # The |abort_host_queue_entries| rpc does nearly exactly this,
1882 # however, trying to re-use the code generates some horrible
1883 # circular import error. I'd be nice to refactor things around
1884 # sometime so the code could be reused.
1885
1886 # Fixpoint algorithm to find the whole tree of HQEs to abort to
1887 # minimize the total number of database queries:
1888 children = set()
1889 new_children = set(host_queue_entries)
1890 while new_children:
1891 children.update(new_children)
1892 new_child_ids = [hqe.job_id for hqe in new_children]
1893 new_children = HostQueueEntry.objects.filter(
1894 job__parent_job__in=new_child_ids,
1895 complete=False, aborted=False).all()
1896 # To handle circular parental relationships
1897 new_children = set(new_children) - children
1898
1899 # Associate a user with the host queue entries that we're about
1900 # to abort so that we can look up who to blame for the aborts.
1901 now = datetime.now()
1902 user = User.current_user()
1903 aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
1904 aborted_by=user, aborted_on=now) for hqe in children]
1905 AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
1906 # Bulk update all of the HQEs to set the abort bit.
1907 child_ids = [hqe.id for hqe in children]
1908 HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
1909
1910
Scott Zawalski23041432013-04-17 07:39:09 -07001911 def abort(self):
Alex Millerdea67042013-04-22 17:23:34 -07001912 """ Aborts this host queue entry.
Simran Basic1b26762013-06-26 14:23:21 -07001913
Alex Millerdea67042013-04-22 17:23:34 -07001914 Abort this host queue entry and all host queue entries of jobs created by
1915 this one.
1916
1917 """
showardd3dc1992009-04-22 21:01:40 +00001918 if not self.complete and not self.aborted:
Simran Basi97582a22013-06-27 12:03:21 -07001919 HostQueueEntry.abort_host_queue_entries([self])
mblighe8819cd2008-02-15 16:48:40 +00001920
showardd3dc1992009-04-22 21:01:40 +00001921
1922 @classmethod
1923 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001924 """Returns a modified status msg if the host queue entry was aborted.
1925
1926 @param cls: Implicit class object.
1927 @param status: The original status message.
1928 @param aborted: Whether the host queue entry was aborted.
1929 @param complete: Whether the host queue entry was completed.
1930 """
showardd3dc1992009-04-22 21:01:40 +00001931 if aborted and not complete:
1932 return 'Aborted (%s)' % status
1933 return status
1934
1935
1936 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001937 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00001938 return self.compute_full_status(self.status, self.aborted,
1939 self.complete)
1940
1941
1942 def _postprocess_object_dict(self, object_dict):
1943 object_dict['full_status'] = self.full_status()
1944
1945
jadmanski0afbb632008-06-06 21:10:57 +00001946 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001947 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001948 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00001949
showard12f3e322009-05-13 21:27:42 +00001950
showard4c119042008-09-29 19:16:18 +00001951
showarda5288b42009-07-28 20:06:08 +00001952 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00001953 hostname = None
1954 if self.host:
1955 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00001956 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00001957
1958
showard4c119042008-09-29 19:16:18 +00001959class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001960 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00001961 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
1962 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00001963 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00001964
1965 objects = model_logic.ExtendedManager()
1966
showard68c7aa02008-10-09 16:49:11 +00001967
showarda5288b42009-07-28 20:06:08 +00001968 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00001969 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00001970 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00001971
showard4c119042008-09-29 19:16:18 +00001972 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001973 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001974 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00001975
1976
1977class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
1978 """\
1979 job: job to use as a template
1980 owner: owner of the instantiated template
1981 start_date: Run the job at scheduled date
1982 loop_period: Re-run (loop) the job periodically
1983 (in every loop_period seconds)
1984 loop_count: Re-run (loop) count
1985 """
1986
1987 job = dbmodels.ForeignKey(Job)
1988 owner = dbmodels.ForeignKey(User)
1989 start_date = dbmodels.DateTimeField()
1990 loop_period = dbmodels.IntegerField(blank=True)
1991 loop_count = dbmodels.IntegerField(blank=True)
1992
1993 objects = model_logic.ExtendedManager()
1994
1995 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001996 """Metadata for class RecurringRun."""
showardeab66ce2009-12-23 00:03:56 +00001997 db_table = 'afe_recurring_run'
showard29f7cd22009-04-29 21:16:24 +00001998
showarda5288b42009-07-28 20:06:08 +00001999 def __unicode__(self):
2000 return u'RecurringRun(job %s, start %s, period %s, count %s)' % (
showard29f7cd22009-04-29 21:16:24 +00002001 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +00002002
2003
2004class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
2005 """\
2006 Tasks to run on hosts at the next time they are in the Ready state. Use this
2007 for high-priority tasks, such as forced repair or forced reinstall.
2008
2009 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00002010 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00002011 time_requested: date and time the request for this task was made
2012 is_active: task is currently running
2013 is_complete: task has finished running
beeps8bb1f7d2013-08-05 01:30:09 -07002014 is_aborted: task was aborted
showard2fe3f1d2009-07-06 20:19:11 +00002015 time_started: date and time the task started
Dan Shid0725542014-06-23 15:34:27 -07002016 time_finished: date and time the task finished
showard2fe3f1d2009-07-06 20:19:11 +00002017 queue_entry: Host queue entry waiting on this task (or None, if task was not
2018 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00002019 """
Alex Millerdfff2fd2013-05-28 13:05:06 -07002020 Task = enum.Enum('Verify', 'Cleanup', 'Repair', 'Reset', 'Provision',
Dan Shi07e09af2013-04-12 09:31:29 -07002021 string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00002022
2023 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00002024 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00002025 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00002026 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00002027 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
2028 null=False)
2029 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
2030 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
beeps8bb1f7d2013-08-05 01:30:09 -07002031 is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00002032 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00002033 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00002034 success = dbmodels.BooleanField(default=False, blank=False, null=False)
Dan Shid0725542014-06-23 15:34:27 -07002035 time_finished = dbmodels.DateTimeField(null=True, blank=True)
showard6d7b2ff2009-06-10 00:16:47 +00002036
2037 objects = model_logic.ExtendedManager()
2038
2039
showard9bb960b2009-11-19 01:02:11 +00002040 def save(self, **kwargs):
2041 if self.queue_entry:
2042 self.requested_by = User.objects.get(
2043 login=self.queue_entry.job.owner)
2044 super(SpecialTask, self).save(**kwargs)
2045
2046
showarded2afea2009-07-07 20:54:07 +00002047 def execution_path(self):
MK Ryu0c1a37d2015-04-30 12:00:55 -07002048 """Returns the execution path for a special task."""
2049 return server_utils.get_special_task_exec_path(
2050 self.host.hostname, self.id, self.task, self.time_requested)
showarded2afea2009-07-07 20:54:07 +00002051
2052
showardc0ac3a72009-07-08 21:14:45 +00002053 # property to emulate HostQueueEntry.status
2054 @property
2055 def status(self):
MK Ryu0c1a37d2015-04-30 12:00:55 -07002056 """Returns a host queue entry status appropriate for a speical task."""
2057 return server_utils.get_special_task_status(
2058 self.is_complete, self.success, self.is_active)
showardc0ac3a72009-07-08 21:14:45 +00002059
2060
2061 # property to emulate HostQueueEntry.started_on
2062 @property
2063 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002064 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00002065 return self.time_started
2066
2067
showard6d7b2ff2009-06-10 00:16:47 +00002068 @classmethod
showardc5103442010-01-15 00:20:26 +00002069 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002070 """Schedules a special task on a host if not already scheduled.
2071
2072 @param cls: Implicit class object.
2073 @param host: The host to use.
2074 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00002075 """
showardc5103442010-01-15 00:20:26 +00002076 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
2077 is_active=False,
2078 is_complete=False)
2079 if existing_tasks:
2080 return existing_tasks[0]
2081
2082 special_task = SpecialTask(host=host, task=task,
2083 requested_by=User.current_user())
2084 special_task.save()
2085 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00002086
2087
beeps8bb1f7d2013-08-05 01:30:09 -07002088 def abort(self):
2089 """ Abort this special task."""
2090 self.is_aborted = True
2091 self.save()
2092
2093
showarded2afea2009-07-07 20:54:07 +00002094 def activate(self):
showard474d1362009-08-20 23:32:01 +00002095 """
2096 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00002097 """
showard97446882009-07-20 22:37:28 +00002098 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00002099 self.is_active = True
2100 self.time_started = datetime.now()
2101 self.save()
2102
2103
showarde60e44e2009-11-13 20:45:38 +00002104 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002105 """Sets a task as completed.
2106
2107 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00002108 """
showard97446882009-07-20 22:37:28 +00002109 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00002110 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00002111 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00002112 self.success = success
Dan Shid85d6112014-07-14 10:32:55 -07002113 if self.time_started:
2114 self.time_finished = datetime.now()
showard2fe3f1d2009-07-06 20:19:11 +00002115 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00002116
2117
2118 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002119 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00002120 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00002121
showard474d1362009-08-20 23:32:01 +00002122
showarda5288b42009-07-28 20:06:08 +00002123 def __unicode__(self):
2124 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00002125 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00002126 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00002127 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00002128 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00002129 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00002130
2131 return result
Dan Shi6964fa52014-12-18 11:04:27 -08002132
2133
2134class StableVersion(dbmodels.Model, model_logic.ModelExtensions):
2135
2136 board = dbmodels.CharField(max_length=255, unique=True)
2137 version = dbmodels.CharField(max_length=255)
2138
2139 class Meta:
2140 """Metadata for class StableVersion."""
Fang Deng86248502014-12-18 16:38:00 -08002141 db_table = 'afe_stable_versions'