blob: 99199779dbc753b586965fb7371fb19cce384e0c [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:
Prashanth B489b91d2014-03-15 12:17:16 -0700442 status: string describing status of host
443 invalid: true if the host has been deleted
444 protection: indicates what can be done to this host during repair
445 lock_time: DateTime at which the host was locked
446 dirty: true if the host has been used without being rebooted
447 Local:
448 locked_by: user that locked the host, or null if the host is unlocked
jadmanski0afbb632008-06-06 21:10:57 +0000449 """
mblighe8819cd2008-02-15 16:48:40 +0000450
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700451 SERIALIZATION_LINKS_TO_FOLLOW = set(['aclgroup_set',
452 'hostattribute_set',
453 'labels',
454 'shard'])
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800455 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['invalid'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700456
Jakob Juelichf88fa932014-09-03 17:58:04 -0700457
458 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700459 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -0700460 self.shard = Shard.deserialize(data)
461
462
Prashanth B489b91d2014-03-15 12:17:16 -0700463 # Note: Only specify foreign keys here, specify all native host columns in
464 # rdb_model_extensions instead.
465 Protection = host_protections.Protection
showardeab66ce2009-12-23 00:03:56 +0000466 labels = dbmodels.ManyToManyField(Label, blank=True,
467 db_table='afe_hosts_labels')
showardfb2a7fa2008-07-17 17:04:12 +0000468 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
jadmanski0afbb632008-06-06 21:10:57 +0000469 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000470 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000471 valid_objects = model_logic.ValidObjectsManager()
beepscc9fc702013-12-02 12:45:38 -0800472 leased_objects = model_logic.LeasedHostManager()
mbligh5244cbb2008-04-24 20:39:52 +0000473
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700474 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
showard2bab8f42008-11-12 18:15:22 +0000475
476 def __init__(self, *args, **kwargs):
477 super(Host, self).__init__(*args, **kwargs)
478 self._record_attributes(['status'])
479
480
showardb8471e32008-07-03 19:51:08 +0000481 @staticmethod
482 def create_one_time_host(hostname):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800483 """Creates a one-time host.
484
485 @param hostname: The name for the host.
486 """
showardb8471e32008-07-03 19:51:08 +0000487 query = Host.objects.filter(hostname=hostname)
488 if query.count() == 0:
489 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000490 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000491 else:
492 host = query[0]
493 if not host.invalid:
494 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000495 'hostname' : '%s already exists in the autotest DB. '
496 'Select it rather than entering it as a one time '
497 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000498 })
showard1ab512b2008-07-30 23:39:04 +0000499 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000500 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000501 host.save()
showard2924b0a2009-06-18 23:16:15 +0000502 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000503 return host
mbligh5244cbb2008-04-24 20:39:52 +0000504
showard1ff7b2e2009-05-15 23:17:18 +0000505
Jakob Juelich59cfe542014-09-02 16:37:46 -0700506 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -0700507 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -0700508 """Assigns hosts to a shard.
509
MK Ryu638a6cf2015-05-08 14:49:43 -0700510 For all labels that have been assigned to a shard, all hosts that
511 have at least one of the shard's labels are assigned to the shard.
Jakob Juelich1b525742014-09-30 13:08:07 -0700512 Hosts that are assigned to the shard but aren't already present on the
513 shard are returned.
Jakob Juelich59cfe542014-09-02 16:37:46 -0700514
MK Ryu638a6cf2015-05-08 14:49:43 -0700515 Board to shard mapping is many-to-one. Many different boards can be
516 hosted in a shard. However, DUTs of a single board cannot be distributed
517 into more than one shard.
518
Jakob Juelich59cfe542014-09-02 16:37:46 -0700519 @param shard: The shard object to assign labels/hosts for.
Jakob Juelich1b525742014-09-30 13:08:07 -0700520 @param known_ids: List of all host-ids the shard already knows.
521 This is used to figure out which hosts should be sent
522 to the shard. If shard_ids were used instead, hosts
523 would only be transferred once, even if the client
524 failed persisting them.
525 The number of hosts usually lies in O(100), so the
526 overhead is acceptable.
527
Jakob Juelich59cfe542014-09-02 16:37:46 -0700528 @returns the hosts objects that should be sent to the shard.
529 """
530
531 # Disclaimer: concurrent heartbeats should theoretically not occur in
532 # the current setup. As they may be introduced in the near future,
533 # this comment will be left here.
534
535 # Sending stuff twice is acceptable, but forgetting something isn't.
536 # Detecting duplicates on the client is easy, but here it's harder. The
537 # following options were considered:
538 # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
539 # than select returned, as concurrently more hosts might have been
540 # inserted
541 # - UPDATE and then SELECT WHERE shard=shard: select always returns all
542 # hosts for the shard, this is overhead
543 # - SELECT and then UPDATE only selected without requerying afterwards:
544 # returns the old state of the records.
MK Ryu638a6cf2015-05-08 14:49:43 -0700545 host_ids = set(Host.objects.filter(
546 labels__in=shard.labels.all(),
Jakob Juelich59cfe542014-09-02 16:37:46 -0700547 leased=False
Jakob Juelich1b525742014-09-30 13:08:07 -0700548 ).exclude(
549 id__in=known_ids,
Jakob Juelich59cfe542014-09-02 16:37:46 -0700550 ).values_list('pk', flat=True))
551
552 if host_ids:
553 Host.objects.filter(pk__in=host_ids).update(shard=shard)
554 return list(Host.objects.filter(pk__in=host_ids).all())
555 return []
556
showardafd97de2009-10-01 18:45:09 +0000557 def resurrect_object(self, old_object):
558 super(Host, self).resurrect_object(old_object)
559 # invalid hosts can be in use by the scheduler (as one-time hosts), so
560 # don't change the status
561 self.status = old_object.status
562
563
jadmanski0afbb632008-06-06 21:10:57 +0000564 def clean_object(self):
565 self.aclgroup_set.clear()
566 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000567
568
Dan Shia0acfbc2014-10-14 15:56:23 -0700569 def record_state(self, type_str, state, value, other_metadata=None):
570 """Record metadata in elasticsearch.
571
572 @param type_str: sets the _type field in elasticsearch db.
573 @param state: string representing what state we are recording,
574 e.g. 'locked'
575 @param value: value of the state, e.g. True
576 @param other_metadata: Other metadata to store in metaDB.
577 """
578 metadata = {
579 state: value,
580 'hostname': self.hostname,
581 }
582 if other_metadata:
583 metadata = dict(metadata.items() + other_metadata.items())
Matthew Sartori68186332015-04-27 17:19:53 -0700584 autotest_es.post(use_http=True, type_str=type_str, metadata=metadata)
Dan Shia0acfbc2014-10-14 15:56:23 -0700585
586
showarda5288b42009-07-28 20:06:08 +0000587 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000588 # extra spaces in the hostname can be a sneaky source of errors
589 self.hostname = self.hostname.strip()
590 # is this a new object being saved for the first time?
591 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000592 if not first_time:
593 AclGroup.check_for_acl_violation_hosts([self])
Dan Shia0acfbc2014-10-14 15:56:23 -0700594 # If locked is changed, send its status and user made the change to
595 # metaDB. Locks are important in host history because if a device is
596 # locked then we don't really care what state it is in.
showardfb2a7fa2008-07-17 17:04:12 +0000597 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000598 self.locked_by = User.current_user()
MK Ryud53e1492015-12-15 12:09:03 -0800599 if not self.lock_time:
600 self.lock_time = datetime.now()
Dan Shia0acfbc2014-10-14 15:56:23 -0700601 self.record_state('lock_history', 'locked', self.locked,
Matthew Sartori68186332015-04-27 17:19:53 -0700602 {'changed_by': self.locked_by.login,
603 'lock_reason': self.lock_reason})
showard21baa452008-10-21 00:08:39 +0000604 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000605 elif not self.locked and self.locked_by:
Dan Shia0acfbc2014-10-14 15:56:23 -0700606 self.record_state('lock_history', 'locked', self.locked,
607 {'changed_by': self.locked_by.login})
showardfb2a7fa2008-07-17 17:04:12 +0000608 self.locked_by = None
609 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000610 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000611 if first_time:
612 everyone = AclGroup.objects.get(name='Everyone')
613 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000614 self._check_for_updated_attributes()
615
mblighe8819cd2008-02-15 16:48:40 +0000616
showardb8471e32008-07-03 19:51:08 +0000617 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000618 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000619 for queue_entry in self.hostqueueentry_set.all():
620 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000621 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000622 super(Host, self).delete()
623
mblighe8819cd2008-02-15 16:48:40 +0000624
showard2bab8f42008-11-12 18:15:22 +0000625 def on_attribute_changed(self, attribute, old_value):
626 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000627 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000628
629
showard29f7cd22009-04-29 21:16:24 +0000630 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800631 """Enqueue a job on this host.
632
633 @param job: A job to enqueue.
634 @param atomic_group: The associated atomic group.
635 @param is_template: Whther the status should be "Template".
636 """
showard29f7cd22009-04-29 21:16:24 +0000637 queue_entry = HostQueueEntry.create(host=self, job=job,
638 is_template=is_template,
639 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000640 # allow recovery of dead hosts from the frontend
641 if not self.active_queue_entry() and self.is_dead():
642 self.status = Host.Status.READY
643 self.save()
644 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000645
showard08f981b2008-06-24 21:59:03 +0000646 block = IneligibleHostQueue(job=job, host=self)
647 block.save()
648
mblighe8819cd2008-02-15 16:48:40 +0000649
jadmanski0afbb632008-06-06 21:10:57 +0000650 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800651 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000652 # TODO(showard): slighly hacky?
653 platforms = self.labels.filter(platform=True)
654 if len(platforms) == 0:
655 return None
656 return platforms[0]
657 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000658
659
showardcafd16e2009-05-29 18:37:49 +0000660 @classmethod
661 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800662 """Verify the specified hosts have no associated platforms.
663
664 @param cls: Implicit class object.
665 @param hosts: The hosts to verify.
666 @raises model_logic.ValidationError if any hosts already have a
667 platform.
668 """
showardcafd16e2009-05-29 18:37:49 +0000669 Host.objects.populate_relationships(hosts, Label, 'label_list')
670 errors = []
671 for host in hosts:
672 platforms = [label.name for label in host.label_list
673 if label.platform]
674 if platforms:
675 # do a join, just in case this host has multiple platforms,
676 # we'll be able to see it
677 errors.append('Host %s already has a platform: %s' % (
678 host.hostname, ', '.join(platforms)))
679 if errors:
680 raise model_logic.ValidationError({'labels': '; '.join(errors)})
681
682
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700683 @classmethod
Dan Shib5b8b4f2016-11-02 14:04:02 -0700684 def check_board_labels_allowed(cls, hosts, new_labels=[]):
685 """Verify the specified hosts have valid board labels and the given
686 new board labels can be added.
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700687
688 @param cls: Implicit class object.
689 @param hosts: The hosts to verify.
Dan Shib5b8b4f2016-11-02 14:04:02 -0700690 @param new_labels: A list of labels to be added to the hosts.
691
692 @raises model_logic.ValidationError if any host has invalid board labels
693 or the given board labels cannot be added to the hsots.
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700694 """
695 Host.objects.populate_relationships(hosts, Label, 'label_list')
696 errors = []
697 for host in hosts:
698 boards = [label.name for label in host.label_list
699 if label.name.startswith('board:')]
Dan Shib5b8b4f2016-11-02 14:04:02 -0700700 if not server_utils.board_labels_allowed(boards + new_labels):
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700701 # do a join, just in case this host has multiple boards,
702 # we'll be able to see it
Dan Shib5b8b4f2016-11-02 14:04:02 -0700703 errors.append('Host %s already has board labels: %s' % (
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700704 host.hostname, ', '.join(boards)))
705 if errors:
706 raise model_logic.ValidationError({'labels': '; '.join(errors)})
707
708
jadmanski0afbb632008-06-06 21:10:57 +0000709 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800710 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000711 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000712
713
jadmanski0afbb632008-06-06 21:10:57 +0000714 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800715 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000716 active = list(self.hostqueueentry_set.filter(active=True))
717 if not active:
718 return None
719 assert len(active) == 1, ('More than one active entry for '
720 'host ' + self.hostname)
721 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000722
723
showardf8b19042009-05-12 17:22:49 +0000724 def _get_attribute_model_and_args(self, attribute):
725 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000726
727
Fang Dengff361592015-02-02 15:27:34 -0800728 @classmethod
729 def get_attribute_model(cls):
730 """Return the attribute model.
731
732 Override method in parent class. See ModelExtensions for details.
733 @returns: The attribute model of Host.
734 """
735 return HostAttribute
736
737
jadmanski0afbb632008-06-06 21:10:57 +0000738 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800739 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000740 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000741
Fang Dengff361592015-02-02 15:27:34 -0800742
showarda5288b42009-07-28 20:06:08 +0000743 def __unicode__(self):
744 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000745
746
MK Ryuacf35922014-10-03 14:56:49 -0700747class HostAttribute(dbmodels.Model, model_logic.ModelExtensions):
showard0957a842009-05-11 19:25:08 +0000748 """Arbitrary keyvals associated with hosts."""
Fang Deng86248502014-12-18 16:38:00 -0800749
750 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Fang Dengff361592015-02-02 15:27:34 -0800751 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
showard0957a842009-05-11 19:25:08 +0000752 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000753 attribute = dbmodels.CharField(max_length=90)
754 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000755
756 objects = model_logic.ExtendedManager()
757
758 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800759 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000760 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000761
762
Fang Dengff361592015-02-02 15:27:34 -0800763 @classmethod
764 def get_record(cls, data):
765 """Check the database for an identical record.
766
767 Use host_id and attribute to search for a existing record.
768
769 @raises: DoesNotExist, if no record found
770 @raises: MultipleObjectsReturned if multiple records found.
771 """
772 # TODO(fdeng): We should use host_id and attribute together as
773 # a primary key in the db.
774 return cls.objects.get(host_id=data['host_id'],
775 attribute=data['attribute'])
776
777
778 @classmethod
779 def deserialize(cls, data):
780 """Override deserialize in parent class.
781
782 Do not deserialize id as id is not kept consistent on master and shards.
783
784 @param data: A dictionary of data to deserialize.
785
786 @returns: A HostAttribute object.
787 """
788 if data:
789 data.pop('id')
790 return super(HostAttribute, cls).deserialize(data)
791
792
showard7c785282008-05-29 19:45:12 +0000793class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000794 """\
795 Required:
showard909c7a62008-07-15 21:52:38 +0000796 author: author name
797 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000798 name: test name
showard909c7a62008-07-15 21:52:38 +0000799 time: short, medium, long
800 test_class: This describes the class for your the test belongs in.
801 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000802 test_type: Client or Server
803 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000804 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
805 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000806 i.e. if sync_count = 2 it is a sync job that requires two
807 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000808 Optional:
showard909c7a62008-07-15 21:52:38 +0000809 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000810 dependency_labels: many-to-many relationship with labels corresponding to
811 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000812 experimental: If this is set to True production servers will ignore the test
813 run_verify: Whether or not the scheduler should run the verify stage
Dan Shi07e09af2013-04-12 09:31:29 -0700814 run_reset: Whether or not the scheduler should run the reset stage
Aviv Keshet9af96d32013-03-05 12:56:24 -0800815 test_retry: Number of times to retry test if the test did not complete
816 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +0000817 """
showard909c7a62008-07-15 21:52:38 +0000818 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000819
showarda5288b42009-07-28 20:06:08 +0000820 name = dbmodels.CharField(max_length=255, unique=True)
821 author = dbmodels.CharField(max_length=255)
822 test_class = dbmodels.CharField(max_length=255)
823 test_category = dbmodels.CharField(max_length=255)
824 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000825 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000826 experimental = dbmodels.BooleanField(default=True)
Dan Shi07e09af2013-04-12 09:31:29 -0700827 run_verify = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +0000828 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
829 default=TestTime.MEDIUM)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700830 test_type = dbmodels.SmallIntegerField(
831 choices=control_data.CONTROL_TYPE.choices())
showard909c7a62008-07-15 21:52:38 +0000832 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000833 path = dbmodels.CharField(max_length=255, unique=True)
Aviv Keshet9af96d32013-03-05 12:56:24 -0800834 test_retry = dbmodels.IntegerField(blank=True, default=0)
Dan Shi07e09af2013-04-12 09:31:29 -0700835 run_reset = dbmodels.BooleanField(default=True)
mblighe8819cd2008-02-15 16:48:40 +0000836
showardeab66ce2009-12-23 00:03:56 +0000837 dependency_labels = (
838 dbmodels.ManyToManyField(Label, blank=True,
839 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000840 name_field = 'name'
841 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000842
843
jamesren35a70222010-02-16 19:30:46 +0000844 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800845 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +0000846 escaped_description = saxutils.escape(self.description)
847 return '<span style="white-space:pre">%s</span>' % escaped_description
848 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000849 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000850
851
jadmanski0afbb632008-06-06 21:10:57 +0000852 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800853 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +0000854 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000855
showarda5288b42009-07-28 20:06:08 +0000856 def __unicode__(self):
857 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000858
859
jamesren4a41e012010-07-16 22:33:48 +0000860class TestParameter(dbmodels.Model):
861 """
862 A declared parameter of a test
863 """
864 test = dbmodels.ForeignKey(Test)
865 name = dbmodels.CharField(max_length=255)
866
867 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800868 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000869 db_table = 'afe_test_parameters'
870 unique_together = ('test', 'name')
871
872 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -0700873 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +0000874
875
showard2b9a88b2008-06-13 20:55:03 +0000876class Profiler(dbmodels.Model, model_logic.ModelExtensions):
877 """\
878 Required:
879 name: profiler name
880 test_type: Client or Server
881
882 Optional:
883 description: arbirary text description
884 """
showarda5288b42009-07-28 20:06:08 +0000885 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000886 description = dbmodels.TextField(blank=True)
887
888 name_field = 'name'
889 objects = model_logic.ExtendedManager()
890
891
892 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800893 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +0000894 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000895
showarda5288b42009-07-28 20:06:08 +0000896 def __unicode__(self):
897 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000898
899
showard7c785282008-05-29 19:45:12 +0000900class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000901 """\
902 Required:
903 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000904
jadmanski0afbb632008-06-06 21:10:57 +0000905 Optional:
906 description: arbitrary description of group
907 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700908
909 SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
910
showarda5288b42009-07-28 20:06:08 +0000911 name = dbmodels.CharField(max_length=255, unique=True)
912 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000913 users = dbmodels.ManyToManyField(User, blank=False,
914 db_table='afe_acl_groups_users')
915 hosts = dbmodels.ManyToManyField(Host, blank=True,
916 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000917
jadmanski0afbb632008-06-06 21:10:57 +0000918 name_field = 'name'
919 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000920
showard08f981b2008-06-24 21:59:03 +0000921 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000922 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800923 """Verify the current user has access to the specified hosts.
924
925 @param hosts: The hosts to verify against.
926 @raises AclAccessViolation if the current user doesn't have access
927 to a host.
928 """
showard64a95952010-01-13 21:27:16 +0000929 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000930 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000931 return
showard3dd47c22008-07-10 00:41:36 +0000932 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000933 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000934 for host in hosts:
935 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800936 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +0000937 no_access = (isinstance(host, Host)
938 and not host.invalid
939 and int(host.id) not in accessible_host_ids)
940 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000941 raise AclAccessViolation("%s does not have access to %s" %
942 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000943
showard9dbdcda2008-10-14 17:34:36 +0000944
945 @staticmethod
showarddc817512008-11-12 18:16:41 +0000946 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800947 """Look for queue entries that aren't abortable by the current user.
948
949 An entry is not abortable if:
950 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +0000951 * the machine isn't ACL-accessible, or
952 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800953
954 @param queue_entries: The queue entries to check.
955 @raises AclAccessViolation if a queue entry is not abortable by the
956 current user.
showarddc817512008-11-12 18:16:41 +0000957 """
showard64a95952010-01-13 21:27:16 +0000958 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000959 if user.is_superuser():
960 return
showarddc817512008-11-12 18:16:41 +0000961 not_owned = queue_entries.exclude(job__owner=user.login)
962 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000963 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
964 # 1.0.
965 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000966 accessible_ids = set(
967 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000968 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000969 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000970 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000971 cannot_abort = [entry for entry in not_owned.select_related()
972 if entry.id not in accessible_ids
973 or entry.id in public_ids]
974 if len(cannot_abort) == 0:
975 return
976 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000977 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000978 for entry in cannot_abort)
979 raise AclAccessViolation('You cannot abort the following job entries: '
980 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000981
982
showard3dd47c22008-07-10 00:41:36 +0000983 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800984 """Verifies the current user has acces to this ACL group.
985
986 @raises AclAccessViolation if the current user doesn't have access to
987 this ACL group.
988 """
showard64a95952010-01-13 21:27:16 +0000989 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000990 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000991 return
992 if self.name == 'Everyone':
993 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000994 if not user in self.users.all():
995 raise AclAccessViolation("You do not have access to %s"
996 % self.name)
997
998 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000999 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001000 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +00001001 everyone = AclGroup.objects.get(name='Everyone')
1002
showard3dd47c22008-07-10 00:41:36 +00001003 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +00001004 # TODO(showard): this is a bit of a hack, since the fact that this query
1005 # works is kind of a coincidence of Django internals. This trick
1006 # doesn't work in general (on all foreign key relationships). I'll
1007 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +00001008 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +00001009 everyone.hosts.add(*orphaned_hosts.distinct())
1010
1011 # find hosts in both Everyone and another ACL group, and remove them
1012 # from Everyone
showarda5288b42009-07-28 20:06:08 +00001013 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
1014 acled_hosts = set()
1015 for host in hosts_in_everyone:
1016 # Has an ACL group other than Everyone
1017 if host.aclgroup_set.count() > 1:
1018 acled_hosts.add(host)
1019 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +00001020
1021
1022 def delete(self):
showard3dd47c22008-07-10 00:41:36 +00001023 if (self.name == 'Everyone'):
1024 raise AclAccessViolation("You cannot delete 'Everyone'!")
1025 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +00001026 super(AclGroup, self).delete()
1027 self.on_host_membership_change()
1028
1029
showard04f2cd82008-07-25 20:53:31 +00001030 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001031 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +00001032 if not self.users.count():
showard64a95952010-01-13 21:27:16 +00001033 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +00001034
1035
showard8cbaf1e2009-09-08 16:27:04 +00001036 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001037 """Called after a save.
1038
1039 @param change: Whether there was a change.
1040 """
showard8cbaf1e2009-09-08 16:27:04 +00001041 if not change:
showard64a95952010-01-13 21:27:16 +00001042 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +00001043 self.add_current_user_if_empty()
1044 self.on_host_membership_change()
1045
1046
1047 def save(self, *args, **kwargs):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001048 change = bool(self.id)
1049 if change:
showard8cbaf1e2009-09-08 16:27:04 +00001050 # Check the original object for an ACL violation
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001051 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
showard8cbaf1e2009-09-08 16:27:04 +00001052 super(AclGroup, self).save(*args, **kwargs)
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001053 self.perform_after_save(change)
showard8cbaf1e2009-09-08 16:27:04 +00001054
showardeb3be4d2008-04-21 20:59:26 +00001055
jadmanski0afbb632008-06-06 21:10:57 +00001056 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001057 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +00001058 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +00001059
showarda5288b42009-07-28 20:06:08 +00001060 def __unicode__(self):
1061 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +00001062
mblighe8819cd2008-02-15 16:48:40 +00001063
Shuqian Zhao54a5b672016-05-11 22:12:17 +00001064class Kernel(dbmodels.Model):
1065 """
1066 A kernel configuration for a parameterized job
1067 """
1068 version = dbmodels.CharField(max_length=255)
1069 cmdline = dbmodels.CharField(max_length=255, blank=True)
1070
1071 @classmethod
1072 def create_kernels(cls, kernel_list):
1073 """Creates all kernels in the kernel list.
1074
1075 @param cls: Implicit class object.
1076 @param kernel_list: A list of dictionaries that describe the kernels,
1077 in the same format as the 'kernel' argument to
1078 rpc_interface.generate_control_file.
1079 @return A list of the created kernels.
1080 """
1081 if not kernel_list:
1082 return None
1083 return [cls._create(kernel) for kernel in kernel_list]
1084
1085
1086 @classmethod
1087 def _create(cls, kernel_dict):
1088 version = kernel_dict.pop('version')
1089 cmdline = kernel_dict.pop('cmdline', '')
1090
1091 if kernel_dict:
1092 raise Exception('Extraneous kernel arguments remain: %r'
1093 % kernel_dict)
1094
1095 kernel, _ = cls.objects.get_or_create(version=version,
1096 cmdline=cmdline)
1097 return kernel
1098
1099
1100 class Meta:
1101 """Metadata for class Kernel."""
1102 db_table = 'afe_kernels'
1103 unique_together = ('version', 'cmdline')
1104
1105 def __unicode__(self):
1106 return u'%s %s' % (self.version, self.cmdline)
1107
1108
jamesren4a41e012010-07-16 22:33:48 +00001109class ParameterizedJob(dbmodels.Model):
1110 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001111 Auxiliary configuration for a parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001112 """
1113 test = dbmodels.ForeignKey(Test)
1114 label = dbmodels.ForeignKey(Label, null=True)
1115 use_container = dbmodels.BooleanField(default=False)
1116 profile_only = dbmodels.BooleanField(default=False)
1117 upload_kernel_config = dbmodels.BooleanField(default=False)
1118
Shuqian Zhao54a5b672016-05-11 22:12:17 +00001119 kernels = dbmodels.ManyToManyField(
1120 Kernel, db_table='afe_parameterized_job_kernels')
jamesren4a41e012010-07-16 22:33:48 +00001121 profilers = dbmodels.ManyToManyField(
1122 Profiler, through='ParameterizedJobProfiler')
1123
1124
1125 @classmethod
1126 def smart_get(cls, id_or_name, *args, **kwargs):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001127 """For compatibility with Job.add_object.
1128
1129 @param cls: Implicit class object.
1130 @param id_or_name: The ID or name to get.
1131 @param args: Non-keyword arguments.
1132 @param kwargs: Keyword arguments.
1133 """
jamesren4a41e012010-07-16 22:33:48 +00001134 return cls.objects.get(pk=id_or_name)
1135
1136
1137 def job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001138 """Returns the job if it exists, or else None."""
jamesren4a41e012010-07-16 22:33:48 +00001139 jobs = self.job_set.all()
1140 assert jobs.count() <= 1
1141 return jobs and jobs[0] or None
1142
1143
1144 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001145 """Metadata for class ParameterizedJob."""
jamesren4a41e012010-07-16 22:33:48 +00001146 db_table = 'afe_parameterized_jobs'
1147
1148 def __unicode__(self):
1149 return u'%s (parameterized) - %s' % (self.test.name, self.job())
1150
1151
1152class ParameterizedJobProfiler(dbmodels.Model):
1153 """
1154 A profiler to run on a parameterized job
1155 """
1156 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1157 profiler = dbmodels.ForeignKey(Profiler)
1158
1159 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001160 """Metedata for class ParameterizedJobProfiler."""
jamesren4a41e012010-07-16 22:33:48 +00001161 db_table = 'afe_parameterized_jobs_profilers'
1162 unique_together = ('parameterized_job', 'profiler')
1163
1164
Shuqian Zhao54a5b672016-05-11 22:12:17 +00001165class ParameterizedJobProfilerParameter(dbmodels.Model):
1166 """
1167 A parameter for a profiler in a parameterized job
1168 """
1169 parameterized_job_profiler = dbmodels.ForeignKey(ParameterizedJobProfiler)
1170 parameter_name = dbmodels.CharField(max_length=255)
1171 parameter_value = dbmodels.TextField()
1172 parameter_type = dbmodels.CharField(
1173 max_length=8, choices=model_attributes.ParameterTypes.choices())
1174
1175 class Meta:
1176 """Metadata for class ParameterizedJobProfilerParameter."""
1177 db_table = 'afe_parameterized_job_profiler_parameters'
1178 unique_together = ('parameterized_job_profiler', 'parameter_name')
1179
1180 def __unicode__(self):
1181 return u'%s - %s' % (self.parameterized_job_profiler.profiler.name,
1182 self.parameter_name)
1183
1184
1185class ParameterizedJobParameter(dbmodels.Model):
1186 """
1187 Parameters for a parameterized job
1188 """
1189 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1190 test_parameter = dbmodels.ForeignKey(TestParameter)
1191 parameter_value = dbmodels.TextField()
1192 parameter_type = dbmodels.CharField(
1193 max_length=8, choices=model_attributes.ParameterTypes.choices())
1194
1195 class Meta:
1196 """Metadata for class ParameterizedJobParameter."""
1197 db_table = 'afe_parameterized_job_parameters'
1198 unique_together = ('parameterized_job', 'test_parameter')
1199
1200 def __unicode__(self):
1201 return u'%s - %s' % (self.parameterized_job.job().name,
1202 self.test_parameter.name)
1203
1204
showard7c785282008-05-29 19:45:12 +00001205class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +00001206 'Custom manager to provide efficient status counts querying.'
1207 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001208 """Returns a dict mapping the given job IDs to their status count dicts.
1209
1210 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +00001211 """
1212 if not job_ids:
1213 return {}
1214 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1215 cursor = connection.cursor()
1216 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +00001217 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +00001218 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +00001219 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +00001220 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +00001221 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +00001222 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +00001223 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +00001224 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +00001225 full_status = HostQueueEntry.compute_full_status(status, aborted,
1226 complete)
showardb6d16622009-05-26 19:35:29 +00001227 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +00001228 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +00001229 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +00001230
1231
showard7c785282008-05-29 19:45:12 +00001232class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001233 """\
1234 owner: username of job owner
1235 name: job name (does not have to be unique)
Alex Miller7d658cf2013-09-04 16:00:35 -07001236 priority: Integer priority value. Higher is more important.
jadmanski0afbb632008-06-06 21:10:57 +00001237 control_file: contents of control file
1238 control_type: Client or Server
1239 created_on: date of job creation
1240 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +00001241 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +00001242 run_verify: Whether or not to run the verify phase
Dan Shi07e09af2013-04-12 09:31:29 -07001243 run_reset: Whether or not to run the reset phase
Simran Basi94dc0032013-11-12 14:09:46 -08001244 timeout: DEPRECATED - hours from queuing time until job times out
1245 timeout_mins: minutes from job queuing time until the job times out
Simran Basi34217022012-11-06 13:43:15 -08001246 max_runtime_hrs: DEPRECATED - hours from job starting time until job
1247 times out
1248 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +00001249 email_list: list of people to email on completion delimited by any of:
1250 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +00001251 dependency_labels: many-to-many relationship with labels corresponding to
1252 job dependencies
showard21baa452008-10-21 00:08:39 +00001253 reboot_before: Never, If dirty, or Always
1254 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001255 parse_failed_repair: if True, a failed repair launched by this job will have
1256 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001257 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001258 parent_job: Parent job (optional)
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001259 test_retry: Number of times to retry test if the test did not complete
1260 successfully. (optional, default: 0)
Dan Shic9e17142015-02-19 11:50:55 -08001261 require_ssp: Require server-side packaging unless require_ssp is set to
1262 False. (optional, default: None)
jadmanski0afbb632008-06-06 21:10:57 +00001263 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001264
1265 # TODO: Investigate, if jobkeyval_set is really needed.
1266 # dynamic_suite will write them into an attached file for the drone, but
1267 # it doesn't seem like they are actually used. If they aren't used, remove
1268 # jobkeyval_set here.
1269 SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1270 'hostqueueentry_set',
1271 'jobkeyval_set',
1272 'shard'])
1273
Fang Deng2b79fd72015-05-20 19:11:36 -07001274 # SQL for selecting jobs that should be sent to shard.
1275 # We use raw sql as django filters were not optimized.
1276 # The following jobs are excluded by the SQL.
1277 # - Non-aborted jobs known to shard as specified in |known_ids|.
1278 # Note for jobs aborted on master, even if already known to shard,
1279 # will be sent to shard again so that shard can abort them.
1280 # - Completed jobs
1281 # - Active jobs
1282 # - Jobs without host_queue_entries
1283 NON_ABORTED_KNOWN_JOBS = '(t2.aborted = 0 AND t1.id IN (%(known_ids)s))'
1284
1285 SQL_SHARD_JOBS = (
MK Ryu5c7ef302015-07-10 17:23:08 -07001286 'SELECT DISTINCT(t1.id) FROM afe_jobs t1 '
Fang Deng2b79fd72015-05-20 19:11:36 -07001287 'INNER JOIN afe_host_queue_entries t2 ON '
MK Ryu5c7ef302015-07-10 17:23:08 -07001288 ' (t1.id = t2.job_id AND t2.complete != 1 AND t2.active != 1 '
Fang Deng2b79fd72015-05-20 19:11:36 -07001289 ' %(check_known_jobs)s) '
MK Ryu5c7ef302015-07-10 17:23:08 -07001290 'LEFT OUTER JOIN afe_jobs_dependency_labels t3 ON (t1.id = t3.job_id) '
Dan Shi6b89f1c2016-09-17 22:25:53 -07001291 'JOIN afe_shards_labels t4 '
1292 ' ON (t4.label_id = t3.label_id OR t4.label_id = t2.meta_host) '
1293 'WHERE t4.shard_id = %(shard_id)s'
Fang Deng2b79fd72015-05-20 19:11:36 -07001294 )
1295
MK Ryu5c7ef302015-07-10 17:23:08 -07001296 # Jobs can be created with assigned hosts and have no dependency
1297 # labels nor meta_host.
Fang Deng2b79fd72015-05-20 19:11:36 -07001298 # We are looking for:
MK Ryu5c7ef302015-07-10 17:23:08 -07001299 # - a job whose hqe's meta_host is null
Fang Deng2b79fd72015-05-20 19:11:36 -07001300 # - a job whose hqe has a host
1301 # - one of the host's labels matches the shard's label.
1302 # Non-aborted known jobs, completed jobs, active jobs, jobs
MK Ryu5c7ef302015-07-10 17:23:08 -07001303 # without hqe are exluded as we do with SQL_SHARD_JOBS.
1304 SQL_SHARD_JOBS_WITH_HOSTS = (
1305 'SELECT DISTINCT(t1.id) FROM afe_jobs t1 '
Fang Deng2b79fd72015-05-20 19:11:36 -07001306 'INNER JOIN afe_host_queue_entries t2 ON '
1307 ' (t1.id = t2.job_id AND t2.complete != 1 AND t2.active != 1 '
1308 ' AND t2.meta_host IS NULL AND t2.host_id IS NOT NULL '
1309 ' %(check_known_jobs)s) '
1310 'LEFT OUTER JOIN afe_hosts_labels t3 ON (t2.host_id = t3.host_id) '
1311 'WHERE (t3.label_id IN '
1312 ' (SELECT label_id FROM afe_shards_labels '
1313 ' WHERE shard_id = %(shard_id)s))'
1314 )
MK Ryu5c7ef302015-07-10 17:23:08 -07001315
Fang Deng2b79fd72015-05-20 19:11:36 -07001316 # Even if we had filters about complete, active and aborted
MK Ryu5c7ef302015-07-10 17:23:08 -07001317 # bits in the above two SQLs, there is a chance that
Fang Deng2b79fd72015-05-20 19:11:36 -07001318 # the result may still contain a job with an hqe with 'complete=1'
1319 # or 'active=1' or 'aborted=0 and afe_job.id in known jobs.'
1320 # This happens when a job has two (or more) hqes and at least
1321 # one hqe has different bits than others.
1322 # We use a second sql to ensure we exclude all un-desired jobs.
1323 SQL_JOBS_TO_EXCLUDE =(
1324 'SELECT t1.id FROM afe_jobs t1 '
1325 'INNER JOIN afe_host_queue_entries t2 ON '
1326 ' (t1.id = t2.job_id) '
1327 'WHERE (t1.id in (%(candidates)s) '
1328 ' AND (t2.complete=1 OR t2.active=1 '
1329 ' %(check_known_jobs)s))'
1330 )
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001331
Jakob Juelichf88fa932014-09-03 17:58:04 -07001332 def _deserialize_relation(self, link, data):
1333 if link in ['hostqueueentry_set', 'jobkeyval_set']:
1334 for obj in data:
1335 obj['job_id'] = self.id
1336
1337 super(Job, self)._deserialize_relation(link, data)
1338
1339
1340 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001341 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -07001342 self.shard = Shard.deserialize(data)
1343
1344
Jakob Juelicha94efe62014-09-18 16:02:49 -07001345 def sanity_check_update_from_shard(self, shard, updated_serialized):
Jakob Juelich02e61292014-10-17 12:36:55 -07001346 # If the job got aborted on the master after the client fetched it
1347 # no shard_id will be set. The shard might still push updates though,
1348 # as the job might complete before the abort bit syncs to the shard.
1349 # Alternative considered: The master scheduler could be changed to not
1350 # set aborted jobs to completed that are sharded out. But that would
1351 # require database queries and seemed more complicated to implement.
1352 # This seems safe to do, as there won't be updates pushed from the wrong
1353 # shards should be powered off and wiped hen they are removed from the
1354 # master.
1355 if self.shard_id and self.shard_id != shard.id:
Jakob Juelicha94efe62014-09-18 16:02:49 -07001356 raise error.UnallowedRecordsSentToMaster(
1357 'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1358 'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1359 shard.id))
1360
1361
Simran Basi94dc0032013-11-12 14:09:46 -08001362 # TIMEOUT is deprecated.
showardb1e51872008-10-07 11:08:18 +00001363 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
Simran Basi94dc0032013-11-12 14:09:46 -08001364 'AUTOTEST_WEB', 'job_timeout_default', default=24)
1365 DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1366 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
Simran Basi34217022012-11-06 13:43:15 -08001367 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1368 # completed.
showard12f3e322009-05-13 21:27:42 +00001369 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1370 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001371 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1372 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001373 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1374 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1375 default=False)
showardb1e51872008-10-07 11:08:18 +00001376
showarda5288b42009-07-28 20:06:08 +00001377 owner = dbmodels.CharField(max_length=255)
1378 name = dbmodels.CharField(max_length=255)
Alex Miller7d658cf2013-09-04 16:00:35 -07001379 priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
jamesren4a41e012010-07-16 22:33:48 +00001380 control_file = dbmodels.TextField(null=True, blank=True)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001381 control_type = dbmodels.SmallIntegerField(
1382 choices=control_data.CONTROL_TYPE.choices(),
1383 blank=True, # to allow 0
1384 default=control_data.CONTROL_TYPE.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001385 created_on = dbmodels.DateTimeField()
Simran Basi8d89b642014-05-02 17:33:20 -07001386 synch_count = dbmodels.IntegerField(blank=True, default=0)
showardb1e51872008-10-07 11:08:18 +00001387 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
Dan Shi07e09af2013-04-12 09:31:29 -07001388 run_verify = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001389 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001390 dependency_labels = (
1391 dbmodels.ManyToManyField(Label, blank=True,
1392 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001393 reboot_before = dbmodels.SmallIntegerField(
1394 choices=model_attributes.RebootBefore.choices(), blank=True,
1395 default=DEFAULT_REBOOT_BEFORE)
1396 reboot_after = dbmodels.SmallIntegerField(
1397 choices=model_attributes.RebootAfter.choices(), blank=True,
1398 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001399 parse_failed_repair = dbmodels.BooleanField(
1400 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001401 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1402 # completed.
showard12f3e322009-05-13 21:27:42 +00001403 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001404 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001405 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001406
jamesren4a41e012010-07-16 22:33:48 +00001407 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1408 blank=True)
1409
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001410 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001411
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001412 test_retry = dbmodels.IntegerField(blank=True, default=0)
1413
Dan Shi07e09af2013-04-12 09:31:29 -07001414 run_reset = dbmodels.BooleanField(default=True)
1415
Simran Basi94dc0032013-11-12 14:09:46 -08001416 timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1417
Jakob Juelich8421d592014-09-17 15:27:06 -07001418 # If this is None on the master, a slave should be found.
1419 # If this is None on a slave, it should be synced back to the master
Jakob Jülich92c06332014-08-25 19:06:57 +00001420 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1421
Dan Shic9e17142015-02-19 11:50:55 -08001422 # If this is None, server-side packaging will be used for server side test,
1423 # unless it's disabled in global config AUTOSERV/enable_ssp_container.
1424 require_ssp = dbmodels.NullBooleanField(default=None, blank=True, null=True)
1425
jadmanski0afbb632008-06-06 21:10:57 +00001426 # custom manager
1427 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001428
1429
Alex Millerec212252014-02-28 16:48:34 -08001430 @decorators.cached_property
1431 def labels(self):
1432 """All the labels of this job"""
1433 # We need to convert dependency_labels to a list, because all() gives us
1434 # back an iterator, and storing/caching an iterator means we'd only be
1435 # able to read from it once.
1436 return list(self.dependency_labels.all())
1437
1438
jadmanski0afbb632008-06-06 21:10:57 +00001439 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001440 """Returns whether this job is of type server."""
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001441 return self.control_type == control_data.CONTROL_TYPE.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001442
1443
jadmanski0afbb632008-06-06 21:10:57 +00001444 @classmethod
jamesren4a41e012010-07-16 22:33:48 +00001445 def parameterized_jobs_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001446 """Returns whether parameterized jobs are enabled.
1447
1448 @param cls: Implicit class object.
1449 """
jamesren4a41e012010-07-16 22:33:48 +00001450 return global_config.global_config.get_config_value(
1451 'AUTOTEST_WEB', 'parameterized_jobs', type=bool)
1452
1453
1454 @classmethod
1455 def check_parameterized_job(cls, control_file, parameterized_job):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001456 """Checks that the job is valid given the global config settings.
jamesren4a41e012010-07-16 22:33:48 +00001457
1458 First, either control_file must be set, or parameterized_job must be
1459 set, but not both. Second, parameterized_job must be set if and only if
1460 the parameterized_jobs option in the global config is set to True.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001461
1462 @param cls: Implict class object.
1463 @param control_file: A control file.
1464 @param parameterized_job: A parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001465 """
1466 if not (bool(control_file) ^ bool(parameterized_job)):
1467 raise Exception('Job must have either control file or '
1468 'parameterization, but not both')
1469
1470 parameterized_jobs_enabled = cls.parameterized_jobs_enabled()
1471 if control_file and parameterized_jobs_enabled:
1472 raise Exception('Control file specified, but parameterized jobs '
1473 'are enabled')
1474 if parameterized_job and not parameterized_jobs_enabled:
1475 raise Exception('Parameterized job specified, but parameterized '
1476 'jobs are not enabled')
1477
1478
1479 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001480 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001481 """Creates a job.
1482
1483 The job is created by taking some information (the listed args) and
1484 filling in the rest of the necessary information.
1485
1486 @param cls: Implicit class object.
1487 @param owner: The owner for the job.
1488 @param options: An options object.
1489 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001490 """
showard3dd47c22008-07-10 00:41:36 +00001491 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001492
jamesren4a41e012010-07-16 22:33:48 +00001493 control_file = options.get('control_file')
1494 parameterized_job = options.get('parameterized_job')
jamesren4a41e012010-07-16 22:33:48 +00001495
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001496 # The current implementation of parameterized jobs requires that only
1497 # control files or parameterized jobs are used. Using the image
1498 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1499 # parameterized jobs and control files jobs, it does muck enough with
1500 # normal jobs by adding a parameterized id to them that this check will
1501 # fail. So for now we just skip this check.
1502 # cls.check_parameterized_job(control_file=control_file,
1503 # parameterized_job=parameterized_job)
showard4d233752010-01-20 19:06:40 +00001504 user = User.current_user()
1505 if options.get('reboot_before') is None:
1506 options['reboot_before'] = user.get_reboot_before_display()
1507 if options.get('reboot_after') is None:
1508 options['reboot_after'] = user.get_reboot_after_display()
1509
jamesren76fcf192010-04-21 20:39:50 +00001510 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1511
Simran Basi94dc0032013-11-12 14:09:46 -08001512 if options.get('timeout_mins') is None and options.get('timeout'):
1513 options['timeout_mins'] = options['timeout'] * 60
1514
jadmanski0afbb632008-06-06 21:10:57 +00001515 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001516 owner=owner,
1517 name=options['name'],
1518 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001519 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001520 control_type=options['control_type'],
1521 synch_count=options.get('synch_count'),
Simran Basi94dc0032013-11-12 14:09:46 -08001522 # timeout needs to be deleted in the future.
showarda1e74b32009-05-12 17:32:04 +00001523 timeout=options.get('timeout'),
Simran Basi94dc0032013-11-12 14:09:46 -08001524 timeout_mins=options.get('timeout_mins'),
Simran Basi34217022012-11-06 13:43:15 -08001525 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001526 run_verify=options.get('run_verify'),
1527 email_list=options.get('email_list'),
1528 reboot_before=options.get('reboot_before'),
1529 reboot_after=options.get('reboot_after'),
1530 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001531 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001532 drone_set=drone_set,
Aviv Keshet18308922013-02-19 17:49:49 -08001533 parameterized_job=parameterized_job,
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001534 parent_job=options.get('parent_job_id'),
Dan Shi07e09af2013-04-12 09:31:29 -07001535 test_retry=options.get('test_retry'),
Dan Shic9e17142015-02-19 11:50:55 -08001536 run_reset=options.get('run_reset'),
1537 require_ssp=options.get('require_ssp'))
mblighe8819cd2008-02-15 16:48:40 +00001538
showarda1e74b32009-05-12 17:32:04 +00001539 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001540
jamesrend8b6e172010-04-16 23:45:00 +00001541 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001542 for key, value in options['keyvals'].iteritems():
1543 JobKeyval.objects.create(job=job, key=key, value=value)
1544
jadmanski0afbb632008-06-06 21:10:57 +00001545 return job
mblighe8819cd2008-02-15 16:48:40 +00001546
1547
Jakob Juelich59cfe542014-09-02 16:37:46 -07001548 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -07001549 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -07001550 """Assigns unassigned jobs to a shard.
1551
Jakob Juelich1b525742014-09-30 13:08:07 -07001552 For all labels that have been assigned to this shard, all jobs that
1553 have this label, are assigned to this shard.
1554
1555 Jobs that are assigned to the shard but aren't already present on the
1556 shard are returned.
1557
Jakob Juelich59cfe542014-09-02 16:37:46 -07001558 @param shard: The shard to assign jobs to.
Jakob Juelich1b525742014-09-30 13:08:07 -07001559 @param known_ids: List of all ids of incomplete jobs, the shard already
1560 knows about.
1561 This is used to figure out which jobs should be sent
1562 to the shard. If shard_ids were used instead, jobs
1563 would only be transferred once, even if the client
1564 failed persisting them.
1565 The number of unfinished jobs usually lies in O(1000).
1566 Assuming one id takes 8 chars in the json, this means
1567 overhead that lies in the lower kilobyte range.
1568 A not in query with 5000 id's takes about 30ms.
1569
Jakob Juelich59cfe542014-09-02 16:37:46 -07001570 @returns The job objects that should be sent to the shard.
1571 """
1572 # Disclaimer: Concurrent heartbeats should not occur in today's setup.
1573 # If this changes or they are triggered manually, this applies:
1574 # Jobs may be returned more than once by concurrent calls of this
1575 # function, as there is a race condition between SELECT and UPDATE.
Fang Deng2b79fd72015-05-20 19:11:36 -07001576 job_ids = set([])
1577 check_known_jobs_exclude = ''
1578 check_known_jobs_include = ''
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001579
Fang Deng2b79fd72015-05-20 19:11:36 -07001580 if known_ids:
1581 check_known_jobs = (
1582 cls.NON_ABORTED_KNOWN_JOBS %
1583 {'known_ids': ','.join([str(i) for i in known_ids])})
1584 check_known_jobs_exclude = 'AND NOT ' + check_known_jobs
1585 check_known_jobs_include = 'OR ' + check_known_jobs
1586
MK Ryu5c7ef302015-07-10 17:23:08 -07001587 for sql in [cls.SQL_SHARD_JOBS, cls.SQL_SHARD_JOBS_WITH_HOSTS]:
MK Ryu2e3e6052015-07-09 16:15:18 -07001588 query = Job.objects.raw(sql % {
1589 'check_known_jobs': check_known_jobs_exclude,
1590 'shard_id': shard.id})
Fang Deng2b79fd72015-05-20 19:11:36 -07001591 job_ids |= set([j.id for j in query])
1592
1593 if job_ids:
1594 query = Job.objects.raw(
1595 cls.SQL_JOBS_TO_EXCLUDE %
1596 {'check_known_jobs': check_known_jobs_include,
1597 'candidates': ','.join([str(i) for i in job_ids])})
1598 job_ids -= set([j.id for j in query])
1599
Jakob Juelich59cfe542014-09-02 16:37:46 -07001600 if job_ids:
1601 Job.objects.filter(pk__in=job_ids).update(shard=shard)
1602 return list(Job.objects.filter(pk__in=job_ids).all())
1603 return []
1604
1605
jamesren4a41e012010-07-16 22:33:48 +00001606 def save(self, *args, **kwargs):
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001607 # The current implementation of parameterized jobs requires that only
1608 # control files or parameterized jobs are used. Using the image
1609 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1610 # parameterized jobs and control files jobs, it does muck enough with
1611 # normal jobs by adding a parameterized id to them that this check will
1612 # fail. So for now we just skip this check.
1613 # cls.check_parameterized_job(control_file=self.control_file,
1614 # parameterized_job=self.parameterized_job)
jamesren4a41e012010-07-16 22:33:48 +00001615 super(Job, self).save(*args, **kwargs)
1616
1617
showard29f7cd22009-04-29 21:16:24 +00001618 def queue(self, hosts, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001619 """Enqueue a job on the given hosts.
1620
1621 @param hosts: The hosts to use.
1622 @param atomic_group: The associated atomic group.
1623 @param is_template: Whether the status should be "Template".
1624 """
showarda9545c02009-12-18 22:44:26 +00001625 if not hosts:
1626 if atomic_group:
1627 # No hosts or labels are required to queue an atomic group
1628 # Job. However, if they are given, we respect them below.
1629 atomic_group.enqueue_job(self, is_template=is_template)
1630 else:
1631 # hostless job
1632 entry = HostQueueEntry.create(job=self, is_template=is_template)
1633 entry.save()
1634 return
1635
jadmanski0afbb632008-06-06 21:10:57 +00001636 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +00001637 host.enqueue_job(self, atomic_group=atomic_group,
1638 is_template=is_template)
1639
1640
jadmanski0afbb632008-06-06 21:10:57 +00001641 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001642 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001643 try:
1644 return User.objects.get(login=self.owner)
1645 except self.DoesNotExist:
1646 return None
mblighe8819cd2008-02-15 16:48:40 +00001647
1648
showard64a95952010-01-13 21:27:16 +00001649 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001650 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001651 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001652 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001653
1654
showardd1195652009-12-08 22:21:02 +00001655 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001656 """Returns a string tag for this job."""
MK Ryu0c1a37d2015-04-30 12:00:55 -07001657 return server_utils.get_job_tag(self.id, self.owner)
showardd1195652009-12-08 22:21:02 +00001658
1659
showardc1a98d12010-01-15 00:22:22 +00001660 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001661 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001662 return dict((keyval.key, keyval.value)
1663 for keyval in self.jobkeyval_set.all())
1664
1665
Fang Dengff361592015-02-02 15:27:34 -08001666 @classmethod
1667 def get_attribute_model(cls):
1668 """Return the attribute model.
1669
1670 Override method in parent class. This class is called when
1671 deserializing the one-to-many relationship betwen Job and JobKeyval.
1672 On deserialization, we will try to clear any existing job keyvals
1673 associated with a job to avoid any inconsistency.
1674 Though Job doesn't implement ModelWithAttribute, we still treat
1675 it as an attribute model for this purpose.
1676
1677 @returns: The attribute model of Job.
1678 """
1679 return JobKeyval
1680
1681
jadmanski0afbb632008-06-06 21:10:57 +00001682 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001683 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001684 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001685
showarda5288b42009-07-28 20:06:08 +00001686 def __unicode__(self):
1687 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001688
1689
showardc1a98d12010-01-15 00:22:22 +00001690class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1691 """Keyvals associated with jobs"""
Fang Dengff361592015-02-02 15:27:34 -08001692
1693 SERIALIZATION_LINKS_TO_KEEP = set(['job'])
1694 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
1695
showardc1a98d12010-01-15 00:22:22 +00001696 job = dbmodels.ForeignKey(Job)
1697 key = dbmodels.CharField(max_length=90)
1698 value = dbmodels.CharField(max_length=300)
1699
1700 objects = model_logic.ExtendedManager()
1701
Fang Dengff361592015-02-02 15:27:34 -08001702
1703 @classmethod
1704 def get_record(cls, data):
1705 """Check the database for an identical record.
1706
1707 Use job_id and key to search for a existing record.
1708
1709 @raises: DoesNotExist, if no record found
1710 @raises: MultipleObjectsReturned if multiple records found.
1711 """
1712 # TODO(fdeng): We should use job_id and key together as
1713 # a primary key in the db.
1714 return cls.objects.get(job_id=data['job_id'], key=data['key'])
1715
1716
1717 @classmethod
1718 def deserialize(cls, data):
1719 """Override deserialize in parent class.
1720
1721 Do not deserialize id as id is not kept consistent on master and shards.
1722
1723 @param data: A dictionary of data to deserialize.
1724
1725 @returns: A JobKeyval object.
1726 """
1727 if data:
1728 data.pop('id')
1729 return super(JobKeyval, cls).deserialize(data)
1730
1731
showardc1a98d12010-01-15 00:22:22 +00001732 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001733 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001734 db_table = 'afe_job_keyvals'
1735
1736
showard7c785282008-05-29 19:45:12 +00001737class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001738 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001739 job = dbmodels.ForeignKey(Job)
1740 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001741
jadmanski0afbb632008-06-06 21:10:57 +00001742 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001743
jadmanski0afbb632008-06-06 21:10:57 +00001744 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001745 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001746 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001747
mblighe8819cd2008-02-15 16:48:40 +00001748
showard7c785282008-05-29 19:45:12 +00001749class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001750 """Represents a host queue entry."""
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001751
1752 SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001753 SERIALIZATION_LINKS_TO_KEEP = set(['host'])
Jakob Juelichf865d332014-09-29 10:47:49 -07001754 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001755
Jakob Juelichf88fa932014-09-03 17:58:04 -07001756
1757 def custom_deserialize_relation(self, link, data):
1758 assert link == 'meta_host'
1759 self.meta_host = Label.deserialize(data)
1760
1761
Jakob Juelicha94efe62014-09-18 16:02:49 -07001762 def sanity_check_update_from_shard(self, shard, updated_serialized,
1763 job_ids_sent):
1764 if self.job_id not in job_ids_sent:
1765 raise error.UnallowedRecordsSentToMaster(
1766 'Sent HostQueueEntry without corresponding '
1767 'job entry: %s' % updated_serialized)
1768
1769
showardeaa408e2009-09-11 18:45:31 +00001770 Status = host_queue_entry_states.Status
1771 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001772 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
Laurence Goodby303d2662016-07-01 15:57:04 -07001773 PRE_JOB_STATUSES = host_queue_entry_states.PRE_JOB_STATUSES
1774 IDLE_PRE_JOB_STATUSES = host_queue_entry_states.IDLE_PRE_JOB_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001775
jadmanski0afbb632008-06-06 21:10:57 +00001776 job = dbmodels.ForeignKey(Job)
1777 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001778 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001779 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1780 db_column='meta_host')
1781 active = dbmodels.BooleanField(default=False)
1782 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001783 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001784 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1785 default='')
showard89f84db2009-03-12 20:39:13 +00001786 # If atomic_group is set, this is a virtual HostQueueEntry that will
1787 # be expanded into many actual hosts within the group at schedule time.
1788 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001789 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001790 started_on = dbmodels.DateTimeField(null=True, blank=True)
Fang Deng51599032014-06-23 17:24:27 -07001791 finished_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001792
jadmanski0afbb632008-06-06 21:10:57 +00001793 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001794
mblighe8819cd2008-02-15 16:48:40 +00001795
showard2bab8f42008-11-12 18:15:22 +00001796 def __init__(self, *args, **kwargs):
1797 super(HostQueueEntry, self).__init__(*args, **kwargs)
1798 self._record_attributes(['status'])
1799
1800
showard29f7cd22009-04-29 21:16:24 +00001801 @classmethod
1802 def create(cls, job, host=None, meta_host=None, atomic_group=None,
1803 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001804 """Creates a new host queue entry.
1805
1806 @param cls: Implicit class object.
1807 @param job: The associated job.
1808 @param host: The associated host.
1809 @param meta_host: The associated meta host.
1810 @param atomic_group: The associated atomic group.
1811 @param is_template: Whether the status should be "Template".
1812 """
showard29f7cd22009-04-29 21:16:24 +00001813 if is_template:
1814 status = cls.Status.TEMPLATE
1815 else:
1816 status = cls.Status.QUEUED
1817
1818 return cls(job=job, host=host, meta_host=meta_host,
1819 atomic_group=atomic_group, status=status)
1820
1821
showarda5288b42009-07-28 20:06:08 +00001822 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001823 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001824 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001825 self._check_for_updated_attributes()
1826
1827
showardc0ac3a72009-07-08 21:14:45 +00001828 def execution_path(self):
1829 """
1830 Path to this entry's results (relative to the base results directory).
1831 """
MK Ryu0c1a37d2015-04-30 12:00:55 -07001832 return server_utils.get_hqe_exec_path(self.job.tag(),
1833 self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001834
1835
showard3f15eed2008-11-14 22:40:48 +00001836 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001837 """Returns the first non-None name found in priority order.
1838
1839 The priority order checked is: (1) host name; (2) meta host name; and
1840 (3) atomic group name.
1841 """
showard3f15eed2008-11-14 22:40:48 +00001842 if self.host:
1843 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001844 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +00001845 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +00001846 else:
1847 assert self.atomic_group, "no host, meta_host or atomic group!"
1848 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +00001849
1850
showard2bab8f42008-11-12 18:15:22 +00001851 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001852 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001853 self.active, self.complete = True, False
1854 elif self.status in self.COMPLETE_STATUSES:
1855 self.active, self.complete = False, True
1856 else:
1857 self.active, self.complete = False, False
1858
1859
1860 def on_attribute_changed(self, attribute, old_value):
1861 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001862 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1863 self.status)
showard2bab8f42008-11-12 18:15:22 +00001864
1865
jadmanski0afbb632008-06-06 21:10:57 +00001866 def is_meta_host_entry(self):
1867 'True if this is a entry has a meta_host instead of a host.'
1868 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001869
showarda3ab0d52008-11-03 19:03:47 +00001870
Simran Basic1b26762013-06-26 14:23:21 -07001871 # This code is shared between rpc_interface and models.HostQueueEntry.
1872 # Sadly due to circular imports between the 2 (crbug.com/230100) making it
1873 # a class method was the best way to refactor it. Attempting to put it in
1874 # rpc_utils or a new utils module failed as that would require us to import
1875 # models.py but to call it from here we would have to import the utils.py
1876 # thus creating a cycle.
1877 @classmethod
1878 def abort_host_queue_entries(cls, host_queue_entries):
1879 """Aborts a collection of host_queue_entries.
1880
1881 Abort these host queue entry and all host queue entries of jobs created
1882 by them.
1883
1884 @param host_queue_entries: List of host queue entries we want to abort.
1885 """
1886 # This isn't completely immune to race conditions since it's not atomic,
1887 # but it should be safe given the scheduler's behavior.
1888
1889 # TODO(milleral): crbug.com/230100
1890 # The |abort_host_queue_entries| rpc does nearly exactly this,
1891 # however, trying to re-use the code generates some horrible
1892 # circular import error. I'd be nice to refactor things around
1893 # sometime so the code could be reused.
1894
1895 # Fixpoint algorithm to find the whole tree of HQEs to abort to
1896 # minimize the total number of database queries:
1897 children = set()
1898 new_children = set(host_queue_entries)
1899 while new_children:
1900 children.update(new_children)
1901 new_child_ids = [hqe.job_id for hqe in new_children]
1902 new_children = HostQueueEntry.objects.filter(
1903 job__parent_job__in=new_child_ids,
1904 complete=False, aborted=False).all()
1905 # To handle circular parental relationships
1906 new_children = set(new_children) - children
1907
1908 # Associate a user with the host queue entries that we're about
1909 # to abort so that we can look up who to blame for the aborts.
1910 now = datetime.now()
1911 user = User.current_user()
1912 aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
1913 aborted_by=user, aborted_on=now) for hqe in children]
1914 AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
1915 # Bulk update all of the HQEs to set the abort bit.
1916 child_ids = [hqe.id for hqe in children]
1917 HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
1918
1919
Scott Zawalski23041432013-04-17 07:39:09 -07001920 def abort(self):
Alex Millerdea67042013-04-22 17:23:34 -07001921 """ Aborts this host queue entry.
Simran Basic1b26762013-06-26 14:23:21 -07001922
Alex Millerdea67042013-04-22 17:23:34 -07001923 Abort this host queue entry and all host queue entries of jobs created by
1924 this one.
1925
1926 """
showardd3dc1992009-04-22 21:01:40 +00001927 if not self.complete and not self.aborted:
Simran Basi97582a22013-06-27 12:03:21 -07001928 HostQueueEntry.abort_host_queue_entries([self])
mblighe8819cd2008-02-15 16:48:40 +00001929
showardd3dc1992009-04-22 21:01:40 +00001930
1931 @classmethod
1932 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001933 """Returns a modified status msg if the host queue entry was aborted.
1934
1935 @param cls: Implicit class object.
1936 @param status: The original status message.
1937 @param aborted: Whether the host queue entry was aborted.
1938 @param complete: Whether the host queue entry was completed.
1939 """
showardd3dc1992009-04-22 21:01:40 +00001940 if aborted and not complete:
1941 return 'Aborted (%s)' % status
1942 return status
1943
1944
1945 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001946 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00001947 return self.compute_full_status(self.status, self.aborted,
1948 self.complete)
1949
1950
1951 def _postprocess_object_dict(self, object_dict):
1952 object_dict['full_status'] = self.full_status()
1953
1954
jadmanski0afbb632008-06-06 21:10:57 +00001955 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001956 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001957 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00001958
showard12f3e322009-05-13 21:27:42 +00001959
showard4c119042008-09-29 19:16:18 +00001960
showarda5288b42009-07-28 20:06:08 +00001961 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00001962 hostname = None
1963 if self.host:
1964 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00001965 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00001966
1967
showard4c119042008-09-29 19:16:18 +00001968class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001969 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00001970 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
1971 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00001972 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00001973
1974 objects = model_logic.ExtendedManager()
1975
showard68c7aa02008-10-09 16:49:11 +00001976
showarda5288b42009-07-28 20:06:08 +00001977 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00001978 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00001979 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00001980
showard4c119042008-09-29 19:16:18 +00001981 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001982 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001983 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00001984
1985
showard6d7b2ff2009-06-10 00:16:47 +00001986class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
1987 """\
1988 Tasks to run on hosts at the next time they are in the Ready state. Use this
1989 for high-priority tasks, such as forced repair or forced reinstall.
1990
1991 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00001992 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00001993 time_requested: date and time the request for this task was made
1994 is_active: task is currently running
1995 is_complete: task has finished running
beeps8bb1f7d2013-08-05 01:30:09 -07001996 is_aborted: task was aborted
showard2fe3f1d2009-07-06 20:19:11 +00001997 time_started: date and time the task started
Dan Shid0725542014-06-23 15:34:27 -07001998 time_finished: date and time the task finished
showard2fe3f1d2009-07-06 20:19:11 +00001999 queue_entry: Host queue entry waiting on this task (or None, if task was not
2000 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00002001 """
Alex Millerdfff2fd2013-05-28 13:05:06 -07002002 Task = enum.Enum('Verify', 'Cleanup', 'Repair', 'Reset', 'Provision',
Dan Shi07e09af2013-04-12 09:31:29 -07002003 string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00002004
2005 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00002006 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00002007 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00002008 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00002009 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
2010 null=False)
2011 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
2012 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
beeps8bb1f7d2013-08-05 01:30:09 -07002013 is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00002014 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00002015 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00002016 success = dbmodels.BooleanField(default=False, blank=False, null=False)
Dan Shid0725542014-06-23 15:34:27 -07002017 time_finished = dbmodels.DateTimeField(null=True, blank=True)
showard6d7b2ff2009-06-10 00:16:47 +00002018
2019 objects = model_logic.ExtendedManager()
2020
2021
showard9bb960b2009-11-19 01:02:11 +00002022 def save(self, **kwargs):
2023 if self.queue_entry:
2024 self.requested_by = User.objects.get(
2025 login=self.queue_entry.job.owner)
2026 super(SpecialTask, self).save(**kwargs)
2027
2028
showarded2afea2009-07-07 20:54:07 +00002029 def execution_path(self):
MK Ryu0c1a37d2015-04-30 12:00:55 -07002030 """Returns the execution path for a special task."""
2031 return server_utils.get_special_task_exec_path(
2032 self.host.hostname, self.id, self.task, self.time_requested)
showarded2afea2009-07-07 20:54:07 +00002033
2034
showardc0ac3a72009-07-08 21:14:45 +00002035 # property to emulate HostQueueEntry.status
2036 @property
2037 def status(self):
MK Ryu0c1a37d2015-04-30 12:00:55 -07002038 """Returns a host queue entry status appropriate for a speical task."""
2039 return server_utils.get_special_task_status(
2040 self.is_complete, self.success, self.is_active)
showardc0ac3a72009-07-08 21:14:45 +00002041
2042
2043 # property to emulate HostQueueEntry.started_on
2044 @property
2045 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002046 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00002047 return self.time_started
2048
2049
showard6d7b2ff2009-06-10 00:16:47 +00002050 @classmethod
showardc5103442010-01-15 00:20:26 +00002051 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002052 """Schedules a special task on a host if not already scheduled.
2053
2054 @param cls: Implicit class object.
2055 @param host: The host to use.
2056 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00002057 """
showardc5103442010-01-15 00:20:26 +00002058 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
2059 is_active=False,
2060 is_complete=False)
2061 if existing_tasks:
2062 return existing_tasks[0]
2063
2064 special_task = SpecialTask(host=host, task=task,
2065 requested_by=User.current_user())
2066 special_task.save()
2067 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00002068
2069
beeps8bb1f7d2013-08-05 01:30:09 -07002070 def abort(self):
2071 """ Abort this special task."""
2072 self.is_aborted = True
2073 self.save()
2074
2075
showarded2afea2009-07-07 20:54:07 +00002076 def activate(self):
showard474d1362009-08-20 23:32:01 +00002077 """
2078 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00002079 """
showard97446882009-07-20 22:37:28 +00002080 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00002081 self.is_active = True
2082 self.time_started = datetime.now()
2083 self.save()
2084
2085
showarde60e44e2009-11-13 20:45:38 +00002086 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002087 """Sets a task as completed.
2088
2089 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00002090 """
showard97446882009-07-20 22:37:28 +00002091 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00002092 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00002093 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00002094 self.success = success
Dan Shid85d6112014-07-14 10:32:55 -07002095 if self.time_started:
2096 self.time_finished = datetime.now()
showard2fe3f1d2009-07-06 20:19:11 +00002097 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00002098
2099
2100 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08002101 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00002102 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00002103
showard474d1362009-08-20 23:32:01 +00002104
showarda5288b42009-07-28 20:06:08 +00002105 def __unicode__(self):
2106 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00002107 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00002108 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00002109 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00002110 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00002111 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00002112
2113 return result
Dan Shi6964fa52014-12-18 11:04:27 -08002114
2115
2116class StableVersion(dbmodels.Model, model_logic.ModelExtensions):
2117
2118 board = dbmodels.CharField(max_length=255, unique=True)
2119 version = dbmodels.CharField(max_length=255)
2120
2121 class Meta:
2122 """Metadata for class StableVersion."""
Fang Deng86248502014-12-18 16:38:00 -08002123 db_table = 'afe_stable_versions'