blob: c07f881878f4e80816d9568f73acc10bcea12c6e [file] [log] [blame]
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001# pylint: disable-msg=C0111
2
showardd1195652009-12-08 22:21:02 +00003import logging, os
showardfb2a7fa2008-07-17 17:04:12 +00004from datetime import datetime
Aviv Keshetfa199002013-05-09 13:31:46 -07005import django.core
6try:
7 from django.db import models as dbmodels, connection
8except django.core.exceptions.ImproperlyConfigured:
9 raise ImportError('Django database not yet configured. Import either '
10 'setup_django_environment or '
11 'setup_django_lite_environment from '
12 'autotest_lib.frontend before any imports that '
13 'depend on django models.')
jamesren35a70222010-02-16 19:30:46 +000014from xml.sax import saxutils
showardcafd16e2009-05-29 18:37:49 +000015import common
jamesrendd855242010-03-02 22:23:44 +000016from autotest_lib.frontend.afe import model_logic, model_attributes
Prashanth B489b91d2014-03-15 12:17:16 -070017from autotest_lib.frontend.afe import rdb_model_extensions
showardcafd16e2009-05-29 18:37:49 +000018from autotest_lib.frontend import settings, thread_local
Jakob Juelicha94efe62014-09-18 16:02:49 -070019from autotest_lib.client.common_lib import enum, error, host_protections
20from autotest_lib.client.common_lib import global_config
showardeaa408e2009-09-11 18:45:31 +000021from autotest_lib.client.common_lib import host_queue_entry_states
Jakob Juelicha94efe62014-09-18 16:02:49 -070022from autotest_lib.client.common_lib import control_data, priorities, decorators
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -080023from autotest_lib.client.common_lib import site_utils
Dan Shia0acfbc2014-10-14 15:56:23 -070024from autotest_lib.client.common_lib.cros.graphite import es_utils
mblighe8819cd2008-02-15 16:48:40 +000025
showard0fc38302008-10-23 00:44:07 +000026# job options and user preferences
jamesrendd855242010-03-02 22:23:44 +000027DEFAULT_REBOOT_BEFORE = model_attributes.RebootBefore.IF_DIRTY
Dan Shi07e09af2013-04-12 09:31:29 -070028DEFAULT_REBOOT_AFTER = model_attributes.RebootBefore.NEVER
mblighe8819cd2008-02-15 16:48:40 +000029
showard89f84db2009-03-12 20:39:13 +000030
mblighe8819cd2008-02-15 16:48:40 +000031class AclAccessViolation(Exception):
jadmanski0afbb632008-06-06 21:10:57 +000032 """\
33 Raised when an operation is attempted with proper permissions as
34 dictated by ACLs.
35 """
mblighe8819cd2008-02-15 16:48:40 +000036
37
showard205fd602009-03-21 00:17:35 +000038class AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model):
showard89f84db2009-03-12 20:39:13 +000039 """\
40 An atomic group defines a collection of hosts which must only be scheduled
41 all at once. Any host with a label having an atomic group will only be
42 scheduled for a job at the same time as other hosts sharing that label.
43
44 Required:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080045 name: A name for this atomic group, e.g. 'rack23' or 'funky_net'.
showard89f84db2009-03-12 20:39:13 +000046 max_number_of_machines: The maximum number of machines that will be
47 scheduled at once when scheduling jobs to this atomic group.
48 The job.synch_count is considered the minimum.
49
50 Optional:
51 description: Arbitrary text description of this group's purpose.
52 """
showarda5288b42009-07-28 20:06:08 +000053 name = dbmodels.CharField(max_length=255, unique=True)
showard89f84db2009-03-12 20:39:13 +000054 description = dbmodels.TextField(blank=True)
showarde9450c92009-06-30 01:58:52 +000055 # This magic value is the default to simplify the scheduler logic.
56 # It must be "large". The common use of atomic groups is to want all
57 # machines in the group to be used, limits on which subset used are
58 # often chosen via dependency labels.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080059 # TODO(dennisjeffrey): Revisit this so we don't have to assume that
60 # "infinity" is around 3.3 million.
showarde9450c92009-06-30 01:58:52 +000061 INFINITE_MACHINES = 333333333
62 max_number_of_machines = dbmodels.IntegerField(default=INFINITE_MACHINES)
showard205fd602009-03-21 00:17:35 +000063 invalid = dbmodels.BooleanField(default=False,
showarda5288b42009-07-28 20:06:08 +000064 editable=settings.FULL_ADMIN)
showard89f84db2009-03-12 20:39:13 +000065
showard89f84db2009-03-12 20:39:13 +000066 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +000067 objects = model_logic.ModelWithInvalidManager()
showard205fd602009-03-21 00:17:35 +000068 valid_objects = model_logic.ValidObjectsManager()
69
70
showard29f7cd22009-04-29 21:16:24 +000071 def enqueue_job(self, job, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080072 """Enqueue a job on an associated atomic group of hosts.
73
74 @param job: A job to enqueue.
75 @param is_template: Whether the status should be "Template".
76 """
showard29f7cd22009-04-29 21:16:24 +000077 queue_entry = HostQueueEntry.create(atomic_group=self, job=job,
78 is_template=is_template)
showardc92da832009-04-07 18:14:34 +000079 queue_entry.save()
80
81
showard205fd602009-03-21 00:17:35 +000082 def clean_object(self):
83 self.label_set.clear()
showard89f84db2009-03-12 20:39:13 +000084
85
86 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -080087 """Metadata for class AtomicGroup."""
showardeab66ce2009-12-23 00:03:56 +000088 db_table = 'afe_atomic_groups'
showard89f84db2009-03-12 20:39:13 +000089
showard205fd602009-03-21 00:17:35 +000090
showarda5288b42009-07-28 20:06:08 +000091 def __unicode__(self):
92 return unicode(self.name)
showard89f84db2009-03-12 20:39:13 +000093
94
showard7c785282008-05-29 19:45:12 +000095class Label(model_logic.ModelWithInvalid, dbmodels.Model):
jadmanski0afbb632008-06-06 21:10:57 +000096 """\
97 Required:
showard89f84db2009-03-12 20:39:13 +000098 name: label name
mblighe8819cd2008-02-15 16:48:40 +000099
jadmanski0afbb632008-06-06 21:10:57 +0000100 Optional:
showard89f84db2009-03-12 20:39:13 +0000101 kernel_config: URL/path to kernel config for jobs run on this label.
102 platform: If True, this is a platform label (defaults to False).
103 only_if_needed: If True, a Host with this label can only be used if that
104 label is requested by the job/test (either as the meta_host or
105 in the job_dependencies).
106 atomic_group: The atomic group associated with this label.
jadmanski0afbb632008-06-06 21:10:57 +0000107 """
showarda5288b42009-07-28 20:06:08 +0000108 name = dbmodels.CharField(max_length=255, unique=True)
109 kernel_config = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000110 platform = dbmodels.BooleanField(default=False)
111 invalid = dbmodels.BooleanField(default=False,
112 editable=settings.FULL_ADMIN)
showardb1e51872008-10-07 11:08:18 +0000113 only_if_needed = dbmodels.BooleanField(default=False)
mblighe8819cd2008-02-15 16:48:40 +0000114
jadmanski0afbb632008-06-06 21:10:57 +0000115 name_field = 'name'
jamesrene3656232010-03-02 00:00:30 +0000116 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000117 valid_objects = model_logic.ValidObjectsManager()
showard89f84db2009-03-12 20:39:13 +0000118 atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
119
mbligh5244cbb2008-04-24 20:39:52 +0000120
jadmanski0afbb632008-06-06 21:10:57 +0000121 def clean_object(self):
122 self.host_set.clear()
showard01a51672009-05-29 18:42:37 +0000123 self.test_set.clear()
mblighe8819cd2008-02-15 16:48:40 +0000124
125
showard29f7cd22009-04-29 21:16:24 +0000126 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800127 """Enqueue a job on any host of this label.
128
129 @param job: A job to enqueue.
130 @param atomic_group: The associated atomic group.
131 @param is_template: Whether the status should be "Template".
132 """
showard29f7cd22009-04-29 21:16:24 +0000133 queue_entry = HostQueueEntry.create(meta_host=self, job=job,
134 is_template=is_template,
135 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000136 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000137
138
jadmanski0afbb632008-06-06 21:10:57 +0000139 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800140 """Metadata for class Label."""
showardeab66ce2009-12-23 00:03:56 +0000141 db_table = 'afe_labels'
mblighe8819cd2008-02-15 16:48:40 +0000142
showarda5288b42009-07-28 20:06:08 +0000143 def __unicode__(self):
144 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000145
146
Jakob Jülich92c06332014-08-25 19:06:57 +0000147class Shard(dbmodels.Model, model_logic.ModelExtensions):
148
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700149 hostname = dbmodels.CharField(max_length=255, unique=True)
150
151 name_field = 'hostname'
152
Jakob Jülich92c06332014-08-25 19:06:57 +0000153 labels = dbmodels.ManyToManyField(Label, blank=True,
154 db_table='afe_shards_labels')
155
156 class Meta:
157 """Metadata for class ParameterizedJob."""
158 db_table = 'afe_shards'
159
160
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -0800161 def rpc_hostname(self):
162 """Get the rpc hostname of the shard.
163
164 @return: Just the shard hostname for all non-testing environments.
165 The address of the default gateway for vm testing environments.
166 """
167 # TODO: Figure out a better solution for testing. Since no 2 shards
168 # can run on the same host, if the shard hostname is localhost we
169 # conclude that it must be a vm in a test cluster. In such situations
170 # a name of localhost:<port> is necessary to achieve the correct
171 # afe links/redirection from the frontend (this happens through the
172 # host), but for rpcs that are performed *on* the shard, they need to
173 # use the address of the gateway.
174 hostname = self.hostname.split(':')[0]
175 if site_utils.is_localhost(hostname):
176 return self.hostname.replace(
177 hostname, site_utils.DEFAULT_VM_GATEWAY)
178 return self.hostname
179
180
jamesren76fcf192010-04-21 20:39:50 +0000181class Drone(dbmodels.Model, model_logic.ModelExtensions):
182 """
183 A scheduler drone
184
185 hostname: the drone's hostname
186 """
187 hostname = dbmodels.CharField(max_length=255, unique=True)
188
189 name_field = 'hostname'
190 objects = model_logic.ExtendedManager()
191
192
193 def save(self, *args, **kwargs):
194 if not User.current_user().is_superuser():
195 raise Exception('Only superusers may edit drones')
196 super(Drone, self).save(*args, **kwargs)
197
198
199 def delete(self):
200 if not User.current_user().is_superuser():
201 raise Exception('Only superusers may delete drones')
202 super(Drone, self).delete()
203
204
205 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800206 """Metadata for class Drone."""
jamesren76fcf192010-04-21 20:39:50 +0000207 db_table = 'afe_drones'
208
209 def __unicode__(self):
210 return unicode(self.hostname)
211
212
213class DroneSet(dbmodels.Model, model_logic.ModelExtensions):
214 """
215 A set of scheduler drones
216
217 These will be used by the scheduler to decide what drones a job is allowed
218 to run on.
219
220 name: the drone set's name
221 drones: the drones that are part of the set
222 """
223 DRONE_SETS_ENABLED = global_config.global_config.get_config_value(
224 'SCHEDULER', 'drone_sets_enabled', type=bool, default=False)
225 DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value(
226 'SCHEDULER', 'default_drone_set_name', default=None)
227
228 name = dbmodels.CharField(max_length=255, unique=True)
229 drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones')
230
231 name_field = 'name'
232 objects = model_logic.ExtendedManager()
233
234
235 def save(self, *args, **kwargs):
236 if not User.current_user().is_superuser():
237 raise Exception('Only superusers may edit drone sets')
238 super(DroneSet, self).save(*args, **kwargs)
239
240
241 def delete(self):
242 if not User.current_user().is_superuser():
243 raise Exception('Only superusers may delete drone sets')
244 super(DroneSet, self).delete()
245
246
247 @classmethod
248 def drone_sets_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800249 """Returns whether drone sets are enabled.
250
251 @param cls: Implicit class object.
252 """
jamesren76fcf192010-04-21 20:39:50 +0000253 return cls.DRONE_SETS_ENABLED
254
255
256 @classmethod
257 def default_drone_set_name(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800258 """Returns the default drone set name.
259
260 @param cls: Implicit class object.
261 """
jamesren76fcf192010-04-21 20:39:50 +0000262 return cls.DEFAULT_DRONE_SET_NAME
263
264
265 @classmethod
266 def get_default(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800267 """Gets the default drone set name, compatible with Job.add_object.
268
269 @param cls: Implicit class object.
270 """
jamesren76fcf192010-04-21 20:39:50 +0000271 return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME)
272
273
274 @classmethod
275 def resolve_name(cls, drone_set_name):
276 """
277 Returns the name of one of these, if not None, in order of preference:
278 1) the drone set given,
279 2) the current user's default drone set, or
280 3) the global default drone set
281
282 or returns None if drone sets are disabled
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800283
284 @param cls: Implicit class object.
285 @param drone_set_name: A drone set name.
jamesren76fcf192010-04-21 20:39:50 +0000286 """
287 if not cls.drone_sets_enabled():
288 return None
289
290 user = User.current_user()
291 user_drone_set_name = user.drone_set and user.drone_set.name
292
293 return drone_set_name or user_drone_set_name or cls.get_default().name
294
295
296 def get_drone_hostnames(self):
297 """
298 Gets the hostnames of all drones in this drone set
299 """
300 return set(self.drones.all().values_list('hostname', flat=True))
301
302
303 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800304 """Metadata for class DroneSet."""
jamesren76fcf192010-04-21 20:39:50 +0000305 db_table = 'afe_drone_sets'
306
307 def __unicode__(self):
308 return unicode(self.name)
309
310
showardfb2a7fa2008-07-17 17:04:12 +0000311class User(dbmodels.Model, model_logic.ModelExtensions):
312 """\
313 Required:
314 login :user login name
315
316 Optional:
317 access_level: 0=User (default), 1=Admin, 100=Root
318 """
319 ACCESS_ROOT = 100
320 ACCESS_ADMIN = 1
321 ACCESS_USER = 0
322
showard64a95952010-01-13 21:27:16 +0000323 AUTOTEST_SYSTEM = 'autotest_system'
324
showarda5288b42009-07-28 20:06:08 +0000325 login = dbmodels.CharField(max_length=255, unique=True)
showardfb2a7fa2008-07-17 17:04:12 +0000326 access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
327
showard0fc38302008-10-23 00:44:07 +0000328 # user preferences
jamesrendd855242010-03-02 22:23:44 +0000329 reboot_before = dbmodels.SmallIntegerField(
330 choices=model_attributes.RebootBefore.choices(), blank=True,
331 default=DEFAULT_REBOOT_BEFORE)
332 reboot_after = dbmodels.SmallIntegerField(
333 choices=model_attributes.RebootAfter.choices(), blank=True,
334 default=DEFAULT_REBOOT_AFTER)
jamesren76fcf192010-04-21 20:39:50 +0000335 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
showard97db5ba2008-11-12 18:18:02 +0000336 show_experimental = dbmodels.BooleanField(default=False)
showard0fc38302008-10-23 00:44:07 +0000337
showardfb2a7fa2008-07-17 17:04:12 +0000338 name_field = 'login'
339 objects = model_logic.ExtendedManager()
340
341
showarda5288b42009-07-28 20:06:08 +0000342 def save(self, *args, **kwargs):
showardfb2a7fa2008-07-17 17:04:12 +0000343 # is this a new object being saved for the first time?
344 first_time = (self.id is None)
345 user = thread_local.get_user()
showard0fc38302008-10-23 00:44:07 +0000346 if user and not user.is_superuser() and user.login != self.login:
347 raise AclAccessViolation("You cannot modify user " + self.login)
showarda5288b42009-07-28 20:06:08 +0000348 super(User, self).save(*args, **kwargs)
showardfb2a7fa2008-07-17 17:04:12 +0000349 if first_time:
350 everyone = AclGroup.objects.get(name='Everyone')
351 everyone.users.add(self)
352
353
354 def is_superuser(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800355 """Returns whether the user has superuser access."""
showardfb2a7fa2008-07-17 17:04:12 +0000356 return self.access_level >= self.ACCESS_ROOT
357
358
showard64a95952010-01-13 21:27:16 +0000359 @classmethod
360 def current_user(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800361 """Returns the current user.
362
363 @param cls: Implicit class object.
364 """
showard64a95952010-01-13 21:27:16 +0000365 user = thread_local.get_user()
366 if user is None:
showardcfcdd802010-01-15 00:16:33 +0000367 user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM)
showard64a95952010-01-13 21:27:16 +0000368 user.access_level = cls.ACCESS_ROOT
369 user.save()
370 return user
371
372
showardfb2a7fa2008-07-17 17:04:12 +0000373 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800374 """Metadata for class User."""
showardeab66ce2009-12-23 00:03:56 +0000375 db_table = 'afe_users'
showardfb2a7fa2008-07-17 17:04:12 +0000376
showarda5288b42009-07-28 20:06:08 +0000377 def __unicode__(self):
378 return unicode(self.login)
showardfb2a7fa2008-07-17 17:04:12 +0000379
380
Prashanth B489b91d2014-03-15 12:17:16 -0700381class Host(model_logic.ModelWithInvalid, rdb_model_extensions.AbstractHostModel,
showardf8b19042009-05-12 17:22:49 +0000382 model_logic.ModelWithAttributes):
jadmanski0afbb632008-06-06 21:10:57 +0000383 """\
384 Required:
385 hostname
mblighe8819cd2008-02-15 16:48:40 +0000386
jadmanski0afbb632008-06-06 21:10:57 +0000387 optional:
showard21baa452008-10-21 00:08:39 +0000388 locked: if true, host is locked and will not be queued
mblighe8819cd2008-02-15 16:48:40 +0000389
jadmanski0afbb632008-06-06 21:10:57 +0000390 Internal:
Prashanth B489b91d2014-03-15 12:17:16 -0700391 From AbstractHostModel:
392 synch_id: currently unused
393 status: string describing status of host
394 invalid: true if the host has been deleted
395 protection: indicates what can be done to this host during repair
396 lock_time: DateTime at which the host was locked
397 dirty: true if the host has been used without being rebooted
398 Local:
399 locked_by: user that locked the host, or null if the host is unlocked
jadmanski0afbb632008-06-06 21:10:57 +0000400 """
mblighe8819cd2008-02-15 16:48:40 +0000401
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700402 SERIALIZATION_LINKS_TO_FOLLOW = set(['aclgroup_set',
403 'hostattribute_set',
404 'labels',
405 'shard'])
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800406 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['invalid'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700407
Jakob Juelichf88fa932014-09-03 17:58:04 -0700408
409 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700410 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -0700411 self.shard = Shard.deserialize(data)
412
413
Prashanth B489b91d2014-03-15 12:17:16 -0700414 # Note: Only specify foreign keys here, specify all native host columns in
415 # rdb_model_extensions instead.
416 Protection = host_protections.Protection
showardeab66ce2009-12-23 00:03:56 +0000417 labels = dbmodels.ManyToManyField(Label, blank=True,
418 db_table='afe_hosts_labels')
showardfb2a7fa2008-07-17 17:04:12 +0000419 locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
jadmanski0afbb632008-06-06 21:10:57 +0000420 name_field = 'hostname'
jamesrene3656232010-03-02 00:00:30 +0000421 objects = model_logic.ModelWithInvalidManager()
jadmanski0afbb632008-06-06 21:10:57 +0000422 valid_objects = model_logic.ValidObjectsManager()
beepscc9fc702013-12-02 12:45:38 -0800423 leased_objects = model_logic.LeasedHostManager()
mbligh5244cbb2008-04-24 20:39:52 +0000424
Jakob Juelichde2b9a92014-09-02 15:29:28 -0700425 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
showard2bab8f42008-11-12 18:15:22 +0000426
427 def __init__(self, *args, **kwargs):
428 super(Host, self).__init__(*args, **kwargs)
429 self._record_attributes(['status'])
430
431
showardb8471e32008-07-03 19:51:08 +0000432 @staticmethod
433 def create_one_time_host(hostname):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800434 """Creates a one-time host.
435
436 @param hostname: The name for the host.
437 """
showardb8471e32008-07-03 19:51:08 +0000438 query = Host.objects.filter(hostname=hostname)
439 if query.count() == 0:
440 host = Host(hostname=hostname, invalid=True)
showarda8411af2008-08-07 22:35:58 +0000441 host.do_validate()
showardb8471e32008-07-03 19:51:08 +0000442 else:
443 host = query[0]
444 if not host.invalid:
445 raise model_logic.ValidationError({
mblighb5b7b5d2009-02-03 17:47:15 +0000446 'hostname' : '%s already exists in the autotest DB. '
447 'Select it rather than entering it as a one time '
448 'host.' % hostname
showardb8471e32008-07-03 19:51:08 +0000449 })
showard1ab512b2008-07-30 23:39:04 +0000450 host.protection = host_protections.Protection.DO_NOT_REPAIR
showard946a7af2009-04-15 21:53:23 +0000451 host.locked = False
showardb8471e32008-07-03 19:51:08 +0000452 host.save()
showard2924b0a2009-06-18 23:16:15 +0000453 host.clean_object()
showardb8471e32008-07-03 19:51:08 +0000454 return host
mbligh5244cbb2008-04-24 20:39:52 +0000455
showard1ff7b2e2009-05-15 23:17:18 +0000456
Jakob Juelich59cfe542014-09-02 16:37:46 -0700457 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -0700458 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -0700459 """Assigns hosts to a shard.
460
Jakob Juelich1b525742014-09-30 13:08:07 -0700461 For all labels that have been assigned to this shard, all hosts that
462 have this label, are assigned to this shard.
463
464 Hosts that are assigned to the shard but aren't already present on the
465 shard are returned.
Jakob Juelich59cfe542014-09-02 16:37:46 -0700466
467 @param shard: The shard object to assign labels/hosts for.
Jakob Juelich1b525742014-09-30 13:08:07 -0700468 @param known_ids: List of all host-ids the shard already knows.
469 This is used to figure out which hosts should be sent
470 to the shard. If shard_ids were used instead, hosts
471 would only be transferred once, even if the client
472 failed persisting them.
473 The number of hosts usually lies in O(100), so the
474 overhead is acceptable.
475
Jakob Juelich59cfe542014-09-02 16:37:46 -0700476 @returns the hosts objects that should be sent to the shard.
477 """
478
479 # Disclaimer: concurrent heartbeats should theoretically not occur in
480 # the current setup. As they may be introduced in the near future,
481 # this comment will be left here.
482
483 # Sending stuff twice is acceptable, but forgetting something isn't.
484 # Detecting duplicates on the client is easy, but here it's harder. The
485 # following options were considered:
486 # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
487 # than select returned, as concurrently more hosts might have been
488 # inserted
489 # - UPDATE and then SELECT WHERE shard=shard: select always returns all
490 # hosts for the shard, this is overhead
491 # - SELECT and then UPDATE only selected without requerying afterwards:
492 # returns the old state of the records.
493 host_ids = list(Host.objects.filter(
Jakob Juelich59cfe542014-09-02 16:37:46 -0700494 labels=shard.labels.all(),
495 leased=False
Jakob Juelich1b525742014-09-30 13:08:07 -0700496 ).exclude(
497 id__in=known_ids,
Jakob Juelich59cfe542014-09-02 16:37:46 -0700498 ).values_list('pk', flat=True))
499
500 if host_ids:
501 Host.objects.filter(pk__in=host_ids).update(shard=shard)
502 return list(Host.objects.filter(pk__in=host_ids).all())
503 return []
504
showardafd97de2009-10-01 18:45:09 +0000505 def resurrect_object(self, old_object):
506 super(Host, self).resurrect_object(old_object)
507 # invalid hosts can be in use by the scheduler (as one-time hosts), so
508 # don't change the status
509 self.status = old_object.status
510
511
jadmanski0afbb632008-06-06 21:10:57 +0000512 def clean_object(self):
513 self.aclgroup_set.clear()
514 self.labels.clear()
mblighe8819cd2008-02-15 16:48:40 +0000515
516
Dan Shia0acfbc2014-10-14 15:56:23 -0700517 def record_state(self, type_str, state, value, other_metadata=None):
518 """Record metadata in elasticsearch.
519
520 @param type_str: sets the _type field in elasticsearch db.
521 @param state: string representing what state we are recording,
522 e.g. 'locked'
523 @param value: value of the state, e.g. True
524 @param other_metadata: Other metadata to store in metaDB.
525 """
526 metadata = {
527 state: value,
528 'hostname': self.hostname,
529 }
530 if other_metadata:
531 metadata = dict(metadata.items() + other_metadata.items())
532 es_utils.ESMetadata().post(type_str=type_str, metadata=metadata)
533
534
showarda5288b42009-07-28 20:06:08 +0000535 def save(self, *args, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000536 # extra spaces in the hostname can be a sneaky source of errors
537 self.hostname = self.hostname.strip()
538 # is this a new object being saved for the first time?
539 first_time = (self.id is None)
showard3dd47c22008-07-10 00:41:36 +0000540 if not first_time:
541 AclGroup.check_for_acl_violation_hosts([self])
Dan Shia0acfbc2014-10-14 15:56:23 -0700542 # If locked is changed, send its status and user made the change to
543 # metaDB. Locks are important in host history because if a device is
544 # locked then we don't really care what state it is in.
showardfb2a7fa2008-07-17 17:04:12 +0000545 if self.locked and not self.locked_by:
showard64a95952010-01-13 21:27:16 +0000546 self.locked_by = User.current_user()
showardfb2a7fa2008-07-17 17:04:12 +0000547 self.lock_time = datetime.now()
Dan Shia0acfbc2014-10-14 15:56:23 -0700548 self.record_state('lock_history', 'locked', self.locked,
549 {'changed_by': self.locked_by.login})
showard21baa452008-10-21 00:08:39 +0000550 self.dirty = True
showardfb2a7fa2008-07-17 17:04:12 +0000551 elif not self.locked and self.locked_by:
Dan Shia0acfbc2014-10-14 15:56:23 -0700552 self.record_state('lock_history', 'locked', self.locked,
553 {'changed_by': self.locked_by.login})
showardfb2a7fa2008-07-17 17:04:12 +0000554 self.locked_by = None
555 self.lock_time = None
showarda5288b42009-07-28 20:06:08 +0000556 super(Host, self).save(*args, **kwargs)
jadmanski0afbb632008-06-06 21:10:57 +0000557 if first_time:
558 everyone = AclGroup.objects.get(name='Everyone')
559 everyone.hosts.add(self)
showard2bab8f42008-11-12 18:15:22 +0000560 self._check_for_updated_attributes()
561
mblighe8819cd2008-02-15 16:48:40 +0000562
showardb8471e32008-07-03 19:51:08 +0000563 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000564 AclGroup.check_for_acl_violation_hosts([self])
showardb8471e32008-07-03 19:51:08 +0000565 for queue_entry in self.hostqueueentry_set.all():
566 queue_entry.deleted = True
showard64a95952010-01-13 21:27:16 +0000567 queue_entry.abort()
showardb8471e32008-07-03 19:51:08 +0000568 super(Host, self).delete()
569
mblighe8819cd2008-02-15 16:48:40 +0000570
showard2bab8f42008-11-12 18:15:22 +0000571 def on_attribute_changed(self, attribute, old_value):
572 assert attribute == 'status'
showardf1175bb2009-06-17 19:34:36 +0000573 logging.info(self.hostname + ' -> ' + self.status)
showard2bab8f42008-11-12 18:15:22 +0000574
575
showard29f7cd22009-04-29 21:16:24 +0000576 def enqueue_job(self, job, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800577 """Enqueue a job on this host.
578
579 @param job: A job to enqueue.
580 @param atomic_group: The associated atomic group.
581 @param is_template: Whther the status should be "Template".
582 """
showard29f7cd22009-04-29 21:16:24 +0000583 queue_entry = HostQueueEntry.create(host=self, job=job,
584 is_template=is_template,
585 atomic_group=atomic_group)
jadmanski0afbb632008-06-06 21:10:57 +0000586 # allow recovery of dead hosts from the frontend
587 if not self.active_queue_entry() and self.is_dead():
588 self.status = Host.Status.READY
589 self.save()
590 queue_entry.save()
mblighe8819cd2008-02-15 16:48:40 +0000591
showard08f981b2008-06-24 21:59:03 +0000592 block = IneligibleHostQueue(job=job, host=self)
593 block.save()
594
mblighe8819cd2008-02-15 16:48:40 +0000595
jadmanski0afbb632008-06-06 21:10:57 +0000596 def platform(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800597 """The platform of the host."""
jadmanski0afbb632008-06-06 21:10:57 +0000598 # TODO(showard): slighly hacky?
599 platforms = self.labels.filter(platform=True)
600 if len(platforms) == 0:
601 return None
602 return platforms[0]
603 platform.short_description = 'Platform'
mblighe8819cd2008-02-15 16:48:40 +0000604
605
showardcafd16e2009-05-29 18:37:49 +0000606 @classmethod
607 def check_no_platform(cls, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800608 """Verify the specified hosts have no associated platforms.
609
610 @param cls: Implicit class object.
611 @param hosts: The hosts to verify.
612 @raises model_logic.ValidationError if any hosts already have a
613 platform.
614 """
showardcafd16e2009-05-29 18:37:49 +0000615 Host.objects.populate_relationships(hosts, Label, 'label_list')
616 errors = []
617 for host in hosts:
618 platforms = [label.name for label in host.label_list
619 if label.platform]
620 if platforms:
621 # do a join, just in case this host has multiple platforms,
622 # we'll be able to see it
623 errors.append('Host %s already has a platform: %s' % (
624 host.hostname, ', '.join(platforms)))
625 if errors:
626 raise model_logic.ValidationError({'labels': '; '.join(errors)})
627
628
jadmanski0afbb632008-06-06 21:10:57 +0000629 def is_dead(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800630 """Returns whether the host is dead (has status repair failed)."""
jadmanski0afbb632008-06-06 21:10:57 +0000631 return self.status == Host.Status.REPAIR_FAILED
mbligh3cab4a72008-03-05 23:19:09 +0000632
633
jadmanski0afbb632008-06-06 21:10:57 +0000634 def active_queue_entry(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800635 """Returns the active queue entry for this host, or None if none."""
jadmanski0afbb632008-06-06 21:10:57 +0000636 active = list(self.hostqueueentry_set.filter(active=True))
637 if not active:
638 return None
639 assert len(active) == 1, ('More than one active entry for '
640 'host ' + self.hostname)
641 return active[0]
mblighe8819cd2008-02-15 16:48:40 +0000642
643
showardf8b19042009-05-12 17:22:49 +0000644 def _get_attribute_model_and_args(self, attribute):
645 return HostAttribute, dict(host=self, attribute=attribute)
showard0957a842009-05-11 19:25:08 +0000646
647
jadmanski0afbb632008-06-06 21:10:57 +0000648 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800649 """Metadata for the Host class."""
showardeab66ce2009-12-23 00:03:56 +0000650 db_table = 'afe_hosts'
mblighe8819cd2008-02-15 16:48:40 +0000651
showarda5288b42009-07-28 20:06:08 +0000652 def __unicode__(self):
653 return unicode(self.hostname)
mblighe8819cd2008-02-15 16:48:40 +0000654
655
MK Ryuacf35922014-10-03 14:56:49 -0700656class HostAttribute(dbmodels.Model, model_logic.ModelExtensions):
showard0957a842009-05-11 19:25:08 +0000657 """Arbitrary keyvals associated with hosts."""
658 host = dbmodels.ForeignKey(Host)
showarda5288b42009-07-28 20:06:08 +0000659 attribute = dbmodels.CharField(max_length=90)
660 value = dbmodels.CharField(max_length=300)
showard0957a842009-05-11 19:25:08 +0000661
662 objects = model_logic.ExtendedManager()
663
664 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800665 """Metadata for the HostAttribute class."""
showardeab66ce2009-12-23 00:03:56 +0000666 db_table = 'afe_host_attributes'
showard0957a842009-05-11 19:25:08 +0000667
668
showard7c785282008-05-29 19:45:12 +0000669class Test(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000670 """\
671 Required:
showard909c7a62008-07-15 21:52:38 +0000672 author: author name
673 description: description of the test
jadmanski0afbb632008-06-06 21:10:57 +0000674 name: test name
showard909c7a62008-07-15 21:52:38 +0000675 time: short, medium, long
676 test_class: This describes the class for your the test belongs in.
677 test_category: This describes the category for your tests
jadmanski0afbb632008-06-06 21:10:57 +0000678 test_type: Client or Server
679 path: path to pass to run_test()
showard909c7a62008-07-15 21:52:38 +0000680 sync_count: is a number >=1 (1 being the default). If it's 1, then it's an
681 async job. If it's >1 it's sync job for that number of machines
showard2bab8f42008-11-12 18:15:22 +0000682 i.e. if sync_count = 2 it is a sync job that requires two
683 machines.
jadmanski0afbb632008-06-06 21:10:57 +0000684 Optional:
showard909c7a62008-07-15 21:52:38 +0000685 dependencies: What the test requires to run. Comma deliminated list
showard989f25d2008-10-01 11:38:11 +0000686 dependency_labels: many-to-many relationship with labels corresponding to
687 test dependencies.
showard909c7a62008-07-15 21:52:38 +0000688 experimental: If this is set to True production servers will ignore the test
689 run_verify: Whether or not the scheduler should run the verify stage
Dan Shi07e09af2013-04-12 09:31:29 -0700690 run_reset: Whether or not the scheduler should run the reset stage
Aviv Keshet9af96d32013-03-05 12:56:24 -0800691 test_retry: Number of times to retry test if the test did not complete
692 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +0000693 """
showard909c7a62008-07-15 21:52:38 +0000694 TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
mblighe8819cd2008-02-15 16:48:40 +0000695
showarda5288b42009-07-28 20:06:08 +0000696 name = dbmodels.CharField(max_length=255, unique=True)
697 author = dbmodels.CharField(max_length=255)
698 test_class = dbmodels.CharField(max_length=255)
699 test_category = dbmodels.CharField(max_length=255)
700 dependencies = dbmodels.CharField(max_length=255, blank=True)
jadmanski0afbb632008-06-06 21:10:57 +0000701 description = dbmodels.TextField(blank=True)
showard909c7a62008-07-15 21:52:38 +0000702 experimental = dbmodels.BooleanField(default=True)
Dan Shi07e09af2013-04-12 09:31:29 -0700703 run_verify = dbmodels.BooleanField(default=False)
showard909c7a62008-07-15 21:52:38 +0000704 test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
705 default=TestTime.MEDIUM)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -0700706 test_type = dbmodels.SmallIntegerField(
707 choices=control_data.CONTROL_TYPE.choices())
showard909c7a62008-07-15 21:52:38 +0000708 sync_count = dbmodels.IntegerField(default=1)
showarda5288b42009-07-28 20:06:08 +0000709 path = dbmodels.CharField(max_length=255, unique=True)
Aviv Keshet9af96d32013-03-05 12:56:24 -0800710 test_retry = dbmodels.IntegerField(blank=True, default=0)
Dan Shi07e09af2013-04-12 09:31:29 -0700711 run_reset = dbmodels.BooleanField(default=True)
mblighe8819cd2008-02-15 16:48:40 +0000712
showardeab66ce2009-12-23 00:03:56 +0000713 dependency_labels = (
714 dbmodels.ManyToManyField(Label, blank=True,
715 db_table='afe_autotests_dependency_labels'))
jadmanski0afbb632008-06-06 21:10:57 +0000716 name_field = 'name'
717 objects = model_logic.ExtendedManager()
mblighe8819cd2008-02-15 16:48:40 +0000718
719
jamesren35a70222010-02-16 19:30:46 +0000720 def admin_description(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800721 """Returns a string representing the admin description."""
jamesren35a70222010-02-16 19:30:46 +0000722 escaped_description = saxutils.escape(self.description)
723 return '<span style="white-space:pre">%s</span>' % escaped_description
724 admin_description.allow_tags = True
jamesrencae88c62010-02-19 00:12:28 +0000725 admin_description.short_description = 'Description'
jamesren35a70222010-02-16 19:30:46 +0000726
727
jadmanski0afbb632008-06-06 21:10:57 +0000728 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800729 """Metadata for class Test."""
showardeab66ce2009-12-23 00:03:56 +0000730 db_table = 'afe_autotests'
mblighe8819cd2008-02-15 16:48:40 +0000731
showarda5288b42009-07-28 20:06:08 +0000732 def __unicode__(self):
733 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000734
735
jamesren4a41e012010-07-16 22:33:48 +0000736class TestParameter(dbmodels.Model):
737 """
738 A declared parameter of a test
739 """
740 test = dbmodels.ForeignKey(Test)
741 name = dbmodels.CharField(max_length=255)
742
743 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800744 """Metadata for class TestParameter."""
jamesren4a41e012010-07-16 22:33:48 +0000745 db_table = 'afe_test_parameters'
746 unique_together = ('test', 'name')
747
748 def __unicode__(self):
Eric Li0a993912011-05-17 12:56:25 -0700749 return u'%s (%s)' % (self.name, self.test.name)
jamesren4a41e012010-07-16 22:33:48 +0000750
751
showard2b9a88b2008-06-13 20:55:03 +0000752class Profiler(dbmodels.Model, model_logic.ModelExtensions):
753 """\
754 Required:
755 name: profiler name
756 test_type: Client or Server
757
758 Optional:
759 description: arbirary text description
760 """
showarda5288b42009-07-28 20:06:08 +0000761 name = dbmodels.CharField(max_length=255, unique=True)
showard2b9a88b2008-06-13 20:55:03 +0000762 description = dbmodels.TextField(blank=True)
763
764 name_field = 'name'
765 objects = model_logic.ExtendedManager()
766
767
768 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800769 """Metadata for class Profiler."""
showardeab66ce2009-12-23 00:03:56 +0000770 db_table = 'afe_profilers'
showard2b9a88b2008-06-13 20:55:03 +0000771
showarda5288b42009-07-28 20:06:08 +0000772 def __unicode__(self):
773 return unicode(self.name)
showard2b9a88b2008-06-13 20:55:03 +0000774
775
showard7c785282008-05-29 19:45:12 +0000776class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +0000777 """\
778 Required:
779 name: name of ACL group
mblighe8819cd2008-02-15 16:48:40 +0000780
jadmanski0afbb632008-06-06 21:10:57 +0000781 Optional:
782 description: arbitrary description of group
783 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -0700784
785 SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
786
showarda5288b42009-07-28 20:06:08 +0000787 name = dbmodels.CharField(max_length=255, unique=True)
788 description = dbmodels.CharField(max_length=255, blank=True)
showardeab66ce2009-12-23 00:03:56 +0000789 users = dbmodels.ManyToManyField(User, blank=False,
790 db_table='afe_acl_groups_users')
791 hosts = dbmodels.ManyToManyField(Host, blank=True,
792 db_table='afe_acl_groups_hosts')
mblighe8819cd2008-02-15 16:48:40 +0000793
jadmanski0afbb632008-06-06 21:10:57 +0000794 name_field = 'name'
795 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +0000796
showard08f981b2008-06-24 21:59:03 +0000797 @staticmethod
showard3dd47c22008-07-10 00:41:36 +0000798 def check_for_acl_violation_hosts(hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800799 """Verify the current user has access to the specified hosts.
800
801 @param hosts: The hosts to verify against.
802 @raises AclAccessViolation if the current user doesn't have access
803 to a host.
804 """
showard64a95952010-01-13 21:27:16 +0000805 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000806 if user.is_superuser():
showard9dbdcda2008-10-14 17:34:36 +0000807 return
showard3dd47c22008-07-10 00:41:36 +0000808 accessible_host_ids = set(
showardd9ac4452009-02-07 02:04:37 +0000809 host.id for host in Host.objects.filter(aclgroup__users=user))
showard3dd47c22008-07-10 00:41:36 +0000810 for host in hosts:
811 # Check if the user has access to this host,
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800812 # but only if it is not a metahost or a one-time-host.
showard98ead172009-06-22 18:13:24 +0000813 no_access = (isinstance(host, Host)
814 and not host.invalid
815 and int(host.id) not in accessible_host_ids)
816 if no_access:
showardeaa408e2009-09-11 18:45:31 +0000817 raise AclAccessViolation("%s does not have access to %s" %
818 (str(user), str(host)))
showard3dd47c22008-07-10 00:41:36 +0000819
showard9dbdcda2008-10-14 17:34:36 +0000820
821 @staticmethod
showarddc817512008-11-12 18:16:41 +0000822 def check_abort_permissions(queue_entries):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800823 """Look for queue entries that aren't abortable by the current user.
824
825 An entry is not abortable if:
826 * the job isn't owned by this user, and
showarddc817512008-11-12 18:16:41 +0000827 * the machine isn't ACL-accessible, or
828 * the machine is in the "Everyone" ACL
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800829
830 @param queue_entries: The queue entries to check.
831 @raises AclAccessViolation if a queue entry is not abortable by the
832 current user.
showarddc817512008-11-12 18:16:41 +0000833 """
showard64a95952010-01-13 21:27:16 +0000834 user = User.current_user()
showard9dbdcda2008-10-14 17:34:36 +0000835 if user.is_superuser():
836 return
showarddc817512008-11-12 18:16:41 +0000837 not_owned = queue_entries.exclude(job__owner=user.login)
838 # I do this using ID sets instead of just Django filters because
showarda5288b42009-07-28 20:06:08 +0000839 # filtering on M2M dbmodels is broken in Django 0.96. It's better in
840 # 1.0.
841 # TODO: Use Django filters, now that we're using 1.0.
showarddc817512008-11-12 18:16:41 +0000842 accessible_ids = set(
843 entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000844 in not_owned.filter(host__aclgroup__users__login=user.login))
showarddc817512008-11-12 18:16:41 +0000845 public_ids = set(entry.id for entry
showardd9ac4452009-02-07 02:04:37 +0000846 in not_owned.filter(host__aclgroup__name='Everyone'))
showarddc817512008-11-12 18:16:41 +0000847 cannot_abort = [entry for entry in not_owned.select_related()
848 if entry.id not in accessible_ids
849 or entry.id in public_ids]
850 if len(cannot_abort) == 0:
851 return
852 entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
showard3f15eed2008-11-14 22:40:48 +0000853 entry.host_or_metahost_name())
showarddc817512008-11-12 18:16:41 +0000854 for entry in cannot_abort)
855 raise AclAccessViolation('You cannot abort the following job entries: '
856 + entry_names)
showard9dbdcda2008-10-14 17:34:36 +0000857
858
showard3dd47c22008-07-10 00:41:36 +0000859 def check_for_acl_violation_acl_group(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800860 """Verifies the current user has acces to this ACL group.
861
862 @raises AclAccessViolation if the current user doesn't have access to
863 this ACL group.
864 """
showard64a95952010-01-13 21:27:16 +0000865 user = User.current_user()
showard3dd47c22008-07-10 00:41:36 +0000866 if user.is_superuser():
showard8cbaf1e2009-09-08 16:27:04 +0000867 return
868 if self.name == 'Everyone':
869 raise AclAccessViolation("You cannot modify 'Everyone'!")
showard3dd47c22008-07-10 00:41:36 +0000870 if not user in self.users.all():
871 raise AclAccessViolation("You do not have access to %s"
872 % self.name)
873
874 @staticmethod
showard08f981b2008-06-24 21:59:03 +0000875 def on_host_membership_change():
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800876 """Invoked when host membership changes."""
showard08f981b2008-06-24 21:59:03 +0000877 everyone = AclGroup.objects.get(name='Everyone')
878
showard3dd47c22008-07-10 00:41:36 +0000879 # find hosts that aren't in any ACL group and add them to Everyone
showard08f981b2008-06-24 21:59:03 +0000880 # TODO(showard): this is a bit of a hack, since the fact that this query
881 # works is kind of a coincidence of Django internals. This trick
882 # doesn't work in general (on all foreign key relationships). I'll
883 # replace it with a better technique when the need arises.
showardd9ac4452009-02-07 02:04:37 +0000884 orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
showard08f981b2008-06-24 21:59:03 +0000885 everyone.hosts.add(*orphaned_hosts.distinct())
886
887 # find hosts in both Everyone and another ACL group, and remove them
888 # from Everyone
showarda5288b42009-07-28 20:06:08 +0000889 hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
890 acled_hosts = set()
891 for host in hosts_in_everyone:
892 # Has an ACL group other than Everyone
893 if host.aclgroup_set.count() > 1:
894 acled_hosts.add(host)
895 everyone.hosts.remove(*acled_hosts)
showard08f981b2008-06-24 21:59:03 +0000896
897
898 def delete(self):
showard3dd47c22008-07-10 00:41:36 +0000899 if (self.name == 'Everyone'):
900 raise AclAccessViolation("You cannot delete 'Everyone'!")
901 self.check_for_acl_violation_acl_group()
showard08f981b2008-06-24 21:59:03 +0000902 super(AclGroup, self).delete()
903 self.on_host_membership_change()
904
905
showard04f2cd82008-07-25 20:53:31 +0000906 def add_current_user_if_empty(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800907 """Adds the current user if the set of users is empty."""
showard04f2cd82008-07-25 20:53:31 +0000908 if not self.users.count():
showard64a95952010-01-13 21:27:16 +0000909 self.users.add(User.current_user())
showard04f2cd82008-07-25 20:53:31 +0000910
911
showard8cbaf1e2009-09-08 16:27:04 +0000912 def perform_after_save(self, change):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800913 """Called after a save.
914
915 @param change: Whether there was a change.
916 """
showard8cbaf1e2009-09-08 16:27:04 +0000917 if not change:
showard64a95952010-01-13 21:27:16 +0000918 self.users.add(User.current_user())
showard8cbaf1e2009-09-08 16:27:04 +0000919 self.add_current_user_if_empty()
920 self.on_host_membership_change()
921
922
923 def save(self, *args, **kwargs):
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700924 change = bool(self.id)
925 if change:
showard8cbaf1e2009-09-08 16:27:04 +0000926 # Check the original object for an ACL violation
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700927 AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
showard8cbaf1e2009-09-08 16:27:04 +0000928 super(AclGroup, self).save(*args, **kwargs)
Jakob Juelich116ff0f2014-09-17 18:25:16 -0700929 self.perform_after_save(change)
showard8cbaf1e2009-09-08 16:27:04 +0000930
showardeb3be4d2008-04-21 20:59:26 +0000931
jadmanski0afbb632008-06-06 21:10:57 +0000932 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800933 """Metadata for class AclGroup."""
showardeab66ce2009-12-23 00:03:56 +0000934 db_table = 'afe_acl_groups'
mblighe8819cd2008-02-15 16:48:40 +0000935
showarda5288b42009-07-28 20:06:08 +0000936 def __unicode__(self):
937 return unicode(self.name)
mblighe8819cd2008-02-15 16:48:40 +0000938
mblighe8819cd2008-02-15 16:48:40 +0000939
jamesren4a41e012010-07-16 22:33:48 +0000940class Kernel(dbmodels.Model):
941 """
942 A kernel configuration for a parameterized job
943 """
944 version = dbmodels.CharField(max_length=255)
945 cmdline = dbmodels.CharField(max_length=255, blank=True)
946
947 @classmethod
948 def create_kernels(cls, kernel_list):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800949 """Creates all kernels in the kernel list.
jamesren4a41e012010-07-16 22:33:48 +0000950
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800951 @param cls: Implicit class object.
952 @param kernel_list: A list of dictionaries that describe the kernels,
953 in the same format as the 'kernel' argument to
954 rpc_interface.generate_control_file.
955 @return A list of the created kernels.
jamesren4a41e012010-07-16 22:33:48 +0000956 """
957 if not kernel_list:
958 return None
959 return [cls._create(kernel) for kernel in kernel_list]
960
961
962 @classmethod
963 def _create(cls, kernel_dict):
964 version = kernel_dict.pop('version')
965 cmdline = kernel_dict.pop('cmdline', '')
966
967 if kernel_dict:
968 raise Exception('Extraneous kernel arguments remain: %r'
969 % kernel_dict)
970
971 kernel, _ = cls.objects.get_or_create(version=version,
972 cmdline=cmdline)
973 return kernel
974
975
976 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800977 """Metadata for class Kernel."""
jamesren4a41e012010-07-16 22:33:48 +0000978 db_table = 'afe_kernels'
979 unique_together = ('version', 'cmdline')
980
981 def __unicode__(self):
982 return u'%s %s' % (self.version, self.cmdline)
983
984
985class ParameterizedJob(dbmodels.Model):
986 """
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -0800987 Auxiliary configuration for a parameterized job.
jamesren4a41e012010-07-16 22:33:48 +0000988 """
989 test = dbmodels.ForeignKey(Test)
990 label = dbmodels.ForeignKey(Label, null=True)
991 use_container = dbmodels.BooleanField(default=False)
992 profile_only = dbmodels.BooleanField(default=False)
993 upload_kernel_config = dbmodels.BooleanField(default=False)
994
995 kernels = dbmodels.ManyToManyField(
996 Kernel, db_table='afe_parameterized_job_kernels')
997 profilers = dbmodels.ManyToManyField(
998 Profiler, through='ParameterizedJobProfiler')
999
1000
1001 @classmethod
1002 def smart_get(cls, id_or_name, *args, **kwargs):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001003 """For compatibility with Job.add_object.
1004
1005 @param cls: Implicit class object.
1006 @param id_or_name: The ID or name to get.
1007 @param args: Non-keyword arguments.
1008 @param kwargs: Keyword arguments.
1009 """
jamesren4a41e012010-07-16 22:33:48 +00001010 return cls.objects.get(pk=id_or_name)
1011
1012
1013 def job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001014 """Returns the job if it exists, or else None."""
jamesren4a41e012010-07-16 22:33:48 +00001015 jobs = self.job_set.all()
1016 assert jobs.count() <= 1
1017 return jobs and jobs[0] or None
1018
1019
1020 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001021 """Metadata for class ParameterizedJob."""
jamesren4a41e012010-07-16 22:33:48 +00001022 db_table = 'afe_parameterized_jobs'
1023
1024 def __unicode__(self):
1025 return u'%s (parameterized) - %s' % (self.test.name, self.job())
1026
1027
1028class ParameterizedJobProfiler(dbmodels.Model):
1029 """
1030 A profiler to run on a parameterized job
1031 """
1032 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1033 profiler = dbmodels.ForeignKey(Profiler)
1034
1035 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001036 """Metedata for class ParameterizedJobProfiler."""
jamesren4a41e012010-07-16 22:33:48 +00001037 db_table = 'afe_parameterized_jobs_profilers'
1038 unique_together = ('parameterized_job', 'profiler')
1039
1040
1041class ParameterizedJobProfilerParameter(dbmodels.Model):
1042 """
1043 A parameter for a profiler in a parameterized job
1044 """
1045 parameterized_job_profiler = dbmodels.ForeignKey(ParameterizedJobProfiler)
1046 parameter_name = dbmodels.CharField(max_length=255)
1047 parameter_value = dbmodels.TextField()
1048 parameter_type = dbmodels.CharField(
1049 max_length=8, choices=model_attributes.ParameterTypes.choices())
1050
1051 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001052 """Metadata for class ParameterizedJobProfilerParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001053 db_table = 'afe_parameterized_job_profiler_parameters'
1054 unique_together = ('parameterized_job_profiler', 'parameter_name')
1055
1056 def __unicode__(self):
1057 return u'%s - %s' % (self.parameterized_job_profiler.profiler.name,
1058 self.parameter_name)
1059
1060
1061class ParameterizedJobParameter(dbmodels.Model):
1062 """
1063 Parameters for a parameterized job
1064 """
1065 parameterized_job = dbmodels.ForeignKey(ParameterizedJob)
1066 test_parameter = dbmodels.ForeignKey(TestParameter)
1067 parameter_value = dbmodels.TextField()
1068 parameter_type = dbmodels.CharField(
1069 max_length=8, choices=model_attributes.ParameterTypes.choices())
1070
1071 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001072 """Metadata for class ParameterizedJobParameter."""
jamesren4a41e012010-07-16 22:33:48 +00001073 db_table = 'afe_parameterized_job_parameters'
1074 unique_together = ('parameterized_job', 'test_parameter')
1075
1076 def __unicode__(self):
1077 return u'%s - %s' % (self.parameterized_job.job().name,
1078 self.test_parameter.name)
1079
1080
showard7c785282008-05-29 19:45:12 +00001081class JobManager(model_logic.ExtendedManager):
jadmanski0afbb632008-06-06 21:10:57 +00001082 'Custom manager to provide efficient status counts querying.'
1083 def get_status_counts(self, job_ids):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001084 """Returns a dict mapping the given job IDs to their status count dicts.
1085
1086 @param job_ids: A list of job IDs.
jadmanski0afbb632008-06-06 21:10:57 +00001087 """
1088 if not job_ids:
1089 return {}
1090 id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1091 cursor = connection.cursor()
1092 cursor.execute("""
showardd3dc1992009-04-22 21:01:40 +00001093 SELECT job_id, status, aborted, complete, COUNT(*)
showardeab66ce2009-12-23 00:03:56 +00001094 FROM afe_host_queue_entries
jadmanski0afbb632008-06-06 21:10:57 +00001095 WHERE job_id IN %s
showardd3dc1992009-04-22 21:01:40 +00001096 GROUP BY job_id, status, aborted, complete
jadmanski0afbb632008-06-06 21:10:57 +00001097 """ % id_list)
showard25aaf3f2009-06-08 23:23:40 +00001098 all_job_counts = dict((job_id, {}) for job_id in job_ids)
showardd3dc1992009-04-22 21:01:40 +00001099 for job_id, status, aborted, complete, count in cursor.fetchall():
showard25aaf3f2009-06-08 23:23:40 +00001100 job_dict = all_job_counts[job_id]
showardd3dc1992009-04-22 21:01:40 +00001101 full_status = HostQueueEntry.compute_full_status(status, aborted,
1102 complete)
showardb6d16622009-05-26 19:35:29 +00001103 job_dict.setdefault(full_status, 0)
showard25aaf3f2009-06-08 23:23:40 +00001104 job_dict[full_status] += count
jadmanski0afbb632008-06-06 21:10:57 +00001105 return all_job_counts
mblighe8819cd2008-02-15 16:48:40 +00001106
1107
showard7c785282008-05-29 19:45:12 +00001108class Job(dbmodels.Model, model_logic.ModelExtensions):
jadmanski0afbb632008-06-06 21:10:57 +00001109 """\
1110 owner: username of job owner
1111 name: job name (does not have to be unique)
Alex Miller7d658cf2013-09-04 16:00:35 -07001112 priority: Integer priority value. Higher is more important.
jadmanski0afbb632008-06-06 21:10:57 +00001113 control_file: contents of control file
1114 control_type: Client or Server
1115 created_on: date of job creation
1116 submitted_on: date of job submission
showard2bab8f42008-11-12 18:15:22 +00001117 synch_count: how many hosts should be used per autoserv execution
showard909c7a62008-07-15 21:52:38 +00001118 run_verify: Whether or not to run the verify phase
Dan Shi07e09af2013-04-12 09:31:29 -07001119 run_reset: Whether or not to run the reset phase
Simran Basi94dc0032013-11-12 14:09:46 -08001120 timeout: DEPRECATED - hours from queuing time until job times out
1121 timeout_mins: minutes from job queuing time until the job times out
Simran Basi34217022012-11-06 13:43:15 -08001122 max_runtime_hrs: DEPRECATED - hours from job starting time until job
1123 times out
1124 max_runtime_mins: minutes from job starting time until job times out
showard542e8402008-09-19 20:16:18 +00001125 email_list: list of people to email on completion delimited by any of:
1126 white space, ',', ':', ';'
showard989f25d2008-10-01 11:38:11 +00001127 dependency_labels: many-to-many relationship with labels corresponding to
1128 job dependencies
showard21baa452008-10-21 00:08:39 +00001129 reboot_before: Never, If dirty, or Always
1130 reboot_after: Never, If all tests passed, or Always
showarda1e74b32009-05-12 17:32:04 +00001131 parse_failed_repair: if True, a failed repair launched by this job will have
1132 its results parsed as part of the job.
jamesren76fcf192010-04-21 20:39:50 +00001133 drone_set: The set of drones to run this job on
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001134 parent_job: Parent job (optional)
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001135 test_retry: Number of times to retry test if the test did not complete
1136 successfully. (optional, default: 0)
jadmanski0afbb632008-06-06 21:10:57 +00001137 """
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001138
1139 # TODO: Investigate, if jobkeyval_set is really needed.
1140 # dynamic_suite will write them into an attached file for the drone, but
1141 # it doesn't seem like they are actually used. If they aren't used, remove
1142 # jobkeyval_set here.
1143 SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1144 'hostqueueentry_set',
1145 'jobkeyval_set',
1146 'shard'])
1147
1148
Jakob Juelichf88fa932014-09-03 17:58:04 -07001149 def _deserialize_relation(self, link, data):
1150 if link in ['hostqueueentry_set', 'jobkeyval_set']:
1151 for obj in data:
1152 obj['job_id'] = self.id
1153
1154 super(Job, self)._deserialize_relation(link, data)
1155
1156
1157 def custom_deserialize_relation(self, link, data):
Jakob Juelich116ff0f2014-09-17 18:25:16 -07001158 assert link == 'shard', 'Link %s should not be deserialized' % link
Jakob Juelichf88fa932014-09-03 17:58:04 -07001159 self.shard = Shard.deserialize(data)
1160
1161
Jakob Juelicha94efe62014-09-18 16:02:49 -07001162 def sanity_check_update_from_shard(self, shard, updated_serialized):
Jakob Juelich02e61292014-10-17 12:36:55 -07001163 # If the job got aborted on the master after the client fetched it
1164 # no shard_id will be set. The shard might still push updates though,
1165 # as the job might complete before the abort bit syncs to the shard.
1166 # Alternative considered: The master scheduler could be changed to not
1167 # set aborted jobs to completed that are sharded out. But that would
1168 # require database queries and seemed more complicated to implement.
1169 # This seems safe to do, as there won't be updates pushed from the wrong
1170 # shards should be powered off and wiped hen they are removed from the
1171 # master.
1172 if self.shard_id and self.shard_id != shard.id:
Jakob Juelicha94efe62014-09-18 16:02:49 -07001173 raise error.UnallowedRecordsSentToMaster(
1174 'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1175 'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1176 shard.id))
1177
1178
Simran Basi94dc0032013-11-12 14:09:46 -08001179 # TIMEOUT is deprecated.
showardb1e51872008-10-07 11:08:18 +00001180 DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
Simran Basi94dc0032013-11-12 14:09:46 -08001181 'AUTOTEST_WEB', 'job_timeout_default', default=24)
1182 DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1183 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
Simran Basi34217022012-11-06 13:43:15 -08001184 # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1185 # completed.
showard12f3e322009-05-13 21:27:42 +00001186 DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1187 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
Simran Basi34217022012-11-06 13:43:15 -08001188 DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1189 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
showarda1e74b32009-05-12 17:32:04 +00001190 DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1191 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool,
1192 default=False)
showardb1e51872008-10-07 11:08:18 +00001193
showarda5288b42009-07-28 20:06:08 +00001194 owner = dbmodels.CharField(max_length=255)
1195 name = dbmodels.CharField(max_length=255)
Alex Miller7d658cf2013-09-04 16:00:35 -07001196 priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
jamesren4a41e012010-07-16 22:33:48 +00001197 control_file = dbmodels.TextField(null=True, blank=True)
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001198 control_type = dbmodels.SmallIntegerField(
1199 choices=control_data.CONTROL_TYPE.choices(),
1200 blank=True, # to allow 0
1201 default=control_data.CONTROL_TYPE.CLIENT)
showard68c7aa02008-10-09 16:49:11 +00001202 created_on = dbmodels.DateTimeField()
Simran Basi8d89b642014-05-02 17:33:20 -07001203 synch_count = dbmodels.IntegerField(blank=True, default=0)
showardb1e51872008-10-07 11:08:18 +00001204 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
Dan Shi07e09af2013-04-12 09:31:29 -07001205 run_verify = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001206 email_list = dbmodels.CharField(max_length=250, blank=True)
showardeab66ce2009-12-23 00:03:56 +00001207 dependency_labels = (
1208 dbmodels.ManyToManyField(Label, blank=True,
1209 db_table='afe_jobs_dependency_labels'))
jamesrendd855242010-03-02 22:23:44 +00001210 reboot_before = dbmodels.SmallIntegerField(
1211 choices=model_attributes.RebootBefore.choices(), blank=True,
1212 default=DEFAULT_REBOOT_BEFORE)
1213 reboot_after = dbmodels.SmallIntegerField(
1214 choices=model_attributes.RebootAfter.choices(), blank=True,
1215 default=DEFAULT_REBOOT_AFTER)
showarda1e74b32009-05-12 17:32:04 +00001216 parse_failed_repair = dbmodels.BooleanField(
1217 default=DEFAULT_PARSE_FAILED_REPAIR)
Simran Basi34217022012-11-06 13:43:15 -08001218 # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1219 # completed.
showard12f3e322009-05-13 21:27:42 +00001220 max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
Simran Basi34217022012-11-06 13:43:15 -08001221 max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
jamesren76fcf192010-04-21 20:39:50 +00001222 drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001223
jamesren4a41e012010-07-16 22:33:48 +00001224 parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1225 blank=True)
1226
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08001227 parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
mblighe8819cd2008-02-15 16:48:40 +00001228
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001229 test_retry = dbmodels.IntegerField(blank=True, default=0)
1230
Dan Shi07e09af2013-04-12 09:31:29 -07001231 run_reset = dbmodels.BooleanField(default=True)
1232
Simran Basi94dc0032013-11-12 14:09:46 -08001233 timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1234
Jakob Juelich8421d592014-09-17 15:27:06 -07001235 # If this is None on the master, a slave should be found.
1236 # If this is None on a slave, it should be synced back to the master
Jakob Jülich92c06332014-08-25 19:06:57 +00001237 shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1238
jadmanski0afbb632008-06-06 21:10:57 +00001239 # custom manager
1240 objects = JobManager()
mblighe8819cd2008-02-15 16:48:40 +00001241
1242
Alex Millerec212252014-02-28 16:48:34 -08001243 @decorators.cached_property
1244 def labels(self):
1245 """All the labels of this job"""
1246 # We need to convert dependency_labels to a list, because all() gives us
1247 # back an iterator, and storing/caching an iterator means we'd only be
1248 # able to read from it once.
1249 return list(self.dependency_labels.all())
1250
1251
jadmanski0afbb632008-06-06 21:10:57 +00001252 def is_server_job(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001253 """Returns whether this job is of type server."""
Aviv Keshet3dd8beb2013-05-13 17:36:04 -07001254 return self.control_type == control_data.CONTROL_TYPE.SERVER
mblighe8819cd2008-02-15 16:48:40 +00001255
1256
jadmanski0afbb632008-06-06 21:10:57 +00001257 @classmethod
jamesren4a41e012010-07-16 22:33:48 +00001258 def parameterized_jobs_enabled(cls):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001259 """Returns whether parameterized jobs are enabled.
1260
1261 @param cls: Implicit class object.
1262 """
jamesren4a41e012010-07-16 22:33:48 +00001263 return global_config.global_config.get_config_value(
1264 'AUTOTEST_WEB', 'parameterized_jobs', type=bool)
1265
1266
1267 @classmethod
1268 def check_parameterized_job(cls, control_file, parameterized_job):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001269 """Checks that the job is valid given the global config settings.
jamesren4a41e012010-07-16 22:33:48 +00001270
1271 First, either control_file must be set, or parameterized_job must be
1272 set, but not both. Second, parameterized_job must be set if and only if
1273 the parameterized_jobs option in the global config is set to True.
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001274
1275 @param cls: Implict class object.
1276 @param control_file: A control file.
1277 @param parameterized_job: A parameterized job.
jamesren4a41e012010-07-16 22:33:48 +00001278 """
1279 if not (bool(control_file) ^ bool(parameterized_job)):
1280 raise Exception('Job must have either control file or '
1281 'parameterization, but not both')
1282
1283 parameterized_jobs_enabled = cls.parameterized_jobs_enabled()
1284 if control_file and parameterized_jobs_enabled:
1285 raise Exception('Control file specified, but parameterized jobs '
1286 'are enabled')
1287 if parameterized_job and not parameterized_jobs_enabled:
1288 raise Exception('Parameterized job specified, but parameterized '
1289 'jobs are not enabled')
1290
1291
1292 @classmethod
showarda1e74b32009-05-12 17:32:04 +00001293 def create(cls, owner, options, hosts):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001294 """Creates a job.
1295
1296 The job is created by taking some information (the listed args) and
1297 filling in the rest of the necessary information.
1298
1299 @param cls: Implicit class object.
1300 @param owner: The owner for the job.
1301 @param options: An options object.
1302 @param hosts: The hosts to use.
jadmanski0afbb632008-06-06 21:10:57 +00001303 """
showard3dd47c22008-07-10 00:41:36 +00001304 AclGroup.check_for_acl_violation_hosts(hosts)
showard4d233752010-01-20 19:06:40 +00001305
jamesren4a41e012010-07-16 22:33:48 +00001306 control_file = options.get('control_file')
1307 parameterized_job = options.get('parameterized_job')
jamesren4a41e012010-07-16 22:33:48 +00001308
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001309 # The current implementation of parameterized jobs requires that only
1310 # control files or parameterized jobs are used. Using the image
1311 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1312 # parameterized jobs and control files jobs, it does muck enough with
1313 # normal jobs by adding a parameterized id to them that this check will
1314 # fail. So for now we just skip this check.
1315 # cls.check_parameterized_job(control_file=control_file,
1316 # parameterized_job=parameterized_job)
showard4d233752010-01-20 19:06:40 +00001317 user = User.current_user()
1318 if options.get('reboot_before') is None:
1319 options['reboot_before'] = user.get_reboot_before_display()
1320 if options.get('reboot_after') is None:
1321 options['reboot_after'] = user.get_reboot_after_display()
1322
jamesren76fcf192010-04-21 20:39:50 +00001323 drone_set = DroneSet.resolve_name(options.get('drone_set'))
1324
Simran Basi94dc0032013-11-12 14:09:46 -08001325 if options.get('timeout_mins') is None and options.get('timeout'):
1326 options['timeout_mins'] = options['timeout'] * 60
1327
jadmanski0afbb632008-06-06 21:10:57 +00001328 job = cls.add_object(
showarda1e74b32009-05-12 17:32:04 +00001329 owner=owner,
1330 name=options['name'],
1331 priority=options['priority'],
jamesren4a41e012010-07-16 22:33:48 +00001332 control_file=control_file,
showarda1e74b32009-05-12 17:32:04 +00001333 control_type=options['control_type'],
1334 synch_count=options.get('synch_count'),
Simran Basi94dc0032013-11-12 14:09:46 -08001335 # timeout needs to be deleted in the future.
showarda1e74b32009-05-12 17:32:04 +00001336 timeout=options.get('timeout'),
Simran Basi94dc0032013-11-12 14:09:46 -08001337 timeout_mins=options.get('timeout_mins'),
Simran Basi34217022012-11-06 13:43:15 -08001338 max_runtime_mins=options.get('max_runtime_mins'),
showarda1e74b32009-05-12 17:32:04 +00001339 run_verify=options.get('run_verify'),
1340 email_list=options.get('email_list'),
1341 reboot_before=options.get('reboot_before'),
1342 reboot_after=options.get('reboot_after'),
1343 parse_failed_repair=options.get('parse_failed_repair'),
jamesren76fcf192010-04-21 20:39:50 +00001344 created_on=datetime.now(),
jamesren4a41e012010-07-16 22:33:48 +00001345 drone_set=drone_set,
Aviv Keshet18308922013-02-19 17:49:49 -08001346 parameterized_job=parameterized_job,
Aviv Keshetcd1ff9b2013-03-01 14:55:19 -08001347 parent_job=options.get('parent_job_id'),
Dan Shi07e09af2013-04-12 09:31:29 -07001348 test_retry=options.get('test_retry'),
1349 run_reset=options.get('run_reset'))
mblighe8819cd2008-02-15 16:48:40 +00001350
showarda1e74b32009-05-12 17:32:04 +00001351 job.dependency_labels = options['dependencies']
showardc1a98d12010-01-15 00:22:22 +00001352
jamesrend8b6e172010-04-16 23:45:00 +00001353 if options.get('keyvals'):
showardc1a98d12010-01-15 00:22:22 +00001354 for key, value in options['keyvals'].iteritems():
1355 JobKeyval.objects.create(job=job, key=key, value=value)
1356
jadmanski0afbb632008-06-06 21:10:57 +00001357 return job
mblighe8819cd2008-02-15 16:48:40 +00001358
1359
Jakob Juelich59cfe542014-09-02 16:37:46 -07001360 @classmethod
Jakob Juelich1b525742014-09-30 13:08:07 -07001361 def assign_to_shard(cls, shard, known_ids):
Jakob Juelich59cfe542014-09-02 16:37:46 -07001362 """Assigns unassigned jobs to a shard.
1363
Jakob Juelich1b525742014-09-30 13:08:07 -07001364 For all labels that have been assigned to this shard, all jobs that
1365 have this label, are assigned to this shard.
1366
1367 Jobs that are assigned to the shard but aren't already present on the
1368 shard are returned.
1369
Jakob Juelich59cfe542014-09-02 16:37:46 -07001370 @param shard: The shard to assign jobs to.
Jakob Juelich1b525742014-09-30 13:08:07 -07001371 @param known_ids: List of all ids of incomplete jobs, the shard already
1372 knows about.
1373 This is used to figure out which jobs should be sent
1374 to the shard. If shard_ids were used instead, jobs
1375 would only be transferred once, even if the client
1376 failed persisting them.
1377 The number of unfinished jobs usually lies in O(1000).
1378 Assuming one id takes 8 chars in the json, this means
1379 overhead that lies in the lower kilobyte range.
1380 A not in query with 5000 id's takes about 30ms.
1381
Jakob Juelich59cfe542014-09-02 16:37:46 -07001382 @returns The job objects that should be sent to the shard.
1383 """
1384 # Disclaimer: Concurrent heartbeats should not occur in today's setup.
1385 # If this changes or they are triggered manually, this applies:
1386 # Jobs may be returned more than once by concurrent calls of this
1387 # function, as there is a race condition between SELECT and UPDATE.
1388 job_ids = list(Job.objects.filter(
Jakob Juelich59cfe542014-09-02 16:37:46 -07001389 dependency_labels=shard.labels.all()
1390 ).exclude(
Jakob Juelich1b525742014-09-30 13:08:07 -07001391 id__in=known_ids,
1392 hostqueueentry__aborted=False
1393 ).exclude(
Jakob Juelich59cfe542014-09-02 16:37:46 -07001394 hostqueueentry__complete=True
1395 ).exclude(
1396 hostqueueentry__active=True
Jakob Juelich852ec0d2014-09-15 18:28:23 -07001397 ).distinct().values_list('pk', flat=True))
Jakob Juelich59cfe542014-09-02 16:37:46 -07001398 if job_ids:
1399 Job.objects.filter(pk__in=job_ids).update(shard=shard)
1400 return list(Job.objects.filter(pk__in=job_ids).all())
1401 return []
1402
1403
jamesren4a41e012010-07-16 22:33:48 +00001404 def save(self, *args, **kwargs):
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -08001405 # The current implementation of parameterized jobs requires that only
1406 # control files or parameterized jobs are used. Using the image
1407 # parameter on autoupdate_ParameterizedJob doesn't mix pure
1408 # parameterized jobs and control files jobs, it does muck enough with
1409 # normal jobs by adding a parameterized id to them that this check will
1410 # fail. So for now we just skip this check.
1411 # cls.check_parameterized_job(control_file=self.control_file,
1412 # parameterized_job=self.parameterized_job)
jamesren4a41e012010-07-16 22:33:48 +00001413 super(Job, self).save(*args, **kwargs)
1414
1415
showard29f7cd22009-04-29 21:16:24 +00001416 def queue(self, hosts, atomic_group=None, is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001417 """Enqueue a job on the given hosts.
1418
1419 @param hosts: The hosts to use.
1420 @param atomic_group: The associated atomic group.
1421 @param is_template: Whether the status should be "Template".
1422 """
showarda9545c02009-12-18 22:44:26 +00001423 if not hosts:
1424 if atomic_group:
1425 # No hosts or labels are required to queue an atomic group
1426 # Job. However, if they are given, we respect them below.
1427 atomic_group.enqueue_job(self, is_template=is_template)
1428 else:
1429 # hostless job
1430 entry = HostQueueEntry.create(job=self, is_template=is_template)
1431 entry.save()
1432 return
1433
jadmanski0afbb632008-06-06 21:10:57 +00001434 for host in hosts:
showard29f7cd22009-04-29 21:16:24 +00001435 host.enqueue_job(self, atomic_group=atomic_group,
1436 is_template=is_template)
1437
1438
1439 def create_recurring_job(self, start_date, loop_period, loop_count, owner):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001440 """Creates a recurring job.
1441
1442 @param start_date: The starting date of the job.
1443 @param loop_period: How often to re-run the job, in seconds.
1444 @param loop_count: The re-run count.
1445 @param owner: The owner of the job.
1446 """
showard29f7cd22009-04-29 21:16:24 +00001447 rec = RecurringRun(job=self, start_date=start_date,
1448 loop_period=loop_period,
1449 loop_count=loop_count,
1450 owner=User.objects.get(login=owner))
1451 rec.save()
1452 return rec.id
mblighe8819cd2008-02-15 16:48:40 +00001453
1454
jadmanski0afbb632008-06-06 21:10:57 +00001455 def user(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001456 """Gets the user of this job, or None if it doesn't exist."""
jadmanski0afbb632008-06-06 21:10:57 +00001457 try:
1458 return User.objects.get(login=self.owner)
1459 except self.DoesNotExist:
1460 return None
mblighe8819cd2008-02-15 16:48:40 +00001461
1462
showard64a95952010-01-13 21:27:16 +00001463 def abort(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001464 """Aborts this job."""
showard98863972008-10-29 21:14:56 +00001465 for queue_entry in self.hostqueueentry_set.all():
showard64a95952010-01-13 21:27:16 +00001466 queue_entry.abort()
showard98863972008-10-29 21:14:56 +00001467
1468
showardd1195652009-12-08 22:21:02 +00001469 def tag(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001470 """Returns a string tag for this job."""
showardd1195652009-12-08 22:21:02 +00001471 return '%s-%s' % (self.id, self.owner)
1472
1473
showardc1a98d12010-01-15 00:22:22 +00001474 def keyval_dict(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001475 """Returns all keyvals for this job as a dictionary."""
showardc1a98d12010-01-15 00:22:22 +00001476 return dict((keyval.key, keyval.value)
1477 for keyval in self.jobkeyval_set.all())
1478
1479
jadmanski0afbb632008-06-06 21:10:57 +00001480 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001481 """Metadata for class Job."""
showardeab66ce2009-12-23 00:03:56 +00001482 db_table = 'afe_jobs'
mblighe8819cd2008-02-15 16:48:40 +00001483
showarda5288b42009-07-28 20:06:08 +00001484 def __unicode__(self):
1485 return u'%s (%s-%s)' % (self.name, self.id, self.owner)
mblighe8819cd2008-02-15 16:48:40 +00001486
1487
showardc1a98d12010-01-15 00:22:22 +00001488class JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1489 """Keyvals associated with jobs"""
1490 job = dbmodels.ForeignKey(Job)
1491 key = dbmodels.CharField(max_length=90)
1492 value = dbmodels.CharField(max_length=300)
1493
1494 objects = model_logic.ExtendedManager()
1495
1496 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001497 """Metadata for class JobKeyval."""
showardc1a98d12010-01-15 00:22:22 +00001498 db_table = 'afe_job_keyvals'
1499
1500
showard7c785282008-05-29 19:45:12 +00001501class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001502 """Represents an ineligible host queue."""
jadmanski0afbb632008-06-06 21:10:57 +00001503 job = dbmodels.ForeignKey(Job)
1504 host = dbmodels.ForeignKey(Host)
mblighe8819cd2008-02-15 16:48:40 +00001505
jadmanski0afbb632008-06-06 21:10:57 +00001506 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001507
jadmanski0afbb632008-06-06 21:10:57 +00001508 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001509 """Metadata for class IneligibleHostQueue."""
showardeab66ce2009-12-23 00:03:56 +00001510 db_table = 'afe_ineligible_host_queues'
mblighe8819cd2008-02-15 16:48:40 +00001511
mblighe8819cd2008-02-15 16:48:40 +00001512
showard7c785282008-05-29 19:45:12 +00001513class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001514 """Represents a host queue entry."""
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001515
1516 SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
Jakob Juelichf865d332014-09-29 10:47:49 -07001517 SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted'])
Jakob Juelich3bb7c802014-09-02 16:31:11 -07001518
Jakob Juelichf88fa932014-09-03 17:58:04 -07001519
1520 def custom_deserialize_relation(self, link, data):
1521 assert link == 'meta_host'
1522 self.meta_host = Label.deserialize(data)
1523
1524
Jakob Juelicha94efe62014-09-18 16:02:49 -07001525 def sanity_check_update_from_shard(self, shard, updated_serialized,
1526 job_ids_sent):
1527 if self.job_id not in job_ids_sent:
1528 raise error.UnallowedRecordsSentToMaster(
1529 'Sent HostQueueEntry without corresponding '
1530 'job entry: %s' % updated_serialized)
1531
1532
showardeaa408e2009-09-11 18:45:31 +00001533 Status = host_queue_entry_states.Status
1534 ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
showardeab66ce2009-12-23 00:03:56 +00001535 COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
showarda3ab0d52008-11-03 19:03:47 +00001536
jadmanski0afbb632008-06-06 21:10:57 +00001537 job = dbmodels.ForeignKey(Job)
1538 host = dbmodels.ForeignKey(Host, blank=True, null=True)
showarda5288b42009-07-28 20:06:08 +00001539 status = dbmodels.CharField(max_length=255)
jadmanski0afbb632008-06-06 21:10:57 +00001540 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1541 db_column='meta_host')
1542 active = dbmodels.BooleanField(default=False)
1543 complete = dbmodels.BooleanField(default=False)
showardb8471e32008-07-03 19:51:08 +00001544 deleted = dbmodels.BooleanField(default=False)
showarda5288b42009-07-28 20:06:08 +00001545 execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1546 default='')
showard89f84db2009-03-12 20:39:13 +00001547 # If atomic_group is set, this is a virtual HostQueueEntry that will
1548 # be expanded into many actual hosts within the group at schedule time.
1549 atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
showardd3dc1992009-04-22 21:01:40 +00001550 aborted = dbmodels.BooleanField(default=False)
showardd3771cc2009-10-07 20:48:22 +00001551 started_on = dbmodels.DateTimeField(null=True, blank=True)
Fang Deng51599032014-06-23 17:24:27 -07001552 finished_on = dbmodels.DateTimeField(null=True, blank=True)
mblighe8819cd2008-02-15 16:48:40 +00001553
jadmanski0afbb632008-06-06 21:10:57 +00001554 objects = model_logic.ExtendedManager()
showardeb3be4d2008-04-21 20:59:26 +00001555
mblighe8819cd2008-02-15 16:48:40 +00001556
showard2bab8f42008-11-12 18:15:22 +00001557 def __init__(self, *args, **kwargs):
1558 super(HostQueueEntry, self).__init__(*args, **kwargs)
1559 self._record_attributes(['status'])
1560
1561
showard29f7cd22009-04-29 21:16:24 +00001562 @classmethod
1563 def create(cls, job, host=None, meta_host=None, atomic_group=None,
1564 is_template=False):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001565 """Creates a new host queue entry.
1566
1567 @param cls: Implicit class object.
1568 @param job: The associated job.
1569 @param host: The associated host.
1570 @param meta_host: The associated meta host.
1571 @param atomic_group: The associated atomic group.
1572 @param is_template: Whether the status should be "Template".
1573 """
showard29f7cd22009-04-29 21:16:24 +00001574 if is_template:
1575 status = cls.Status.TEMPLATE
1576 else:
1577 status = cls.Status.QUEUED
1578
1579 return cls(job=job, host=host, meta_host=meta_host,
1580 atomic_group=atomic_group, status=status)
1581
1582
showarda5288b42009-07-28 20:06:08 +00001583 def save(self, *args, **kwargs):
showard2bab8f42008-11-12 18:15:22 +00001584 self._set_active_and_complete()
showarda5288b42009-07-28 20:06:08 +00001585 super(HostQueueEntry, self).save(*args, **kwargs)
showard2bab8f42008-11-12 18:15:22 +00001586 self._check_for_updated_attributes()
1587
1588
showardc0ac3a72009-07-08 21:14:45 +00001589 def execution_path(self):
1590 """
1591 Path to this entry's results (relative to the base results directory).
1592 """
showardd1195652009-12-08 22:21:02 +00001593 return os.path.join(self.job.tag(), self.execution_subdir)
showardc0ac3a72009-07-08 21:14:45 +00001594
1595
showard3f15eed2008-11-14 22:40:48 +00001596 def host_or_metahost_name(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001597 """Returns the first non-None name found in priority order.
1598
1599 The priority order checked is: (1) host name; (2) meta host name; and
1600 (3) atomic group name.
1601 """
showard3f15eed2008-11-14 22:40:48 +00001602 if self.host:
1603 return self.host.hostname
showard7890e792009-07-28 20:10:20 +00001604 elif self.meta_host:
showard3f15eed2008-11-14 22:40:48 +00001605 return self.meta_host.name
showard7890e792009-07-28 20:10:20 +00001606 else:
1607 assert self.atomic_group, "no host, meta_host or atomic group!"
1608 return self.atomic_group.name
showard3f15eed2008-11-14 22:40:48 +00001609
1610
showard2bab8f42008-11-12 18:15:22 +00001611 def _set_active_and_complete(self):
showardd3dc1992009-04-22 21:01:40 +00001612 if self.status in self.ACTIVE_STATUSES:
showard2bab8f42008-11-12 18:15:22 +00001613 self.active, self.complete = True, False
1614 elif self.status in self.COMPLETE_STATUSES:
1615 self.active, self.complete = False, True
1616 else:
1617 self.active, self.complete = False, False
1618
1619
1620 def on_attribute_changed(self, attribute, old_value):
1621 assert attribute == 'status'
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001622 logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
1623 self.status)
showard2bab8f42008-11-12 18:15:22 +00001624
1625
jadmanski0afbb632008-06-06 21:10:57 +00001626 def is_meta_host_entry(self):
1627 'True if this is a entry has a meta_host instead of a host.'
1628 return self.host is None and self.meta_host is not None
mblighe8819cd2008-02-15 16:48:40 +00001629
showarda3ab0d52008-11-03 19:03:47 +00001630
Simran Basic1b26762013-06-26 14:23:21 -07001631 # This code is shared between rpc_interface and models.HostQueueEntry.
1632 # Sadly due to circular imports between the 2 (crbug.com/230100) making it
1633 # a class method was the best way to refactor it. Attempting to put it in
1634 # rpc_utils or a new utils module failed as that would require us to import
1635 # models.py but to call it from here we would have to import the utils.py
1636 # thus creating a cycle.
1637 @classmethod
1638 def abort_host_queue_entries(cls, host_queue_entries):
1639 """Aborts a collection of host_queue_entries.
1640
1641 Abort these host queue entry and all host queue entries of jobs created
1642 by them.
1643
1644 @param host_queue_entries: List of host queue entries we want to abort.
1645 """
1646 # This isn't completely immune to race conditions since it's not atomic,
1647 # but it should be safe given the scheduler's behavior.
1648
1649 # TODO(milleral): crbug.com/230100
1650 # The |abort_host_queue_entries| rpc does nearly exactly this,
1651 # however, trying to re-use the code generates some horrible
1652 # circular import error. I'd be nice to refactor things around
1653 # sometime so the code could be reused.
1654
1655 # Fixpoint algorithm to find the whole tree of HQEs to abort to
1656 # minimize the total number of database queries:
1657 children = set()
1658 new_children = set(host_queue_entries)
1659 while new_children:
1660 children.update(new_children)
1661 new_child_ids = [hqe.job_id for hqe in new_children]
1662 new_children = HostQueueEntry.objects.filter(
1663 job__parent_job__in=new_child_ids,
1664 complete=False, aborted=False).all()
1665 # To handle circular parental relationships
1666 new_children = set(new_children) - children
1667
1668 # Associate a user with the host queue entries that we're about
1669 # to abort so that we can look up who to blame for the aborts.
1670 now = datetime.now()
1671 user = User.current_user()
1672 aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
1673 aborted_by=user, aborted_on=now) for hqe in children]
1674 AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
1675 # Bulk update all of the HQEs to set the abort bit.
1676 child_ids = [hqe.id for hqe in children]
1677 HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
1678
1679
Scott Zawalski23041432013-04-17 07:39:09 -07001680 def abort(self):
Alex Millerdea67042013-04-22 17:23:34 -07001681 """ Aborts this host queue entry.
Simran Basic1b26762013-06-26 14:23:21 -07001682
Alex Millerdea67042013-04-22 17:23:34 -07001683 Abort this host queue entry and all host queue entries of jobs created by
1684 this one.
1685
1686 """
showardd3dc1992009-04-22 21:01:40 +00001687 if not self.complete and not self.aborted:
Simran Basi97582a22013-06-27 12:03:21 -07001688 HostQueueEntry.abort_host_queue_entries([self])
mblighe8819cd2008-02-15 16:48:40 +00001689
showardd3dc1992009-04-22 21:01:40 +00001690
1691 @classmethod
1692 def compute_full_status(cls, status, aborted, complete):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001693 """Returns a modified status msg if the host queue entry was aborted.
1694
1695 @param cls: Implicit class object.
1696 @param status: The original status message.
1697 @param aborted: Whether the host queue entry was aborted.
1698 @param complete: Whether the host queue entry was completed.
1699 """
showardd3dc1992009-04-22 21:01:40 +00001700 if aborted and not complete:
1701 return 'Aborted (%s)' % status
1702 return status
1703
1704
1705 def full_status(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001706 """Returns the full status of this host queue entry, as a string."""
showardd3dc1992009-04-22 21:01:40 +00001707 return self.compute_full_status(self.status, self.aborted,
1708 self.complete)
1709
1710
1711 def _postprocess_object_dict(self, object_dict):
1712 object_dict['full_status'] = self.full_status()
1713
1714
jadmanski0afbb632008-06-06 21:10:57 +00001715 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001716 """Metadata for class HostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001717 db_table = 'afe_host_queue_entries'
mblighe8819cd2008-02-15 16:48:40 +00001718
showard12f3e322009-05-13 21:27:42 +00001719
showard4c119042008-09-29 19:16:18 +00001720
showarda5288b42009-07-28 20:06:08 +00001721 def __unicode__(self):
showard12f3e322009-05-13 21:27:42 +00001722 hostname = None
1723 if self.host:
1724 hostname = self.host.hostname
showarda5288b42009-07-28 20:06:08 +00001725 return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
showard12f3e322009-05-13 21:27:42 +00001726
1727
showard4c119042008-09-29 19:16:18 +00001728class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001729 """Represents an aborted host queue entry."""
showard4c119042008-09-29 19:16:18 +00001730 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
1731 aborted_by = dbmodels.ForeignKey(User)
showard68c7aa02008-10-09 16:49:11 +00001732 aborted_on = dbmodels.DateTimeField()
showard4c119042008-09-29 19:16:18 +00001733
1734 objects = model_logic.ExtendedManager()
1735
showard68c7aa02008-10-09 16:49:11 +00001736
showarda5288b42009-07-28 20:06:08 +00001737 def save(self, *args, **kwargs):
showard68c7aa02008-10-09 16:49:11 +00001738 self.aborted_on = datetime.now()
showarda5288b42009-07-28 20:06:08 +00001739 super(AbortedHostQueueEntry, self).save(*args, **kwargs)
showard68c7aa02008-10-09 16:49:11 +00001740
showard4c119042008-09-29 19:16:18 +00001741 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001742 """Metadata for class AbortedHostQueueEntry."""
showardeab66ce2009-12-23 00:03:56 +00001743 db_table = 'afe_aborted_host_queue_entries'
showard29f7cd22009-04-29 21:16:24 +00001744
1745
1746class RecurringRun(dbmodels.Model, model_logic.ModelExtensions):
1747 """\
1748 job: job to use as a template
1749 owner: owner of the instantiated template
1750 start_date: Run the job at scheduled date
1751 loop_period: Re-run (loop) the job periodically
1752 (in every loop_period seconds)
1753 loop_count: Re-run (loop) count
1754 """
1755
1756 job = dbmodels.ForeignKey(Job)
1757 owner = dbmodels.ForeignKey(User)
1758 start_date = dbmodels.DateTimeField()
1759 loop_period = dbmodels.IntegerField(blank=True)
1760 loop_count = dbmodels.IntegerField(blank=True)
1761
1762 objects = model_logic.ExtendedManager()
1763
1764 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001765 """Metadata for class RecurringRun."""
showardeab66ce2009-12-23 00:03:56 +00001766 db_table = 'afe_recurring_run'
showard29f7cd22009-04-29 21:16:24 +00001767
showarda5288b42009-07-28 20:06:08 +00001768 def __unicode__(self):
1769 return u'RecurringRun(job %s, start %s, period %s, count %s)' % (
showard29f7cd22009-04-29 21:16:24 +00001770 self.job.id, self.start_date, self.loop_period, self.loop_count)
showard6d7b2ff2009-06-10 00:16:47 +00001771
1772
1773class SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
1774 """\
1775 Tasks to run on hosts at the next time they are in the Ready state. Use this
1776 for high-priority tasks, such as forced repair or forced reinstall.
1777
1778 host: host to run this task on
showard2fe3f1d2009-07-06 20:19:11 +00001779 task: special task to run
showard6d7b2ff2009-06-10 00:16:47 +00001780 time_requested: date and time the request for this task was made
1781 is_active: task is currently running
1782 is_complete: task has finished running
beeps8bb1f7d2013-08-05 01:30:09 -07001783 is_aborted: task was aborted
showard2fe3f1d2009-07-06 20:19:11 +00001784 time_started: date and time the task started
Dan Shid0725542014-06-23 15:34:27 -07001785 time_finished: date and time the task finished
showard2fe3f1d2009-07-06 20:19:11 +00001786 queue_entry: Host queue entry waiting on this task (or None, if task was not
1787 started in preparation of a job)
showard6d7b2ff2009-06-10 00:16:47 +00001788 """
Alex Millerdfff2fd2013-05-28 13:05:06 -07001789 Task = enum.Enum('Verify', 'Cleanup', 'Repair', 'Reset', 'Provision',
Dan Shi07e09af2013-04-12 09:31:29 -07001790 string_values=True)
showard6d7b2ff2009-06-10 00:16:47 +00001791
1792 host = dbmodels.ForeignKey(Host, blank=False, null=False)
showarda5288b42009-07-28 20:06:08 +00001793 task = dbmodels.CharField(max_length=64, choices=Task.choices(),
showard6d7b2ff2009-06-10 00:16:47 +00001794 blank=False, null=False)
jamesren76fcf192010-04-21 20:39:50 +00001795 requested_by = dbmodels.ForeignKey(User)
showard6d7b2ff2009-06-10 00:16:47 +00001796 time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
1797 null=False)
1798 is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
1799 is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
beeps8bb1f7d2013-08-05 01:30:09 -07001800 is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
showardc0ac3a72009-07-08 21:14:45 +00001801 time_started = dbmodels.DateTimeField(null=True, blank=True)
showard2fe3f1d2009-07-06 20:19:11 +00001802 queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
showarde60e44e2009-11-13 20:45:38 +00001803 success = dbmodels.BooleanField(default=False, blank=False, null=False)
Dan Shid0725542014-06-23 15:34:27 -07001804 time_finished = dbmodels.DateTimeField(null=True, blank=True)
showard6d7b2ff2009-06-10 00:16:47 +00001805
1806 objects = model_logic.ExtendedManager()
1807
1808
showard9bb960b2009-11-19 01:02:11 +00001809 def save(self, **kwargs):
1810 if self.queue_entry:
1811 self.requested_by = User.objects.get(
1812 login=self.queue_entry.job.owner)
1813 super(SpecialTask, self).save(**kwargs)
1814
1815
showarded2afea2009-07-07 20:54:07 +00001816 def execution_path(self):
Prashanth Balasubramaniande87dea2014-11-09 17:47:10 -08001817 """Get the execution path of the SpecialTask.
1818
1819 This method returns different paths depending on where a
1820 the task ran:
1821 * Master: hosts/hostname/task_id-task_type
1822 * Shard: Master_path/time_created
1823 This is to work around the fact that a shard can fail independent
1824 of the master, and be replaced by another shard that has the same
1825 hosts. Without the time_created stamp the logs of the tasks running
1826 on the second shard will clobber the logs from the first in google
1827 storage, because task ids are not globally unique.
1828
1829 @return: An execution path for the task.
1830 """
1831 results_path = 'hosts/%s/%s-%s' % (self.host.hostname, self.id,
1832 self.task.lower())
1833
1834 # If we do this on the master it will break backward compatibility,
1835 # as there are tasks that currently don't have timestamps. If a host
1836 # or job has been sent to a shard, the rpc for that host/job will
1837 # be redirected to the shard, so this global_config check will happen
1838 # on the shard the logs are on.
1839 is_shard = global_config.global_config.get_config_value(
1840 'SHARD', 'shard_hostname', type=str, default='')
1841 if not is_shard:
1842 return results_path
1843
1844 # Generate a uid to disambiguate special task result directories
1845 # in case this shard fails. The simplest uid is the job_id, however
1846 # in rare cases tasks do not have jobs associated with them (eg:
1847 # frontend verify), so just use the creation timestamp. The clocks
1848 # between a shard and master should always be in sync. Any discrepancies
1849 # will be brought to our attention in the form of job timeouts.
1850 uid = self.time_requested.strftime('%Y%d%m%H%M%S')
1851
1852 # TODO: This is a hack, however it is the easiest way to achieve
1853 # correctness. There is currently some debate over the future of
1854 # tasks in our infrastructure and refactoring everything right
1855 # now isn't worth the time.
1856 return '%s/%s' % (results_path, uid)
showarded2afea2009-07-07 20:54:07 +00001857
1858
showardc0ac3a72009-07-08 21:14:45 +00001859 # property to emulate HostQueueEntry.status
1860 @property
1861 def status(self):
1862 """
1863 Return a host queue entry status appropriate for this task. Although
1864 SpecialTasks are not HostQueueEntries, it is helpful to the user to
1865 present similar statuses.
1866 """
1867 if self.is_complete:
showarde60e44e2009-11-13 20:45:38 +00001868 if self.success:
1869 return HostQueueEntry.Status.COMPLETED
1870 return HostQueueEntry.Status.FAILED
showardc0ac3a72009-07-08 21:14:45 +00001871 if self.is_active:
1872 return HostQueueEntry.Status.RUNNING
1873 return HostQueueEntry.Status.QUEUED
1874
1875
1876 # property to emulate HostQueueEntry.started_on
1877 @property
1878 def started_on(self):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001879 """Returns the time at which this special task started."""
showardc0ac3a72009-07-08 21:14:45 +00001880 return self.time_started
1881
1882
showard6d7b2ff2009-06-10 00:16:47 +00001883 @classmethod
showardc5103442010-01-15 00:20:26 +00001884 def schedule_special_task(cls, host, task):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001885 """Schedules a special task on a host if not already scheduled.
1886
1887 @param cls: Implicit class object.
1888 @param host: The host to use.
1889 @param task: The task to schedule.
showard6d7b2ff2009-06-10 00:16:47 +00001890 """
showardc5103442010-01-15 00:20:26 +00001891 existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
1892 is_active=False,
1893 is_complete=False)
1894 if existing_tasks:
1895 return existing_tasks[0]
1896
1897 special_task = SpecialTask(host=host, task=task,
1898 requested_by=User.current_user())
1899 special_task.save()
1900 return special_task
showard2fe3f1d2009-07-06 20:19:11 +00001901
1902
beeps8bb1f7d2013-08-05 01:30:09 -07001903 def abort(self):
1904 """ Abort this special task."""
1905 self.is_aborted = True
1906 self.save()
1907
1908
showarded2afea2009-07-07 20:54:07 +00001909 def activate(self):
showard474d1362009-08-20 23:32:01 +00001910 """
1911 Sets a task as active and sets the time started to the current time.
showard2fe3f1d2009-07-06 20:19:11 +00001912 """
showard97446882009-07-20 22:37:28 +00001913 logging.info('Starting: %s', self)
showard2fe3f1d2009-07-06 20:19:11 +00001914 self.is_active = True
1915 self.time_started = datetime.now()
1916 self.save()
1917
1918
showarde60e44e2009-11-13 20:45:38 +00001919 def finish(self, success):
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001920 """Sets a task as completed.
1921
1922 @param success: Whether or not the task was successful.
showard2fe3f1d2009-07-06 20:19:11 +00001923 """
showard97446882009-07-20 22:37:28 +00001924 logging.info('Finished: %s', self)
showarded2afea2009-07-07 20:54:07 +00001925 self.is_active = False
showard2fe3f1d2009-07-06 20:19:11 +00001926 self.is_complete = True
showarde60e44e2009-11-13 20:45:38 +00001927 self.success = success
Dan Shid85d6112014-07-14 10:32:55 -07001928 if self.time_started:
1929 self.time_finished = datetime.now()
showard2fe3f1d2009-07-06 20:19:11 +00001930 self.save()
showard6d7b2ff2009-06-10 00:16:47 +00001931
1932
1933 class Meta:
Dennis Jeffrey7db38ba2013-02-13 10:03:17 -08001934 """Metadata for class SpecialTask."""
showardeab66ce2009-12-23 00:03:56 +00001935 db_table = 'afe_special_tasks'
showard6d7b2ff2009-06-10 00:16:47 +00001936
showard474d1362009-08-20 23:32:01 +00001937
showarda5288b42009-07-28 20:06:08 +00001938 def __unicode__(self):
1939 result = u'Special Task %s (host %s, task %s, time %s)' % (
showarded2afea2009-07-07 20:54:07 +00001940 self.id, self.host, self.task, self.time_requested)
showard6d7b2ff2009-06-10 00:16:47 +00001941 if self.is_complete:
showarda5288b42009-07-28 20:06:08 +00001942 result += u' (completed)'
showard6d7b2ff2009-06-10 00:16:47 +00001943 elif self.is_active:
showarda5288b42009-07-28 20:06:08 +00001944 result += u' (active)'
showard6d7b2ff2009-06-10 00:16:47 +00001945
1946 return result