xref: /aosp_15_r20/external/autotest/frontend/afe/models.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# pylint: disable=missing-docstring
2*9c5db199SXin Li
3*9c5db199SXin Liimport contextlib
4*9c5db199SXin Lifrom datetime import datetime
5*9c5db199SXin Lifrom datetime import timedelta
6*9c5db199SXin Liimport logging
7*9c5db199SXin Liimport os
8*9c5db199SXin Li
9*9c5db199SXin Liimport django.core
10*9c5db199SXin Liimport six
11*9c5db199SXin Litry:
12*9c5db199SXin Li    from django.db import models as dbmodels, connection
13*9c5db199SXin Liexcept django.core.exceptions.ImproperlyConfigured:
14*9c5db199SXin Li    raise ImportError('Django database not yet configured. Import either '
15*9c5db199SXin Li                       'setup_django_environment or '
16*9c5db199SXin Li                       'setup_django_lite_environment from '
17*9c5db199SXin Li                       'autotest_lib.frontend before any imports that '
18*9c5db199SXin Li                       'depend on django models.')
19*9c5db199SXin Lifrom xml.sax import saxutils
20*9c5db199SXin Liimport common
21*9c5db199SXin Lifrom autotest_lib.frontend.afe import model_logic, model_attributes
22*9c5db199SXin Lifrom autotest_lib.frontend.afe import rdb_model_extensions
23*9c5db199SXin Lifrom autotest_lib.frontend import settings, thread_local
24*9c5db199SXin Lifrom autotest_lib.client.common_lib import autotest_enum, error, host_protections
25*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config
26*9c5db199SXin Lifrom autotest_lib.client.common_lib import host_queue_entry_states
27*9c5db199SXin Lifrom autotest_lib.client.common_lib import control_data, priorities, decorators
28*9c5db199SXin Lifrom autotest_lib.server import utils as server_utils
29*9c5db199SXin Li
30*9c5db199SXin Li# job options and user preferences
31*9c5db199SXin LiDEFAULT_REBOOT_BEFORE = model_attributes.RebootBefore.IF_DIRTY
32*9c5db199SXin LiDEFAULT_REBOOT_AFTER = model_attributes.RebootBefore.NEVER
33*9c5db199SXin Li
34*9c5db199SXin LiRESPECT_STATIC_LABELS = global_config.global_config.get_config_value(
35*9c5db199SXin Li        'SKYLAB', 'respect_static_labels', type=bool, default=False)
36*9c5db199SXin Li
37*9c5db199SXin LiRESPECT_STATIC_ATTRIBUTES = global_config.global_config.get_config_value(
38*9c5db199SXin Li        'SKYLAB', 'respect_static_attributes', type=bool, default=False)
39*9c5db199SXin Li
40*9c5db199SXin Li
41*9c5db199SXin Liclass AclAccessViolation(Exception):
42*9c5db199SXin Li    """\
43*9c5db199SXin Li    Raised when an operation is attempted with proper permissions as
44*9c5db199SXin Li    dictated by ACLs.
45*9c5db199SXin Li    """
46*9c5db199SXin Li
47*9c5db199SXin Li
48*9c5db199SXin Liclass AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model):
49*9c5db199SXin Li    """\
50*9c5db199SXin Li    An atomic group defines a collection of hosts which must only be scheduled
51*9c5db199SXin Li    all at once.  Any host with a label having an atomic group will only be
52*9c5db199SXin Li    scheduled for a job at the same time as other hosts sharing that label.
53*9c5db199SXin Li
54*9c5db199SXin Li    Required:
55*9c5db199SXin Li      name: A name for this atomic group, e.g. 'rack23' or 'funky_net'.
56*9c5db199SXin Li      max_number_of_machines: The maximum number of machines that will be
57*9c5db199SXin Li              scheduled at once when scheduling jobs to this atomic group.
58*9c5db199SXin Li              The job.synch_count is considered the minimum.
59*9c5db199SXin Li
60*9c5db199SXin Li    Optional:
61*9c5db199SXin Li      description: Arbitrary text description of this group's purpose.
62*9c5db199SXin Li    """
63*9c5db199SXin Li    name = dbmodels.CharField(max_length=255, unique=True)
64*9c5db199SXin Li    description = dbmodels.TextField(blank=True)
65*9c5db199SXin Li    # This magic value is the default to simplify the scheduler logic.
66*9c5db199SXin Li    # It must be "large".  The common use of atomic groups is to want all
67*9c5db199SXin Li    # machines in the group to be used, limits on which subset used are
68*9c5db199SXin Li    # often chosen via dependency labels.
69*9c5db199SXin Li    # TODO(dennisjeffrey): Revisit this so we don't have to assume that
70*9c5db199SXin Li    # "infinity" is around 3.3 million.
71*9c5db199SXin Li    INFINITE_MACHINES = 333333333
72*9c5db199SXin Li    max_number_of_machines = dbmodels.IntegerField(default=INFINITE_MACHINES)
73*9c5db199SXin Li    invalid = dbmodels.BooleanField(default=False,
74*9c5db199SXin Li                                  editable=settings.FULL_ADMIN)
75*9c5db199SXin Li
76*9c5db199SXin Li    name_field = 'name'
77*9c5db199SXin Li    objects = model_logic.ModelWithInvalidManager()
78*9c5db199SXin Li    valid_objects = model_logic.ValidObjectsManager()
79*9c5db199SXin Li
80*9c5db199SXin Li
81*9c5db199SXin Li    def enqueue_job(self, job, is_template=False):
82*9c5db199SXin Li        """Enqueue a job on an associated atomic group of hosts.
83*9c5db199SXin Li
84*9c5db199SXin Li        @param job: A job to enqueue.
85*9c5db199SXin Li        @param is_template: Whether the status should be "Template".
86*9c5db199SXin Li        """
87*9c5db199SXin Li        queue_entry = HostQueueEntry.create(atomic_group=self, job=job,
88*9c5db199SXin Li                                            is_template=is_template)
89*9c5db199SXin Li        queue_entry.save()
90*9c5db199SXin Li
91*9c5db199SXin Li
92*9c5db199SXin Li    def clean_object(self):
93*9c5db199SXin Li        self.label_set.clear()
94*9c5db199SXin Li
95*9c5db199SXin Li
96*9c5db199SXin Li    class Meta:
97*9c5db199SXin Li        """Metadata for class AtomicGroup."""
98*9c5db199SXin Li        db_table = 'afe_atomic_groups'
99*9c5db199SXin Li
100*9c5db199SXin Li
101*9c5db199SXin Li    def __unicode__(self):
102*9c5db199SXin Li        return unicode(self.name)
103*9c5db199SXin Li
104*9c5db199SXin Li
105*9c5db199SXin Liclass Label(model_logic.ModelWithInvalid, dbmodels.Model):
106*9c5db199SXin Li    """\
107*9c5db199SXin Li    Required:
108*9c5db199SXin Li      name: label name
109*9c5db199SXin Li
110*9c5db199SXin Li    Optional:
111*9c5db199SXin Li      kernel_config: URL/path to kernel config for jobs run on this label.
112*9c5db199SXin Li      platform: If True, this is a platform label (defaults to False).
113*9c5db199SXin Li      only_if_needed: If True, a Host with this label can only be used if that
114*9c5db199SXin Li              label is requested by the job/test (either as the meta_host or
115*9c5db199SXin Li              in the job_dependencies).
116*9c5db199SXin Li      atomic_group: The atomic group associated with this label.
117*9c5db199SXin Li    """
118*9c5db199SXin Li    name = dbmodels.CharField(max_length=255, unique=True)
119*9c5db199SXin Li    kernel_config = dbmodels.CharField(max_length=255, blank=True)
120*9c5db199SXin Li    platform = dbmodels.BooleanField(default=False)
121*9c5db199SXin Li    invalid = dbmodels.BooleanField(default=False,
122*9c5db199SXin Li                                    editable=settings.FULL_ADMIN)
123*9c5db199SXin Li    only_if_needed = dbmodels.BooleanField(default=False)
124*9c5db199SXin Li
125*9c5db199SXin Li    name_field = 'name'
126*9c5db199SXin Li    objects = model_logic.ModelWithInvalidManager()
127*9c5db199SXin Li    valid_objects = model_logic.ValidObjectsManager()
128*9c5db199SXin Li    atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
129*9c5db199SXin Li
130*9c5db199SXin Li
131*9c5db199SXin Li    def clean_object(self):
132*9c5db199SXin Li        self.host_set.clear()
133*9c5db199SXin Li        self.test_set.clear()
134*9c5db199SXin Li
135*9c5db199SXin Li
136*9c5db199SXin Li    def enqueue_job(self, job, is_template=False):
137*9c5db199SXin Li        """Enqueue a job on any host of this label.
138*9c5db199SXin Li
139*9c5db199SXin Li        @param job: A job to enqueue.
140*9c5db199SXin Li        @param is_template: Whether the status should be "Template".
141*9c5db199SXin Li        """
142*9c5db199SXin Li        queue_entry = HostQueueEntry.create(meta_host=self, job=job,
143*9c5db199SXin Li                                            is_template=is_template)
144*9c5db199SXin Li        queue_entry.save()
145*9c5db199SXin Li
146*9c5db199SXin Li
147*9c5db199SXin Li
148*9c5db199SXin Li    class Meta:
149*9c5db199SXin Li        """Metadata for class Label."""
150*9c5db199SXin Li        db_table = 'afe_labels'
151*9c5db199SXin Li
152*9c5db199SXin Li
153*9c5db199SXin Li    def __unicode__(self):
154*9c5db199SXin Li        return unicode(self.name)
155*9c5db199SXin Li
156*9c5db199SXin Li
157*9c5db199SXin Li    def is_replaced_by_static(self):
158*9c5db199SXin Li        """Detect whether a label is replaced by a static label.
159*9c5db199SXin Li
160*9c5db199SXin Li        'Static' means it can only be modified by skylab inventory tools.
161*9c5db199SXin Li        """
162*9c5db199SXin Li        if RESPECT_STATIC_LABELS:
163*9c5db199SXin Li            replaced = ReplacedLabel.objects.filter(label__id=self.id)
164*9c5db199SXin Li            if len(replaced) > 0:
165*9c5db199SXin Li                return True
166*9c5db199SXin Li
167*9c5db199SXin Li        return False
168*9c5db199SXin Li
169*9c5db199SXin Li
170*9c5db199SXin Liclass StaticLabel(model_logic.ModelWithInvalid, dbmodels.Model):
171*9c5db199SXin Li    """\
172*9c5db199SXin Li    Required:
173*9c5db199SXin Li      name: label name
174*9c5db199SXin Li
175*9c5db199SXin Li    Optional:
176*9c5db199SXin Li      kernel_config: URL/path to kernel config for jobs run on this label.
177*9c5db199SXin Li      platform: If True, this is a platform label (defaults to False).
178*9c5db199SXin Li      only_if_needed: Deprecated. This is always False.
179*9c5db199SXin Li      atomic_group: Deprecated. This is always NULL.
180*9c5db199SXin Li    """
181*9c5db199SXin Li    name = dbmodels.CharField(max_length=255, unique=True)
182*9c5db199SXin Li    kernel_config = dbmodels.CharField(max_length=255, blank=True)
183*9c5db199SXin Li    platform = dbmodels.BooleanField(default=False)
184*9c5db199SXin Li    invalid = dbmodels.BooleanField(default=False,
185*9c5db199SXin Li                                    editable=settings.FULL_ADMIN)
186*9c5db199SXin Li    only_if_needed = dbmodels.BooleanField(default=False)
187*9c5db199SXin Li
188*9c5db199SXin Li    name_field = 'name'
189*9c5db199SXin Li    objects = model_logic.ModelWithInvalidManager()
190*9c5db199SXin Li    valid_objects = model_logic.ValidObjectsManager()
191*9c5db199SXin Li    atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True)
192*9c5db199SXin Li
193*9c5db199SXin Li    def clean_object(self):
194*9c5db199SXin Li        self.host_set.clear()
195*9c5db199SXin Li        self.test_set.clear()
196*9c5db199SXin Li
197*9c5db199SXin Li
198*9c5db199SXin Li    class Meta:
199*9c5db199SXin Li        """Metadata for class StaticLabel."""
200*9c5db199SXin Li        db_table = 'afe_static_labels'
201*9c5db199SXin Li
202*9c5db199SXin Li
203*9c5db199SXin Li    def __unicode__(self):
204*9c5db199SXin Li        return unicode(self.name)
205*9c5db199SXin Li
206*9c5db199SXin Li
207*9c5db199SXin Liclass ReplacedLabel(dbmodels.Model, model_logic.ModelExtensions):
208*9c5db199SXin Li    """The tag to indicate Whether to replace labels with static labels."""
209*9c5db199SXin Li    label = dbmodels.ForeignKey(Label)
210*9c5db199SXin Li    objects = model_logic.ExtendedManager()
211*9c5db199SXin Li
212*9c5db199SXin Li
213*9c5db199SXin Li    class Meta:
214*9c5db199SXin Li        """Metadata for class ReplacedLabel."""
215*9c5db199SXin Li        db_table = 'afe_replaced_labels'
216*9c5db199SXin Li
217*9c5db199SXin Li
218*9c5db199SXin Li    def __unicode__(self):
219*9c5db199SXin Li        return unicode(self.label)
220*9c5db199SXin Li
221*9c5db199SXin Li
222*9c5db199SXin Liclass Shard(dbmodels.Model, model_logic.ModelExtensions):
223*9c5db199SXin Li
224*9c5db199SXin Li    hostname = dbmodels.CharField(max_length=255, unique=True)
225*9c5db199SXin Li
226*9c5db199SXin Li    name_field = 'hostname'
227*9c5db199SXin Li
228*9c5db199SXin Li    labels = dbmodels.ManyToManyField(Label, blank=True,
229*9c5db199SXin Li                                      db_table='afe_shards_labels')
230*9c5db199SXin Li
231*9c5db199SXin Li    class Meta:
232*9c5db199SXin Li        """Metadata for class ParameterizedJob."""
233*9c5db199SXin Li        db_table = 'afe_shards'
234*9c5db199SXin Li
235*9c5db199SXin Li
236*9c5db199SXin Liclass Drone(dbmodels.Model, model_logic.ModelExtensions):
237*9c5db199SXin Li    """
238*9c5db199SXin Li    A scheduler drone
239*9c5db199SXin Li
240*9c5db199SXin Li    hostname: the drone's hostname
241*9c5db199SXin Li    """
242*9c5db199SXin Li    hostname = dbmodels.CharField(max_length=255, unique=True)
243*9c5db199SXin Li
244*9c5db199SXin Li    name_field = 'hostname'
245*9c5db199SXin Li    objects = model_logic.ExtendedManager()
246*9c5db199SXin Li
247*9c5db199SXin Li
248*9c5db199SXin Li    def save(self, *args, **kwargs):
249*9c5db199SXin Li        if not User.current_user().is_superuser():
250*9c5db199SXin Li            raise Exception('Only superusers may edit drones')
251*9c5db199SXin Li        super(Drone, self).save(*args, **kwargs)
252*9c5db199SXin Li
253*9c5db199SXin Li
254*9c5db199SXin Li    def delete(self):
255*9c5db199SXin Li        if not User.current_user().is_superuser():
256*9c5db199SXin Li            raise Exception('Only superusers may delete drones')
257*9c5db199SXin Li        super(Drone, self).delete()
258*9c5db199SXin Li
259*9c5db199SXin Li
260*9c5db199SXin Li    class Meta:
261*9c5db199SXin Li        """Metadata for class Drone."""
262*9c5db199SXin Li        db_table = 'afe_drones'
263*9c5db199SXin Li
264*9c5db199SXin Li    def __unicode__(self):
265*9c5db199SXin Li        return unicode(self.hostname)
266*9c5db199SXin Li
267*9c5db199SXin Li
268*9c5db199SXin Liclass DroneSet(dbmodels.Model, model_logic.ModelExtensions):
269*9c5db199SXin Li    """
270*9c5db199SXin Li    A set of scheduler drones
271*9c5db199SXin Li
272*9c5db199SXin Li    These will be used by the scheduler to decide what drones a job is allowed
273*9c5db199SXin Li    to run on.
274*9c5db199SXin Li
275*9c5db199SXin Li    name: the drone set's name
276*9c5db199SXin Li    drones: the drones that are part of the set
277*9c5db199SXin Li    """
278*9c5db199SXin Li    DRONE_SETS_ENABLED = global_config.global_config.get_config_value(
279*9c5db199SXin Li            'SCHEDULER', 'drone_sets_enabled', type=bool, default=False)
280*9c5db199SXin Li    DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value(
281*9c5db199SXin Li            'SCHEDULER', 'default_drone_set_name', default=None)
282*9c5db199SXin Li
283*9c5db199SXin Li    name = dbmodels.CharField(max_length=255, unique=True)
284*9c5db199SXin Li    drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones')
285*9c5db199SXin Li
286*9c5db199SXin Li    name_field = 'name'
287*9c5db199SXin Li    objects = model_logic.ExtendedManager()
288*9c5db199SXin Li
289*9c5db199SXin Li
290*9c5db199SXin Li    def save(self, *args, **kwargs):
291*9c5db199SXin Li        if not User.current_user().is_superuser():
292*9c5db199SXin Li            raise Exception('Only superusers may edit drone sets')
293*9c5db199SXin Li        super(DroneSet, self).save(*args, **kwargs)
294*9c5db199SXin Li
295*9c5db199SXin Li
296*9c5db199SXin Li    def delete(self):
297*9c5db199SXin Li        if not User.current_user().is_superuser():
298*9c5db199SXin Li            raise Exception('Only superusers may delete drone sets')
299*9c5db199SXin Li        super(DroneSet, self).delete()
300*9c5db199SXin Li
301*9c5db199SXin Li
302*9c5db199SXin Li    @classmethod
303*9c5db199SXin Li    def drone_sets_enabled(cls):
304*9c5db199SXin Li        """Returns whether drone sets are enabled.
305*9c5db199SXin Li
306*9c5db199SXin Li        @param cls: Implicit class object.
307*9c5db199SXin Li        """
308*9c5db199SXin Li        return cls.DRONE_SETS_ENABLED
309*9c5db199SXin Li
310*9c5db199SXin Li
311*9c5db199SXin Li    @classmethod
312*9c5db199SXin Li    def default_drone_set_name(cls):
313*9c5db199SXin Li        """Returns the default drone set name.
314*9c5db199SXin Li
315*9c5db199SXin Li        @param cls: Implicit class object.
316*9c5db199SXin Li        """
317*9c5db199SXin Li        return cls.DEFAULT_DRONE_SET_NAME
318*9c5db199SXin Li
319*9c5db199SXin Li
320*9c5db199SXin Li    @classmethod
321*9c5db199SXin Li    def get_default(cls):
322*9c5db199SXin Li        """Gets the default drone set name, compatible with Job.add_object.
323*9c5db199SXin Li
324*9c5db199SXin Li        @param cls: Implicit class object.
325*9c5db199SXin Li        """
326*9c5db199SXin Li        return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME)
327*9c5db199SXin Li
328*9c5db199SXin Li
329*9c5db199SXin Li    @classmethod
330*9c5db199SXin Li    def resolve_name(cls, drone_set_name):
331*9c5db199SXin Li        """
332*9c5db199SXin Li        Returns the name of one of these, if not None, in order of preference:
333*9c5db199SXin Li        1) the drone set given,
334*9c5db199SXin Li        2) the current user's default drone set, or
335*9c5db199SXin Li        3) the global default drone set
336*9c5db199SXin Li
337*9c5db199SXin Li        or returns None if drone sets are disabled
338*9c5db199SXin Li
339*9c5db199SXin Li        @param cls: Implicit class object.
340*9c5db199SXin Li        @param drone_set_name: A drone set name.
341*9c5db199SXin Li        """
342*9c5db199SXin Li        if not cls.drone_sets_enabled():
343*9c5db199SXin Li            return None
344*9c5db199SXin Li
345*9c5db199SXin Li        user = User.current_user()
346*9c5db199SXin Li        user_drone_set_name = user.drone_set and user.drone_set.name
347*9c5db199SXin Li
348*9c5db199SXin Li        return drone_set_name or user_drone_set_name or cls.get_default().name
349*9c5db199SXin Li
350*9c5db199SXin Li
351*9c5db199SXin Li    def get_drone_hostnames(self):
352*9c5db199SXin Li        """
353*9c5db199SXin Li        Gets the hostnames of all drones in this drone set
354*9c5db199SXin Li        """
355*9c5db199SXin Li        return set(self.drones.all().values_list('hostname', flat=True))
356*9c5db199SXin Li
357*9c5db199SXin Li
358*9c5db199SXin Li    class Meta:
359*9c5db199SXin Li        """Metadata for class DroneSet."""
360*9c5db199SXin Li        db_table = 'afe_drone_sets'
361*9c5db199SXin Li
362*9c5db199SXin Li    def __unicode__(self):
363*9c5db199SXin Li        return unicode(self.name)
364*9c5db199SXin Li
365*9c5db199SXin Li
366*9c5db199SXin Liclass User(dbmodels.Model, model_logic.ModelExtensions):
367*9c5db199SXin Li    """\
368*9c5db199SXin Li    Required:
369*9c5db199SXin Li    login :user login name
370*9c5db199SXin Li
371*9c5db199SXin Li    Optional:
372*9c5db199SXin Li    access_level: 0=User (default), 1=Admin, 100=Root
373*9c5db199SXin Li    """
374*9c5db199SXin Li    ACCESS_ROOT = 100
375*9c5db199SXin Li    ACCESS_ADMIN = 1
376*9c5db199SXin Li    ACCESS_USER = 0
377*9c5db199SXin Li
378*9c5db199SXin Li    AUTOTEST_SYSTEM = 'autotest_system'
379*9c5db199SXin Li
380*9c5db199SXin Li    login = dbmodels.CharField(max_length=255, unique=True)
381*9c5db199SXin Li    access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
382*9c5db199SXin Li
383*9c5db199SXin Li    # user preferences
384*9c5db199SXin Li    reboot_before = dbmodels.SmallIntegerField(
385*9c5db199SXin Li        choices=model_attributes.RebootBefore.choices(), blank=True,
386*9c5db199SXin Li        default=DEFAULT_REBOOT_BEFORE)
387*9c5db199SXin Li    reboot_after = dbmodels.SmallIntegerField(
388*9c5db199SXin Li        choices=model_attributes.RebootAfter.choices(), blank=True,
389*9c5db199SXin Li        default=DEFAULT_REBOOT_AFTER)
390*9c5db199SXin Li    drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
391*9c5db199SXin Li    show_experimental = dbmodels.BooleanField(default=False)
392*9c5db199SXin Li
393*9c5db199SXin Li    name_field = 'login'
394*9c5db199SXin Li    objects = model_logic.ExtendedManager()
395*9c5db199SXin Li
396*9c5db199SXin Li
397*9c5db199SXin Li    def save(self, *args, **kwargs):
398*9c5db199SXin Li        # is this a new object being saved for the first time?
399*9c5db199SXin Li        first_time = (self.id is None)
400*9c5db199SXin Li        user = thread_local.get_user()
401*9c5db199SXin Li        if user and not user.is_superuser() and user.login != self.login:
402*9c5db199SXin Li            raise AclAccessViolation("You cannot modify user " + self.login)
403*9c5db199SXin Li        super(User, self).save(*args, **kwargs)
404*9c5db199SXin Li        if first_time:
405*9c5db199SXin Li            everyone = AclGroup.objects.get(name='Everyone')
406*9c5db199SXin Li            everyone.users.add(self)
407*9c5db199SXin Li
408*9c5db199SXin Li
409*9c5db199SXin Li    def is_superuser(self):
410*9c5db199SXin Li        """Returns whether the user has superuser access."""
411*9c5db199SXin Li        return self.access_level >= self.ACCESS_ROOT
412*9c5db199SXin Li
413*9c5db199SXin Li
414*9c5db199SXin Li    @classmethod
415*9c5db199SXin Li    def current_user(cls):
416*9c5db199SXin Li        """Returns the current user.
417*9c5db199SXin Li
418*9c5db199SXin Li        @param cls: Implicit class object.
419*9c5db199SXin Li        """
420*9c5db199SXin Li        user = thread_local.get_user()
421*9c5db199SXin Li        if user is None:
422*9c5db199SXin Li            user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM)
423*9c5db199SXin Li            user.access_level = cls.ACCESS_ROOT
424*9c5db199SXin Li            user.save()
425*9c5db199SXin Li        return user
426*9c5db199SXin Li
427*9c5db199SXin Li
428*9c5db199SXin Li    @classmethod
429*9c5db199SXin Li    def get_record(cls, data):
430*9c5db199SXin Li        """Check the database for an identical record.
431*9c5db199SXin Li
432*9c5db199SXin Li        Check for a record with matching id and login. If one exists,
433*9c5db199SXin Li        return it. If one does not exist there is a possibility that
434*9c5db199SXin Li        the following cases have happened:
435*9c5db199SXin Li        1. Same id, different login
436*9c5db199SXin Li            We received: "1 chromeos-test"
437*9c5db199SXin Li            And we have: "1 debug-user"
438*9c5db199SXin Li        In this case we need to delete "1 debug_user" and insert
439*9c5db199SXin Li        "1 chromeos-test".
440*9c5db199SXin Li
441*9c5db199SXin Li        2. Same login, different id:
442*9c5db199SXin Li            We received: "1 chromeos-test"
443*9c5db199SXin Li            And we have: "2 chromeos-test"
444*9c5db199SXin Li        In this case we need to delete "2 chromeos-test" and insert
445*9c5db199SXin Li        "1 chromeos-test".
446*9c5db199SXin Li
447*9c5db199SXin Li        As long as this method deletes bad records and raises the
448*9c5db199SXin Li        DoesNotExist exception the caller will handle creating the
449*9c5db199SXin Li        new record.
450*9c5db199SXin Li
451*9c5db199SXin Li        @raises: DoesNotExist, if a record with the matching login and id
452*9c5db199SXin Li                does not exist.
453*9c5db199SXin Li        """
454*9c5db199SXin Li
455*9c5db199SXin Li        # Both the id and login should be uniqe but there are cases when
456*9c5db199SXin Li        # we might already have a user with the same login/id because
457*9c5db199SXin Li        # current_user will proactively create a user record if it doesn't
458*9c5db199SXin Li        # exist. Since we want to avoid conflict between the main and
459*9c5db199SXin Li        # shard, just delete any existing user records that don't match
460*9c5db199SXin Li        # what we're about to deserialize from the main.
461*9c5db199SXin Li        try:
462*9c5db199SXin Li            return cls.objects.get(login=data['login'], id=data['id'])
463*9c5db199SXin Li        except cls.DoesNotExist:
464*9c5db199SXin Li            cls.delete_matching_record(login=data['login'])
465*9c5db199SXin Li            cls.delete_matching_record(id=data['id'])
466*9c5db199SXin Li            raise
467*9c5db199SXin Li
468*9c5db199SXin Li
469*9c5db199SXin Li    class Meta:
470*9c5db199SXin Li        """Metadata for class User."""
471*9c5db199SXin Li        db_table = 'afe_users'
472*9c5db199SXin Li
473*9c5db199SXin Li    def __unicode__(self):
474*9c5db199SXin Li        return unicode(self.login)
475*9c5db199SXin Li
476*9c5db199SXin Li
477*9c5db199SXin Liclass Host(model_logic.ModelWithInvalid, rdb_model_extensions.AbstractHostModel,
478*9c5db199SXin Li           model_logic.ModelWithAttributes):
479*9c5db199SXin Li    """\
480*9c5db199SXin Li    Required:
481*9c5db199SXin Li    hostname
482*9c5db199SXin Li
483*9c5db199SXin Li    optional:
484*9c5db199SXin Li    locked: if true, host is locked and will not be queued
485*9c5db199SXin Li
486*9c5db199SXin Li    Internal:
487*9c5db199SXin Li    From AbstractHostModel:
488*9c5db199SXin Li        status: string describing status of host
489*9c5db199SXin Li        invalid: true if the host has been deleted
490*9c5db199SXin Li        protection: indicates what can be done to this host during repair
491*9c5db199SXin Li        lock_time: DateTime at which the host was locked
492*9c5db199SXin Li        dirty: true if the host has been used without being rebooted
493*9c5db199SXin Li    Local:
494*9c5db199SXin Li        locked_by: user that locked the host, or null if the host is unlocked
495*9c5db199SXin Li    """
496*9c5db199SXin Li
497*9c5db199SXin Li    SERIALIZATION_LINKS_TO_FOLLOW = set(['aclgroup_set',
498*9c5db199SXin Li                                         'hostattribute_set',
499*9c5db199SXin Li                                         'labels',
500*9c5db199SXin Li                                         'shard'])
501*9c5db199SXin Li    SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['invalid'])
502*9c5db199SXin Li
503*9c5db199SXin Li
504*9c5db199SXin Li    def custom_deserialize_relation(self, link, data):
505*9c5db199SXin Li        assert link == 'shard', 'Link %s should not be deserialized' % link
506*9c5db199SXin Li        self.shard = Shard.deserialize(data)
507*9c5db199SXin Li
508*9c5db199SXin Li
509*9c5db199SXin Li    # Note: Only specify foreign keys here, specify host columns in
510*9c5db199SXin Li    # rdb_model_extensions instead.
511*9c5db199SXin Li    Protection = host_protections.Protection
512*9c5db199SXin Li    labels = dbmodels.ManyToManyField(Label, blank=True,
513*9c5db199SXin Li                                      db_table='afe_hosts_labels')
514*9c5db199SXin Li    static_labels = dbmodels.ManyToManyField(
515*9c5db199SXin Li            StaticLabel, blank=True, db_table='afe_static_hosts_labels')
516*9c5db199SXin Li    locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
517*9c5db199SXin Li    name_field = 'hostname'
518*9c5db199SXin Li    objects = model_logic.ModelWithInvalidManager()
519*9c5db199SXin Li    valid_objects = model_logic.ValidObjectsManager()
520*9c5db199SXin Li    leased_objects = model_logic.LeasedHostManager()
521*9c5db199SXin Li
522*9c5db199SXin Li    shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
523*9c5db199SXin Li
524*9c5db199SXin Li    def __init__(self, *args, **kwargs):
525*9c5db199SXin Li        super(Host, self).__init__(*args, **kwargs)
526*9c5db199SXin Li        self._record_attributes(['status'])
527*9c5db199SXin Li
528*9c5db199SXin Li
529*9c5db199SXin Li    @classmethod
530*9c5db199SXin Li    def classify_labels(cls, label_names):
531*9c5db199SXin Li        """Split labels to static & non-static.
532*9c5db199SXin Li
533*9c5db199SXin Li        @label_names: a list of labels (string).
534*9c5db199SXin Li
535*9c5db199SXin Li        @returns: a list of StaticLabel objects & a list of
536*9c5db199SXin Li                  (non-static) Label objects.
537*9c5db199SXin Li        """
538*9c5db199SXin Li        if not label_names:
539*9c5db199SXin Li            return [], []
540*9c5db199SXin Li
541*9c5db199SXin Li        labels = Label.objects.filter(name__in=label_names)
542*9c5db199SXin Li
543*9c5db199SXin Li        if not RESPECT_STATIC_LABELS:
544*9c5db199SXin Li            return [], labels
545*9c5db199SXin Li
546*9c5db199SXin Li        return cls.classify_label_objects(labels)
547*9c5db199SXin Li
548*9c5db199SXin Li
549*9c5db199SXin Li    @classmethod
550*9c5db199SXin Li    def classify_label_objects(cls, label_objects):
551*9c5db199SXin Li        if not RESPECT_STATIC_LABELS:
552*9c5db199SXin Li            return [], label_objects
553*9c5db199SXin Li
554*9c5db199SXin Li        replaced_labels = ReplacedLabel.objects.filter(label__in=label_objects)
555*9c5db199SXin Li        replaced_ids = [l.label.id for l in replaced_labels]
556*9c5db199SXin Li        non_static_labels = [
557*9c5db199SXin Li                l for l in label_objects if not l.id in replaced_ids]
558*9c5db199SXin Li        static_label_names = [
559*9c5db199SXin Li                l.name for l in label_objects if l.id in replaced_ids]
560*9c5db199SXin Li        static_labels = StaticLabel.objects.filter(name__in=static_label_names)
561*9c5db199SXin Li        return static_labels, non_static_labels
562*9c5db199SXin Li
563*9c5db199SXin Li
564*9c5db199SXin Li    @classmethod
565*9c5db199SXin Li    def get_hosts_with_labels(cls, label_names, initial_query):
566*9c5db199SXin Li        """Get hosts by label filters.
567*9c5db199SXin Li
568*9c5db199SXin Li        @param label_names: label (string) lists for fetching hosts.
569*9c5db199SXin Li        @param initial_query: a model_logic.QuerySet of Host object, e.g.
570*9c5db199SXin Li
571*9c5db199SXin Li                Host.objects.all(), Host.valid_objects.all().
572*9c5db199SXin Li
573*9c5db199SXin Li            This initial_query cannot be a sliced QuerySet, e.g.
574*9c5db199SXin Li
575*9c5db199SXin Li                Host.objects.all().filter(query_limit=10)
576*9c5db199SXin Li        """
577*9c5db199SXin Li        if not label_names:
578*9c5db199SXin Li            return initial_query
579*9c5db199SXin Li
580*9c5db199SXin Li        static_labels, non_static_labels = cls.classify_labels(label_names)
581*9c5db199SXin Li        if len(static_labels) + len(non_static_labels) != len(label_names):
582*9c5db199SXin Li            # Some labels don't exist in afe db, which means no hosts
583*9c5db199SXin Li            # should be matched.
584*9c5db199SXin Li            return set()
585*9c5db199SXin Li
586*9c5db199SXin Li        for l in static_labels:
587*9c5db199SXin Li            initial_query = initial_query.filter(static_labels=l)
588*9c5db199SXin Li
589*9c5db199SXin Li        for l in non_static_labels:
590*9c5db199SXin Li            initial_query = initial_query.filter(labels=l)
591*9c5db199SXin Li
592*9c5db199SXin Li        return initial_query
593*9c5db199SXin Li
594*9c5db199SXin Li
595*9c5db199SXin Li    @classmethod
596*9c5db199SXin Li    def get_hosts_with_label_ids(cls, label_ids, initial_query):
597*9c5db199SXin Li        """Get hosts by label_id filters.
598*9c5db199SXin Li
599*9c5db199SXin Li        @param label_ids: label id (int) lists for fetching hosts.
600*9c5db199SXin Li        @param initial_query: a list of Host object, e.g.
601*9c5db199SXin Li            [<Host: 100.107.151.253>, <Host: 100.107.151.251>, ...]
602*9c5db199SXin Li        """
603*9c5db199SXin Li        labels = Label.objects.filter(id__in=label_ids)
604*9c5db199SXin Li        label_names = [l.name for l in labels]
605*9c5db199SXin Li        return cls.get_hosts_with_labels(label_names, initial_query)
606*9c5db199SXin Li
607*9c5db199SXin Li
608*9c5db199SXin Li    @staticmethod
609*9c5db199SXin Li    def create_one_time_host(hostname):
610*9c5db199SXin Li        """Creates a one-time host.
611*9c5db199SXin Li
612*9c5db199SXin Li        @param hostname: The name for the host.
613*9c5db199SXin Li        """
614*9c5db199SXin Li        query = Host.objects.filter(hostname=hostname)
615*9c5db199SXin Li        if query.count() == 0:
616*9c5db199SXin Li            host = Host(hostname=hostname, invalid=True)
617*9c5db199SXin Li            host.do_validate()
618*9c5db199SXin Li        else:
619*9c5db199SXin Li            host = query[0]
620*9c5db199SXin Li            if not host.invalid:
621*9c5db199SXin Li                raise model_logic.ValidationError({
622*9c5db199SXin Li                    'hostname' : '%s already exists in the autotest DB.  '
623*9c5db199SXin Li                        'Select it rather than entering it as a one time '
624*9c5db199SXin Li                        'host.' % hostname
625*9c5db199SXin Li                    })
626*9c5db199SXin Li        host.protection = host_protections.Protection.DO_NOT_REPAIR
627*9c5db199SXin Li        host.locked = False
628*9c5db199SXin Li        host.save()
629*9c5db199SXin Li        host.clean_object()
630*9c5db199SXin Li        return host
631*9c5db199SXin Li
632*9c5db199SXin Li
633*9c5db199SXin Li    @classmethod
634*9c5db199SXin Li    def _assign_to_shard_nothing_helper(cls):
635*9c5db199SXin Li        """Does nothing.
636*9c5db199SXin Li
637*9c5db199SXin Li        This method is called in the middle of assign_to_shard, and does
638*9c5db199SXin Li        nothing. It exists to allow integration tests to simulate a race
639*9c5db199SXin Li        condition."""
640*9c5db199SXin Li
641*9c5db199SXin Li
642*9c5db199SXin Li    @classmethod
643*9c5db199SXin Li    def assign_to_shard(cls, shard, known_ids):
644*9c5db199SXin Li        """Assigns hosts to a shard.
645*9c5db199SXin Li
646*9c5db199SXin Li        For all labels that have been assigned to a shard, all hosts that
647*9c5db199SXin Li        have at least one of the shard's labels are assigned to the shard.
648*9c5db199SXin Li        Hosts that are assigned to the shard but aren't already present on the
649*9c5db199SXin Li        shard are returned.
650*9c5db199SXin Li
651*9c5db199SXin Li        Any boards that are in |known_ids| but that do not belong to the shard
652*9c5db199SXin Li        are incorrect ids, which are also returned so that the shard can remove
653*9c5db199SXin Li        them locally.
654*9c5db199SXin Li
655*9c5db199SXin Li        Board to shard mapping is many-to-one. Many different boards can be
656*9c5db199SXin Li        hosted in a shard. However, DUTs of a single board cannot be distributed
657*9c5db199SXin Li        into more than one shard.
658*9c5db199SXin Li
659*9c5db199SXin Li        @param shard: The shard object to assign labels/hosts for.
660*9c5db199SXin Li        @param known_ids: List of all host-ids the shard already knows.
661*9c5db199SXin Li                          This is used to figure out which hosts should be sent
662*9c5db199SXin Li                          to the shard. If shard_ids were used instead, hosts
663*9c5db199SXin Li                          would only be transferred once, even if the client
664*9c5db199SXin Li                          failed persisting them.
665*9c5db199SXin Li                          The number of hosts usually lies in O(100), so the
666*9c5db199SXin Li                          overhead is acceptable.
667*9c5db199SXin Li
668*9c5db199SXin Li        @returns a tuple of (hosts objects that should be sent to the shard,
669*9c5db199SXin Li                             incorrect host ids that should not belong to]
670*9c5db199SXin Li                             shard)
671*9c5db199SXin Li        """
672*9c5db199SXin Li        # Disclaimer: concurrent heartbeats should theoretically not occur in
673*9c5db199SXin Li        # the current setup. As they may be introduced in the near future,
674*9c5db199SXin Li        # this comment will be left here.
675*9c5db199SXin Li
676*9c5db199SXin Li        # Sending stuff twice is acceptable, but forgetting something isn't.
677*9c5db199SXin Li        # Detecting duplicates on the client is easy, but here it's harder. The
678*9c5db199SXin Li        # following options were considered:
679*9c5db199SXin Li        # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more
680*9c5db199SXin Li        #   than select returned, as concurrently more hosts might have been
681*9c5db199SXin Li        #   inserted
682*9c5db199SXin Li        # - UPDATE and then SELECT WHERE shard=shard: select always returns all
683*9c5db199SXin Li        #   hosts for the shard, this is overhead
684*9c5db199SXin Li        # - SELECT and then UPDATE only selected without requerying afterwards:
685*9c5db199SXin Li        #   returns the old state of the records.
686*9c5db199SXin Li        new_hosts = []
687*9c5db199SXin Li
688*9c5db199SXin Li        possible_new_host_ids = set(Host.objects.filter(
689*9c5db199SXin Li            labels__in=shard.labels.all(),
690*9c5db199SXin Li            leased=False
691*9c5db199SXin Li            ).exclude(
692*9c5db199SXin Li            id__in=known_ids,
693*9c5db199SXin Li            ).values_list('pk', flat=True))
694*9c5db199SXin Li
695*9c5db199SXin Li        # No-op in production, used to simulate race condition in tests.
696*9c5db199SXin Li        cls._assign_to_shard_nothing_helper()
697*9c5db199SXin Li
698*9c5db199SXin Li        if possible_new_host_ids:
699*9c5db199SXin Li            Host.objects.filter(
700*9c5db199SXin Li                pk__in=possible_new_host_ids,
701*9c5db199SXin Li                labels__in=shard.labels.all(),
702*9c5db199SXin Li                leased=False
703*9c5db199SXin Li                ).update(shard=shard)
704*9c5db199SXin Li            new_hosts = list(Host.objects.filter(
705*9c5db199SXin Li                pk__in=possible_new_host_ids,
706*9c5db199SXin Li                shard=shard
707*9c5db199SXin Li                ).all())
708*9c5db199SXin Li
709*9c5db199SXin Li        invalid_host_ids = list(Host.objects.filter(
710*9c5db199SXin Li            id__in=known_ids
711*9c5db199SXin Li            ).exclude(
712*9c5db199SXin Li            shard=shard
713*9c5db199SXin Li            ).values_list('pk', flat=True))
714*9c5db199SXin Li
715*9c5db199SXin Li        return new_hosts, invalid_host_ids
716*9c5db199SXin Li
717*9c5db199SXin Li    def resurrect_object(self, old_object):
718*9c5db199SXin Li        super(Host, self).resurrect_object(old_object)
719*9c5db199SXin Li        # invalid hosts can be in use by the scheduler (as one-time hosts), so
720*9c5db199SXin Li        # don't change the status
721*9c5db199SXin Li        self.status = old_object.status
722*9c5db199SXin Li
723*9c5db199SXin Li
724*9c5db199SXin Li    def clean_object(self):
725*9c5db199SXin Li        self.aclgroup_set.clear()
726*9c5db199SXin Li        self.labels.clear()
727*9c5db199SXin Li        self.static_labels.clear()
728*9c5db199SXin Li
729*9c5db199SXin Li
730*9c5db199SXin Li    def save(self, *args, **kwargs):
731*9c5db199SXin Li        # extra spaces in the hostname can be a sneaky source of errors
732*9c5db199SXin Li        self.hostname = self.hostname.strip()
733*9c5db199SXin Li        # is this a new object being saved for the first time?
734*9c5db199SXin Li        first_time = (self.id is None)
735*9c5db199SXin Li        if not first_time:
736*9c5db199SXin Li            AclGroup.check_for_acl_violation_hosts([self])
737*9c5db199SXin Li        # If locked is changed, send its status and user made the change to
738*9c5db199SXin Li        # metaDB. Locks are important in host history because if a device is
739*9c5db199SXin Li        # locked then we don't really care what state it is in.
740*9c5db199SXin Li        if self.locked and not self.locked_by:
741*9c5db199SXin Li            self.locked_by = User.current_user()
742*9c5db199SXin Li            if not self.lock_time:
743*9c5db199SXin Li                self.lock_time = datetime.now()
744*9c5db199SXin Li            self.dirty = True
745*9c5db199SXin Li        elif not self.locked and self.locked_by:
746*9c5db199SXin Li            self.locked_by = None
747*9c5db199SXin Li            self.lock_time = None
748*9c5db199SXin Li        super(Host, self).save(*args, **kwargs)
749*9c5db199SXin Li        if first_time:
750*9c5db199SXin Li            everyone = AclGroup.objects.get(name='Everyone')
751*9c5db199SXin Li            everyone.hosts.add(self)
752*9c5db199SXin Li            # remove attributes that may have lingered from an old host and
753*9c5db199SXin Li            # should not be associated with a new host
754*9c5db199SXin Li            for host_attribute in self.hostattribute_set.all():
755*9c5db199SXin Li                self.delete_attribute(host_attribute.attribute)
756*9c5db199SXin Li        self._check_for_updated_attributes()
757*9c5db199SXin Li
758*9c5db199SXin Li
759*9c5db199SXin Li    def delete(self):
760*9c5db199SXin Li        AclGroup.check_for_acl_violation_hosts([self])
761*9c5db199SXin Li        logging.info('Preconditions for deleting host %s...', self.hostname)
762*9c5db199SXin Li        for queue_entry in self.hostqueueentry_set.all():
763*9c5db199SXin Li            logging.info('  Deleting and aborting hqe %s...', queue_entry)
764*9c5db199SXin Li            queue_entry.deleted = True
765*9c5db199SXin Li            queue_entry.abort()
766*9c5db199SXin Li            logging.info('  ... done with hqe %s.', queue_entry)
767*9c5db199SXin Li        for host_attribute in self.hostattribute_set.all():
768*9c5db199SXin Li            logging.info('  Deleting attribute %s...', host_attribute)
769*9c5db199SXin Li            self.delete_attribute(host_attribute.attribute)
770*9c5db199SXin Li            logging.info('  ... done with attribute %s.', host_attribute)
771*9c5db199SXin Li        logging.info('... preconditions done for host %s.', self.hostname)
772*9c5db199SXin Li        logging.info('Deleting host %s...', self.hostname)
773*9c5db199SXin Li        super(Host, self).delete()
774*9c5db199SXin Li        logging.info('... done.')
775*9c5db199SXin Li
776*9c5db199SXin Li
777*9c5db199SXin Li    def on_attribute_changed(self, attribute, old_value):
778*9c5db199SXin Li        assert attribute == 'status'
779*9c5db199SXin Li        logging.info('%s -> %s', self.hostname, self.status)
780*9c5db199SXin Li
781*9c5db199SXin Li
782*9c5db199SXin Li    def enqueue_job(self, job, is_template=False):
783*9c5db199SXin Li        """Enqueue a job on this host.
784*9c5db199SXin Li
785*9c5db199SXin Li        @param job: A job to enqueue.
786*9c5db199SXin Li        @param is_template: Whther the status should be "Template".
787*9c5db199SXin Li        """
788*9c5db199SXin Li        queue_entry = HostQueueEntry.create(host=self, job=job,
789*9c5db199SXin Li                                            is_template=is_template)
790*9c5db199SXin Li        # allow recovery of dead hosts from the frontend
791*9c5db199SXin Li        if not self.active_queue_entry() and self.is_dead():
792*9c5db199SXin Li            self.status = Host.Status.READY
793*9c5db199SXin Li            self.save()
794*9c5db199SXin Li        queue_entry.save()
795*9c5db199SXin Li
796*9c5db199SXin Li        block = IneligibleHostQueue(job=job, host=self)
797*9c5db199SXin Li        block.save()
798*9c5db199SXin Li
799*9c5db199SXin Li
800*9c5db199SXin Li    def platform(self):
801*9c5db199SXin Li        """The platform of the host."""
802*9c5db199SXin Li        # TODO(showard): slighly hacky?
803*9c5db199SXin Li        platforms = self.labels.filter(platform=True)
804*9c5db199SXin Li        if len(platforms) == 0:
805*9c5db199SXin Li            return None
806*9c5db199SXin Li        return platforms[0]
807*9c5db199SXin Li    platform.short_description = 'Platform'
808*9c5db199SXin Li
809*9c5db199SXin Li
810*9c5db199SXin Li    @classmethod
811*9c5db199SXin Li    def check_no_platform(cls, hosts):
812*9c5db199SXin Li        """Verify the specified hosts have no associated platforms.
813*9c5db199SXin Li
814*9c5db199SXin Li        @param cls: Implicit class object.
815*9c5db199SXin Li        @param hosts: The hosts to verify.
816*9c5db199SXin Li        @raises model_logic.ValidationError if any hosts already have a
817*9c5db199SXin Li            platform.
818*9c5db199SXin Li        """
819*9c5db199SXin Li        Host.objects.populate_relationships(hosts, Label, 'label_list')
820*9c5db199SXin Li        Host.objects.populate_relationships(hosts, StaticLabel,
821*9c5db199SXin Li                                            'staticlabel_list')
822*9c5db199SXin Li        errors = []
823*9c5db199SXin Li        for host in hosts:
824*9c5db199SXin Li            platforms = [label.name for label in host.label_list
825*9c5db199SXin Li                         if label.platform]
826*9c5db199SXin Li            if RESPECT_STATIC_LABELS:
827*9c5db199SXin Li                platforms += [label.name for label in host.staticlabel_list
828*9c5db199SXin Li                              if label.platform]
829*9c5db199SXin Li
830*9c5db199SXin Li            if platforms:
831*9c5db199SXin Li                # do a join, just in case this host has multiple platforms,
832*9c5db199SXin Li                # we'll be able to see it
833*9c5db199SXin Li                errors.append('Host %s already has a platform: %s' % (
834*9c5db199SXin Li                              host.hostname, ', '.join(platforms)))
835*9c5db199SXin Li        if errors:
836*9c5db199SXin Li            raise model_logic.ValidationError({'labels': '; '.join(errors)})
837*9c5db199SXin Li
838*9c5db199SXin Li
839*9c5db199SXin Li    @classmethod
840*9c5db199SXin Li    def check_board_labels_allowed(cls, hosts, new_labels=[]):
841*9c5db199SXin Li        """Verify the specified hosts have valid board labels and the given
842*9c5db199SXin Li        new board labels can be added.
843*9c5db199SXin Li
844*9c5db199SXin Li        @param cls: Implicit class object.
845*9c5db199SXin Li        @param hosts: The hosts to verify.
846*9c5db199SXin Li        @param new_labels: A list of labels to be added to the hosts.
847*9c5db199SXin Li
848*9c5db199SXin Li        @raises model_logic.ValidationError if any host has invalid board labels
849*9c5db199SXin Li                or the given board labels cannot be added to the hsots.
850*9c5db199SXin Li        """
851*9c5db199SXin Li        Host.objects.populate_relationships(hosts, Label, 'label_list')
852*9c5db199SXin Li        Host.objects.populate_relationships(hosts, StaticLabel,
853*9c5db199SXin Li                                            'staticlabel_list')
854*9c5db199SXin Li        errors = []
855*9c5db199SXin Li        for host in hosts:
856*9c5db199SXin Li            boards = [label.name for label in host.label_list
857*9c5db199SXin Li                      if label.name.startswith('board:')]
858*9c5db199SXin Li            if RESPECT_STATIC_LABELS:
859*9c5db199SXin Li                boards += [label.name for label in host.staticlabel_list
860*9c5db199SXin Li                           if label.name.startswith('board:')]
861*9c5db199SXin Li
862*9c5db199SXin Li            new_boards = [name for name in new_labels
863*9c5db199SXin Li                          if name.startswith('board:')]
864*9c5db199SXin Li            if len(boards) + len(new_boards) > 1:
865*9c5db199SXin Li                # do a join, just in case this host has multiple boards,
866*9c5db199SXin Li                # we'll be able to see it
867*9c5db199SXin Li                errors.append('Host %s already has board labels: %s' % (
868*9c5db199SXin Li                              host.hostname, ', '.join(boards)))
869*9c5db199SXin Li        if errors:
870*9c5db199SXin Li            raise model_logic.ValidationError({'labels': '; '.join(errors)})
871*9c5db199SXin Li
872*9c5db199SXin Li
873*9c5db199SXin Li    def is_dead(self):
874*9c5db199SXin Li        """Returns whether the host is dead (has status repair failed)."""
875*9c5db199SXin Li        return self.status == Host.Status.REPAIR_FAILED
876*9c5db199SXin Li
877*9c5db199SXin Li
878*9c5db199SXin Li    def active_queue_entry(self):
879*9c5db199SXin Li        """Returns the active queue entry for this host, or None if none."""
880*9c5db199SXin Li        active = list(self.hostqueueentry_set.filter(active=True))
881*9c5db199SXin Li        if not active:
882*9c5db199SXin Li            return None
883*9c5db199SXin Li        assert len(active) == 1, ('More than one active entry for '
884*9c5db199SXin Li                                  'host ' + self.hostname)
885*9c5db199SXin Li        return active[0]
886*9c5db199SXin Li
887*9c5db199SXin Li
888*9c5db199SXin Li    def _get_attribute_model_and_args(self, attribute):
889*9c5db199SXin Li        return HostAttribute, dict(host=self, attribute=attribute)
890*9c5db199SXin Li
891*9c5db199SXin Li
892*9c5db199SXin Li    def _get_static_attribute_model_and_args(self, attribute):
893*9c5db199SXin Li        return StaticHostAttribute, dict(host=self, attribute=attribute)
894*9c5db199SXin Li
895*9c5db199SXin Li
896*9c5db199SXin Li    def _is_replaced_by_static_attribute(self, attribute):
897*9c5db199SXin Li        if RESPECT_STATIC_ATTRIBUTES:
898*9c5db199SXin Li            model, args = self._get_static_attribute_model_and_args(attribute)
899*9c5db199SXin Li            try:
900*9c5db199SXin Li                static_attr = model.objects.get(**args)
901*9c5db199SXin Li                return True
902*9c5db199SXin Li            except StaticHostAttribute.DoesNotExist:
903*9c5db199SXin Li                return False
904*9c5db199SXin Li
905*9c5db199SXin Li        return False
906*9c5db199SXin Li
907*9c5db199SXin Li
908*9c5db199SXin Li    @classmethod
909*9c5db199SXin Li    def get_attribute_model(cls):
910*9c5db199SXin Li        """Return the attribute model.
911*9c5db199SXin Li
912*9c5db199SXin Li        Override method in parent class. See ModelExtensions for details.
913*9c5db199SXin Li        @returns: The attribute model of Host.
914*9c5db199SXin Li        """
915*9c5db199SXin Li        return HostAttribute
916*9c5db199SXin Li
917*9c5db199SXin Li
918*9c5db199SXin Li    class Meta:
919*9c5db199SXin Li        """Metadata for the Host class."""
920*9c5db199SXin Li        db_table = 'afe_hosts'
921*9c5db199SXin Li
922*9c5db199SXin Li
923*9c5db199SXin Li    def __unicode__(self):
924*9c5db199SXin Li        return unicode(self.hostname)
925*9c5db199SXin Li
926*9c5db199SXin Li
927*9c5db199SXin Liclass HostAttribute(dbmodels.Model, model_logic.ModelExtensions):
928*9c5db199SXin Li    """Arbitrary keyvals associated with hosts."""
929*9c5db199SXin Li
930*9c5db199SXin Li    SERIALIZATION_LINKS_TO_KEEP = set(['host'])
931*9c5db199SXin Li    SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
932*9c5db199SXin Li    host = dbmodels.ForeignKey(Host)
933*9c5db199SXin Li    attribute = dbmodels.CharField(max_length=90)
934*9c5db199SXin Li    value = dbmodels.CharField(max_length=300)
935*9c5db199SXin Li
936*9c5db199SXin Li    objects = model_logic.ExtendedManager()
937*9c5db199SXin Li
938*9c5db199SXin Li    class Meta:
939*9c5db199SXin Li        """Metadata for the HostAttribute class."""
940*9c5db199SXin Li        db_table = 'afe_host_attributes'
941*9c5db199SXin Li
942*9c5db199SXin Li
943*9c5db199SXin Li    @classmethod
944*9c5db199SXin Li    def get_record(cls, data):
945*9c5db199SXin Li        """Check the database for an identical record.
946*9c5db199SXin Li
947*9c5db199SXin Li        Use host_id and attribute to search for a existing record.
948*9c5db199SXin Li
949*9c5db199SXin Li        @raises: DoesNotExist, if no record found
950*9c5db199SXin Li        @raises: MultipleObjectsReturned if multiple records found.
951*9c5db199SXin Li        """
952*9c5db199SXin Li        # TODO(fdeng): We should use host_id and attribute together as
953*9c5db199SXin Li        #              a primary key in the db.
954*9c5db199SXin Li        return cls.objects.get(host_id=data['host_id'],
955*9c5db199SXin Li                               attribute=data['attribute'])
956*9c5db199SXin Li
957*9c5db199SXin Li
958*9c5db199SXin Li    @classmethod
959*9c5db199SXin Li    def deserialize(cls, data):
960*9c5db199SXin Li        """Override deserialize in parent class.
961*9c5db199SXin Li
962*9c5db199SXin Li        Do not deserialize id as id is not kept consistent on main and shards.
963*9c5db199SXin Li
964*9c5db199SXin Li        @param data: A dictionary of data to deserialize.
965*9c5db199SXin Li
966*9c5db199SXin Li        @returns: A HostAttribute object.
967*9c5db199SXin Li        """
968*9c5db199SXin Li        if data:
969*9c5db199SXin Li            data.pop('id')
970*9c5db199SXin Li        return super(HostAttribute, cls).deserialize(data)
971*9c5db199SXin Li
972*9c5db199SXin Li
973*9c5db199SXin Liclass StaticHostAttribute(dbmodels.Model, model_logic.ModelExtensions):
974*9c5db199SXin Li    """Static arbitrary keyvals associated with hosts."""
975*9c5db199SXin Li
976*9c5db199SXin Li    SERIALIZATION_LINKS_TO_KEEP = set(['host'])
977*9c5db199SXin Li    SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
978*9c5db199SXin Li    host = dbmodels.ForeignKey(Host)
979*9c5db199SXin Li    attribute = dbmodels.CharField(max_length=90)
980*9c5db199SXin Li    value = dbmodels.CharField(max_length=300)
981*9c5db199SXin Li
982*9c5db199SXin Li    objects = model_logic.ExtendedManager()
983*9c5db199SXin Li
984*9c5db199SXin Li    class Meta:
985*9c5db199SXin Li        """Metadata for the StaticHostAttribute class."""
986*9c5db199SXin Li        db_table = 'afe_static_host_attributes'
987*9c5db199SXin Li
988*9c5db199SXin Li
989*9c5db199SXin Li    @classmethod
990*9c5db199SXin Li    def get_record(cls, data):
991*9c5db199SXin Li        """Check the database for an identical record.
992*9c5db199SXin Li
993*9c5db199SXin Li        Use host_id and attribute to search for a existing record.
994*9c5db199SXin Li
995*9c5db199SXin Li        @raises: DoesNotExist, if no record found
996*9c5db199SXin Li        @raises: MultipleObjectsReturned if multiple records found.
997*9c5db199SXin Li        """
998*9c5db199SXin Li        return cls.objects.get(host_id=data['host_id'],
999*9c5db199SXin Li                               attribute=data['attribute'])
1000*9c5db199SXin Li
1001*9c5db199SXin Li
1002*9c5db199SXin Li    @classmethod
1003*9c5db199SXin Li    def deserialize(cls, data):
1004*9c5db199SXin Li        """Override deserialize in parent class.
1005*9c5db199SXin Li
1006*9c5db199SXin Li        Do not deserialize id as id is not kept consistent on main and shards.
1007*9c5db199SXin Li
1008*9c5db199SXin Li        @param data: A dictionary of data to deserialize.
1009*9c5db199SXin Li
1010*9c5db199SXin Li        @returns: A StaticHostAttribute object.
1011*9c5db199SXin Li        """
1012*9c5db199SXin Li        if data:
1013*9c5db199SXin Li            data.pop('id')
1014*9c5db199SXin Li        return super(StaticHostAttribute, cls).deserialize(data)
1015*9c5db199SXin Li
1016*9c5db199SXin Li
1017*9c5db199SXin Liclass Test(dbmodels.Model, model_logic.ModelExtensions):
1018*9c5db199SXin Li    """\
1019*9c5db199SXin Li    Required:
1020*9c5db199SXin Li    author: author name
1021*9c5db199SXin Li    description: description of the test
1022*9c5db199SXin Li    name: test name
1023*9c5db199SXin Li    time: short, medium, long
1024*9c5db199SXin Li    test_class: This describes the class for your the test belongs in.
1025*9c5db199SXin Li    test_category: This describes the category for your tests
1026*9c5db199SXin Li    test_type: Client or Server
1027*9c5db199SXin Li    path: path to pass to run_test()
1028*9c5db199SXin Li    sync_count:  is a number >=1 (1 being the default). If it's 1, then it's an
1029*9c5db199SXin Li                 async job. If it's >1 it's sync job for that number of machines
1030*9c5db199SXin Li                 i.e. if sync_count = 2 it is a sync job that requires two
1031*9c5db199SXin Li                 machines.
1032*9c5db199SXin Li    Optional:
1033*9c5db199SXin Li    dependencies: What the test requires to run. Comma deliminated list
1034*9c5db199SXin Li    dependency_labels: many-to-many relationship with labels corresponding to
1035*9c5db199SXin Li                       test dependencies.
1036*9c5db199SXin Li    experimental: If this is set to True production servers will ignore the test
1037*9c5db199SXin Li    run_verify: Whether or not the scheduler should run the verify stage
1038*9c5db199SXin Li    run_reset: Whether or not the scheduler should run the reset stage
1039*9c5db199SXin Li    test_retry: Number of times to retry test if the test did not complete
1040*9c5db199SXin Li                successfully. (optional, default: 0)
1041*9c5db199SXin Li    """
1042*9c5db199SXin Li    TestTime = autotest_enum.AutotestEnum('SHORT', 'MEDIUM', 'LONG',
1043*9c5db199SXin Li                                          start_value=1)
1044*9c5db199SXin Li
1045*9c5db199SXin Li    name = dbmodels.CharField(max_length=255, unique=True)
1046*9c5db199SXin Li    author = dbmodels.CharField(max_length=255)
1047*9c5db199SXin Li    test_class = dbmodels.CharField(max_length=255)
1048*9c5db199SXin Li    test_category = dbmodels.CharField(max_length=255)
1049*9c5db199SXin Li    dependencies = dbmodels.CharField(max_length=255, blank=True)
1050*9c5db199SXin Li    description = dbmodels.TextField(blank=True)
1051*9c5db199SXin Li    experimental = dbmodels.BooleanField(default=True)
1052*9c5db199SXin Li    run_verify = dbmodels.BooleanField(default=False)
1053*9c5db199SXin Li    test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
1054*9c5db199SXin Li                                           default=TestTime.MEDIUM)
1055*9c5db199SXin Li    test_type = dbmodels.SmallIntegerField(
1056*9c5db199SXin Li        choices=control_data.CONTROL_TYPE.choices())
1057*9c5db199SXin Li    sync_count = dbmodels.IntegerField(default=1)
1058*9c5db199SXin Li    path = dbmodels.CharField(max_length=255, unique=True)
1059*9c5db199SXin Li    test_retry = dbmodels.IntegerField(blank=True, default=0)
1060*9c5db199SXin Li    run_reset = dbmodels.BooleanField(default=True)
1061*9c5db199SXin Li
1062*9c5db199SXin Li    dependency_labels = (
1063*9c5db199SXin Li        dbmodels.ManyToManyField(Label, blank=True,
1064*9c5db199SXin Li                                 db_table='afe_autotests_dependency_labels'))
1065*9c5db199SXin Li    name_field = 'name'
1066*9c5db199SXin Li    objects = model_logic.ExtendedManager()
1067*9c5db199SXin Li
1068*9c5db199SXin Li
1069*9c5db199SXin Li    def admin_description(self):
1070*9c5db199SXin Li        """Returns a string representing the admin description."""
1071*9c5db199SXin Li        escaped_description = saxutils.escape(self.description)
1072*9c5db199SXin Li        return '<span style="white-space:pre">%s</span>' % escaped_description
1073*9c5db199SXin Li    admin_description.allow_tags = True
1074*9c5db199SXin Li    admin_description.short_description = 'Description'
1075*9c5db199SXin Li
1076*9c5db199SXin Li
1077*9c5db199SXin Li    class Meta:
1078*9c5db199SXin Li        """Metadata for class Test."""
1079*9c5db199SXin Li        db_table = 'afe_autotests'
1080*9c5db199SXin Li
1081*9c5db199SXin Li    def __unicode__(self):
1082*9c5db199SXin Li        return unicode(self.name)
1083*9c5db199SXin Li
1084*9c5db199SXin Li
1085*9c5db199SXin Liclass TestParameter(dbmodels.Model):
1086*9c5db199SXin Li    """
1087*9c5db199SXin Li    A declared parameter of a test
1088*9c5db199SXin Li    """
1089*9c5db199SXin Li    test = dbmodels.ForeignKey(Test)
1090*9c5db199SXin Li    name = dbmodels.CharField(max_length=255)
1091*9c5db199SXin Li
1092*9c5db199SXin Li    class Meta:
1093*9c5db199SXin Li        """Metadata for class TestParameter."""
1094*9c5db199SXin Li        db_table = 'afe_test_parameters'
1095*9c5db199SXin Li        unique_together = ('test', 'name')
1096*9c5db199SXin Li
1097*9c5db199SXin Li    def __unicode__(self):
1098*9c5db199SXin Li        return u'%s (%s)' % (self.name, self.test.name)
1099*9c5db199SXin Li
1100*9c5db199SXin Li
1101*9c5db199SXin Liclass Profiler(dbmodels.Model, model_logic.ModelExtensions):
1102*9c5db199SXin Li    """\
1103*9c5db199SXin Li    Required:
1104*9c5db199SXin Li    name: profiler name
1105*9c5db199SXin Li    test_type: Client or Server
1106*9c5db199SXin Li
1107*9c5db199SXin Li    Optional:
1108*9c5db199SXin Li    description: arbirary text description
1109*9c5db199SXin Li    """
1110*9c5db199SXin Li    name = dbmodels.CharField(max_length=255, unique=True)
1111*9c5db199SXin Li    description = dbmodels.TextField(blank=True)
1112*9c5db199SXin Li
1113*9c5db199SXin Li    name_field = 'name'
1114*9c5db199SXin Li    objects = model_logic.ExtendedManager()
1115*9c5db199SXin Li
1116*9c5db199SXin Li
1117*9c5db199SXin Li    class Meta:
1118*9c5db199SXin Li        """Metadata for class Profiler."""
1119*9c5db199SXin Li        db_table = 'afe_profilers'
1120*9c5db199SXin Li
1121*9c5db199SXin Li    def __unicode__(self):
1122*9c5db199SXin Li        return unicode(self.name)
1123*9c5db199SXin Li
1124*9c5db199SXin Li
1125*9c5db199SXin Liclass AclGroup(dbmodels.Model, model_logic.ModelExtensions):
1126*9c5db199SXin Li    """\
1127*9c5db199SXin Li    Required:
1128*9c5db199SXin Li    name: name of ACL group
1129*9c5db199SXin Li
1130*9c5db199SXin Li    Optional:
1131*9c5db199SXin Li    description: arbitrary description of group
1132*9c5db199SXin Li    """
1133*9c5db199SXin Li
1134*9c5db199SXin Li    SERIALIZATION_LINKS_TO_FOLLOW = set(['users'])
1135*9c5db199SXin Li
1136*9c5db199SXin Li    name = dbmodels.CharField(max_length=255, unique=True)
1137*9c5db199SXin Li    description = dbmodels.CharField(max_length=255, blank=True)
1138*9c5db199SXin Li    users = dbmodels.ManyToManyField(User, blank=False,
1139*9c5db199SXin Li                                     db_table='afe_acl_groups_users')
1140*9c5db199SXin Li    hosts = dbmodels.ManyToManyField(Host, blank=True,
1141*9c5db199SXin Li                                     db_table='afe_acl_groups_hosts')
1142*9c5db199SXin Li
1143*9c5db199SXin Li    name_field = 'name'
1144*9c5db199SXin Li    objects = model_logic.ExtendedManager()
1145*9c5db199SXin Li
1146*9c5db199SXin Li    @staticmethod
1147*9c5db199SXin Li    def check_for_acl_violation_hosts(hosts):
1148*9c5db199SXin Li        """Verify the current user has access to the specified hosts.
1149*9c5db199SXin Li
1150*9c5db199SXin Li        @param hosts: The hosts to verify against.
1151*9c5db199SXin Li        @raises AclAccessViolation if the current user doesn't have access
1152*9c5db199SXin Li            to a host.
1153*9c5db199SXin Li        """
1154*9c5db199SXin Li        user = User.current_user()
1155*9c5db199SXin Li        if user.is_superuser():
1156*9c5db199SXin Li            return
1157*9c5db199SXin Li        accessible_host_ids = set(
1158*9c5db199SXin Li            host.id for host in Host.objects.filter(aclgroup__users=user))
1159*9c5db199SXin Li        for host in hosts:
1160*9c5db199SXin Li            # Check if the user has access to this host,
1161*9c5db199SXin Li            # but only if it is not a metahost or a one-time-host.
1162*9c5db199SXin Li            no_access = (isinstance(host, Host)
1163*9c5db199SXin Li                         and not host.invalid
1164*9c5db199SXin Li                         and int(host.id) not in accessible_host_ids)
1165*9c5db199SXin Li            if no_access:
1166*9c5db199SXin Li                raise AclAccessViolation("%s does not have access to %s" %
1167*9c5db199SXin Li                                         (str(user), str(host)))
1168*9c5db199SXin Li
1169*9c5db199SXin Li
1170*9c5db199SXin Li    @staticmethod
1171*9c5db199SXin Li    def check_abort_permissions(queue_entries):
1172*9c5db199SXin Li        """Look for queue entries that aren't abortable by the current user.
1173*9c5db199SXin Li
1174*9c5db199SXin Li        An entry is not abortable if:
1175*9c5db199SXin Li           * the job isn't owned by this user, and
1176*9c5db199SXin Li           * the machine isn't ACL-accessible, or
1177*9c5db199SXin Li           * the machine is in the "Everyone" ACL
1178*9c5db199SXin Li
1179*9c5db199SXin Li        @param queue_entries: The queue entries to check.
1180*9c5db199SXin Li        @raises AclAccessViolation if a queue entry is not abortable by the
1181*9c5db199SXin Li            current user.
1182*9c5db199SXin Li        """
1183*9c5db199SXin Li        user = User.current_user()
1184*9c5db199SXin Li        if user.is_superuser():
1185*9c5db199SXin Li            return
1186*9c5db199SXin Li        not_owned = queue_entries.exclude(job__owner=user.login)
1187*9c5db199SXin Li        # I do this using ID sets instead of just Django filters because
1188*9c5db199SXin Li        # filtering on M2M dbmodels is broken in Django 0.96. It's better in
1189*9c5db199SXin Li        # 1.0.
1190*9c5db199SXin Li        # TODO: Use Django filters, now that we're using 1.0.
1191*9c5db199SXin Li        accessible_ids = set(
1192*9c5db199SXin Li            entry.id for entry
1193*9c5db199SXin Li            in not_owned.filter(host__aclgroup__users__login=user.login))
1194*9c5db199SXin Li        public_ids = set(entry.id for entry
1195*9c5db199SXin Li                         in not_owned.filter(host__aclgroup__name='Everyone'))
1196*9c5db199SXin Li        cannot_abort = [entry for entry in not_owned.select_related()
1197*9c5db199SXin Li                        if entry.id not in accessible_ids
1198*9c5db199SXin Li                        or entry.id in public_ids]
1199*9c5db199SXin Li        if len(cannot_abort) == 0:
1200*9c5db199SXin Li            return
1201*9c5db199SXin Li        entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
1202*9c5db199SXin Li                                              entry.host_or_metahost_name())
1203*9c5db199SXin Li                                for entry in cannot_abort)
1204*9c5db199SXin Li        raise AclAccessViolation('You cannot abort the following job entries: '
1205*9c5db199SXin Li                                 + entry_names)
1206*9c5db199SXin Li
1207*9c5db199SXin Li
1208*9c5db199SXin Li    def check_for_acl_violation_acl_group(self):
1209*9c5db199SXin Li        """Verifies the current user has acces to this ACL group.
1210*9c5db199SXin Li
1211*9c5db199SXin Li        @raises AclAccessViolation if the current user doesn't have access to
1212*9c5db199SXin Li            this ACL group.
1213*9c5db199SXin Li        """
1214*9c5db199SXin Li        user = User.current_user()
1215*9c5db199SXin Li        if user.is_superuser():
1216*9c5db199SXin Li            return
1217*9c5db199SXin Li        if self.name == 'Everyone':
1218*9c5db199SXin Li            raise AclAccessViolation("You cannot modify 'Everyone'!")
1219*9c5db199SXin Li        if not user in self.users.all():
1220*9c5db199SXin Li            raise AclAccessViolation("You do not have access to %s"
1221*9c5db199SXin Li                                     % self.name)
1222*9c5db199SXin Li
1223*9c5db199SXin Li    @staticmethod
1224*9c5db199SXin Li    def on_host_membership_change():
1225*9c5db199SXin Li        """Invoked when host membership changes."""
1226*9c5db199SXin Li        everyone = AclGroup.objects.get(name='Everyone')
1227*9c5db199SXin Li
1228*9c5db199SXin Li        # find hosts that aren't in any ACL group and add them to Everyone
1229*9c5db199SXin Li        # TODO(showard): this is a bit of a hack, since the fact that this query
1230*9c5db199SXin Li        # works is kind of a coincidence of Django internals.  This trick
1231*9c5db199SXin Li        # doesn't work in general (on all foreign key relationships).  I'll
1232*9c5db199SXin Li        # replace it with a better technique when the need arises.
1233*9c5db199SXin Li        orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True)
1234*9c5db199SXin Li        everyone.hosts.add(*orphaned_hosts.distinct())
1235*9c5db199SXin Li
1236*9c5db199SXin Li        # find hosts in both Everyone and another ACL group, and remove them
1237*9c5db199SXin Li        # from Everyone
1238*9c5db199SXin Li        hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone')
1239*9c5db199SXin Li        acled_hosts = set()
1240*9c5db199SXin Li        for host in hosts_in_everyone:
1241*9c5db199SXin Li            # Has an ACL group other than Everyone
1242*9c5db199SXin Li            if host.aclgroup_set.count() > 1:
1243*9c5db199SXin Li                acled_hosts.add(host)
1244*9c5db199SXin Li        everyone.hosts.remove(*acled_hosts)
1245*9c5db199SXin Li
1246*9c5db199SXin Li
1247*9c5db199SXin Li    def delete(self):
1248*9c5db199SXin Li        if (self.name == 'Everyone'):
1249*9c5db199SXin Li            raise AclAccessViolation("You cannot delete 'Everyone'!")
1250*9c5db199SXin Li        self.check_for_acl_violation_acl_group()
1251*9c5db199SXin Li        super(AclGroup, self).delete()
1252*9c5db199SXin Li        self.on_host_membership_change()
1253*9c5db199SXin Li
1254*9c5db199SXin Li
1255*9c5db199SXin Li    def add_current_user_if_empty(self):
1256*9c5db199SXin Li        """Adds the current user if the set of users is empty."""
1257*9c5db199SXin Li        if not self.users.count():
1258*9c5db199SXin Li            self.users.add(User.current_user())
1259*9c5db199SXin Li
1260*9c5db199SXin Li
1261*9c5db199SXin Li    def perform_after_save(self, change):
1262*9c5db199SXin Li        """Called after a save.
1263*9c5db199SXin Li
1264*9c5db199SXin Li        @param change: Whether there was a change.
1265*9c5db199SXin Li        """
1266*9c5db199SXin Li        if not change:
1267*9c5db199SXin Li            self.users.add(User.current_user())
1268*9c5db199SXin Li        self.add_current_user_if_empty()
1269*9c5db199SXin Li        self.on_host_membership_change()
1270*9c5db199SXin Li
1271*9c5db199SXin Li
1272*9c5db199SXin Li    def save(self, *args, **kwargs):
1273*9c5db199SXin Li        change = bool(self.id)
1274*9c5db199SXin Li        if change:
1275*9c5db199SXin Li            # Check the original object for an ACL violation
1276*9c5db199SXin Li            AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group()
1277*9c5db199SXin Li        super(AclGroup, self).save(*args, **kwargs)
1278*9c5db199SXin Li        self.perform_after_save(change)
1279*9c5db199SXin Li
1280*9c5db199SXin Li
1281*9c5db199SXin Li    class Meta:
1282*9c5db199SXin Li        """Metadata for class AclGroup."""
1283*9c5db199SXin Li        db_table = 'afe_acl_groups'
1284*9c5db199SXin Li
1285*9c5db199SXin Li    def __unicode__(self):
1286*9c5db199SXin Li        return unicode(self.name)
1287*9c5db199SXin Li
1288*9c5db199SXin Li
1289*9c5db199SXin Liclass ParameterizedJob(dbmodels.Model):
1290*9c5db199SXin Li    """
1291*9c5db199SXin Li    Auxiliary configuration for a parameterized job.
1292*9c5db199SXin Li
1293*9c5db199SXin Li    This class is obsolete, and ought to be dead.  Due to a series of
1294*9c5db199SXin Li    unfortunate events, it can't be deleted:
1295*9c5db199SXin Li      * In `class Job` we're required to keep a reference to this class
1296*9c5db199SXin Li        for the sake of the scheduler unit tests.
1297*9c5db199SXin Li      * The existence of the reference in `Job` means that certain
1298*9c5db199SXin Li        methods here will get called from the `get_jobs` RPC.
1299*9c5db199SXin Li    So, the definitions below seem to be the minimum stub we can support
1300*9c5db199SXin Li    unless/until we change the database schema.
1301*9c5db199SXin Li    """
1302*9c5db199SXin Li
1303*9c5db199SXin Li    @classmethod
1304*9c5db199SXin Li    def smart_get(cls, id_or_name, *args, **kwargs):
1305*9c5db199SXin Li        """For compatibility with Job.add_object.
1306*9c5db199SXin Li
1307*9c5db199SXin Li        @param cls: Implicit class object.
1308*9c5db199SXin Li        @param id_or_name: The ID or name to get.
1309*9c5db199SXin Li        @param args: Non-keyword arguments.
1310*9c5db199SXin Li        @param kwargs: Keyword arguments.
1311*9c5db199SXin Li        """
1312*9c5db199SXin Li        return cls.objects.get(pk=id_or_name)
1313*9c5db199SXin Li
1314*9c5db199SXin Li
1315*9c5db199SXin Li    def job(self):
1316*9c5db199SXin Li        """Returns the job if it exists, or else None."""
1317*9c5db199SXin Li        jobs = self.job_set.all()
1318*9c5db199SXin Li        assert jobs.count() <= 1
1319*9c5db199SXin Li        return jobs and jobs[0] or None
1320*9c5db199SXin Li
1321*9c5db199SXin Li
1322*9c5db199SXin Li    class Meta:
1323*9c5db199SXin Li        """Metadata for class ParameterizedJob."""
1324*9c5db199SXin Li        db_table = 'afe_parameterized_jobs'
1325*9c5db199SXin Li
1326*9c5db199SXin Li    def __unicode__(self):
1327*9c5db199SXin Li        return u'%s (parameterized) - %s' % (self.test.name, self.job())
1328*9c5db199SXin Li
1329*9c5db199SXin Li
1330*9c5db199SXin Liclass JobManager(model_logic.ExtendedManager):
1331*9c5db199SXin Li    'Custom manager to provide efficient status counts querying.'
1332*9c5db199SXin Li    def get_status_counts(self, job_ids):
1333*9c5db199SXin Li        """Returns a dict mapping the given job IDs to their status count dicts.
1334*9c5db199SXin Li
1335*9c5db199SXin Li        @param job_ids: A list of job IDs.
1336*9c5db199SXin Li        """
1337*9c5db199SXin Li        if not job_ids:
1338*9c5db199SXin Li            return {}
1339*9c5db199SXin Li        id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
1340*9c5db199SXin Li        cursor = connection.cursor()
1341*9c5db199SXin Li        cursor.execute("""
1342*9c5db199SXin Li            SELECT job_id, status, aborted, complete, COUNT(*)
1343*9c5db199SXin Li            FROM afe_host_queue_entries
1344*9c5db199SXin Li            WHERE job_id IN %s
1345*9c5db199SXin Li            GROUP BY job_id, status, aborted, complete
1346*9c5db199SXin Li            """ % id_list)
1347*9c5db199SXin Li        all_job_counts = dict((job_id, {}) for job_id in job_ids)
1348*9c5db199SXin Li        for job_id, status, aborted, complete, count in cursor.fetchall():
1349*9c5db199SXin Li            job_dict = all_job_counts[job_id]
1350*9c5db199SXin Li            full_status = HostQueueEntry.compute_full_status(status, aborted,
1351*9c5db199SXin Li                                                             complete)
1352*9c5db199SXin Li            job_dict.setdefault(full_status, 0)
1353*9c5db199SXin Li            job_dict[full_status] += count
1354*9c5db199SXin Li        return all_job_counts
1355*9c5db199SXin Li
1356*9c5db199SXin Li
1357*9c5db199SXin Liclass Job(dbmodels.Model, model_logic.ModelExtensions):
1358*9c5db199SXin Li    """\
1359*9c5db199SXin Li    owner: username of job owner
1360*9c5db199SXin Li    name: job name (does not have to be unique)
1361*9c5db199SXin Li    priority: Integer priority value.  Higher is more important.
1362*9c5db199SXin Li    control_file: contents of control file
1363*9c5db199SXin Li    control_type: Client or Server
1364*9c5db199SXin Li    created_on: date of job creation
1365*9c5db199SXin Li    submitted_on: date of job submission
1366*9c5db199SXin Li    synch_count: how many hosts should be used per autoserv execution
1367*9c5db199SXin Li    run_verify: Whether or not to run the verify phase
1368*9c5db199SXin Li    run_reset: Whether or not to run the reset phase
1369*9c5db199SXin Li    timeout: DEPRECATED - hours from queuing time until job times out
1370*9c5db199SXin Li    timeout_mins: minutes from job queuing time until the job times out
1371*9c5db199SXin Li    max_runtime_hrs: DEPRECATED - hours from job starting time until job
1372*9c5db199SXin Li                     times out
1373*9c5db199SXin Li    max_runtime_mins: minutes from job starting time until job times out
1374*9c5db199SXin Li    email_list: list of people to email on completion delimited by any of:
1375*9c5db199SXin Li                white space, ',', ':', ';'
1376*9c5db199SXin Li    dependency_labels: many-to-many relationship with labels corresponding to
1377*9c5db199SXin Li                       job dependencies
1378*9c5db199SXin Li    reboot_before: Never, If dirty, or Always
1379*9c5db199SXin Li    reboot_after: Never, If all tests passed, or Always
1380*9c5db199SXin Li    parse_failed_repair: if True, a failed repair launched by this job will have
1381*9c5db199SXin Li    its results parsed as part of the job.
1382*9c5db199SXin Li    drone_set: The set of drones to run this job on
1383*9c5db199SXin Li    parent_job: Parent job (optional)
1384*9c5db199SXin Li    test_retry: Number of times to retry test if the test did not complete
1385*9c5db199SXin Li                successfully. (optional, default: 0)
1386*9c5db199SXin Li    require_ssp: Require server-side packaging unless require_ssp is set to
1387*9c5db199SXin Li                 False. (optional, default: None)
1388*9c5db199SXin Li    """
1389*9c5db199SXin Li
1390*9c5db199SXin Li    # TODO: Investigate, if jobkeyval_set is really needed.
1391*9c5db199SXin Li    # dynamic_suite will write them into an attached file for the drone, but
1392*9c5db199SXin Li    # it doesn't seem like they are actually used. If they aren't used, remove
1393*9c5db199SXin Li    # jobkeyval_set here.
1394*9c5db199SXin Li    SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels',
1395*9c5db199SXin Li                                         'hostqueueentry_set',
1396*9c5db199SXin Li                                         'jobkeyval_set',
1397*9c5db199SXin Li                                         'shard'])
1398*9c5db199SXin Li
1399*9c5db199SXin Li    EXCLUDE_KNOWN_JOBS_CLAUSE = '''
1400*9c5db199SXin Li        AND NOT (afe_host_queue_entries.aborted = 0
1401*9c5db199SXin Li                 AND afe_jobs.id IN (%(known_ids)s))
1402*9c5db199SXin Li    '''
1403*9c5db199SXin Li
1404*9c5db199SXin Li    EXCLUDE_OLD_JOBS_CLAUSE = 'AND (afe_jobs.created_on > "%(cutoff)s")'
1405*9c5db199SXin Li
1406*9c5db199SXin Li    SQL_SHARD_JOBS = '''
1407*9c5db199SXin Li        SELECT DISTINCT(afe_jobs.id) FROM afe_jobs
1408*9c5db199SXin Li        INNER JOIN afe_host_queue_entries
1409*9c5db199SXin Li          ON (afe_jobs.id = afe_host_queue_entries.job_id)
1410*9c5db199SXin Li        LEFT OUTER JOIN afe_jobs_dependency_labels
1411*9c5db199SXin Li          ON (afe_jobs.id = afe_jobs_dependency_labels.job_id)
1412*9c5db199SXin Li        JOIN afe_shards_labels
1413*9c5db199SXin Li          ON (afe_shards_labels.label_id = afe_jobs_dependency_labels.label_id
1414*9c5db199SXin Li              OR afe_shards_labels.label_id = afe_host_queue_entries.meta_host)
1415*9c5db199SXin Li        WHERE (afe_shards_labels.shard_id = %(shard_id)s
1416*9c5db199SXin Li               AND afe_host_queue_entries.complete != 1
1417*9c5db199SXin Li               AND afe_host_queue_entries.active != 1
1418*9c5db199SXin Li               %(exclude_known_jobs)s
1419*9c5db199SXin Li               %(exclude_old_jobs)s)
1420*9c5db199SXin Li    '''
1421*9c5db199SXin Li
1422*9c5db199SXin Li    # Jobs can be created with assigned hosts and have no dependency
1423*9c5db199SXin Li    # labels nor meta_host.
1424*9c5db199SXin Li    # We are looking for:
1425*9c5db199SXin Li    #     - a job whose hqe's meta_host is null
1426*9c5db199SXin Li    #     - a job whose hqe has a host
1427*9c5db199SXin Li    #     - one of the host's labels matches the shard's label.
1428*9c5db199SXin Li    # Non-aborted known jobs, completed jobs, active jobs, jobs
1429*9c5db199SXin Li    # without hqe are exluded as we do with SQL_SHARD_JOBS.
1430*9c5db199SXin Li    SQL_SHARD_JOBS_WITH_HOSTS = '''
1431*9c5db199SXin Li        SELECT DISTINCT(afe_jobs.id) FROM afe_jobs
1432*9c5db199SXin Li        INNER JOIN afe_host_queue_entries
1433*9c5db199SXin Li          ON (afe_jobs.id = afe_host_queue_entries.job_id)
1434*9c5db199SXin Li        LEFT OUTER JOIN %(host_label_table)s
1435*9c5db199SXin Li          ON (afe_host_queue_entries.host_id = %(host_label_table)s.host_id)
1436*9c5db199SXin Li        WHERE (%(host_label_table)s.%(host_label_column)s IN %(label_ids)s
1437*9c5db199SXin Li               AND afe_host_queue_entries.complete != 1
1438*9c5db199SXin Li               AND afe_host_queue_entries.active != 1
1439*9c5db199SXin Li               AND afe_host_queue_entries.meta_host IS NULL
1440*9c5db199SXin Li               AND afe_host_queue_entries.host_id IS NOT NULL
1441*9c5db199SXin Li               %(exclude_known_jobs)s
1442*9c5db199SXin Li               %(exclude_old_jobs)s)
1443*9c5db199SXin Li    '''
1444*9c5db199SXin Li
1445*9c5db199SXin Li    # Even if we had filters about complete, active and aborted
1446*9c5db199SXin Li    # bits in the above two SQLs, there is a chance that
1447*9c5db199SXin Li    # the result may still contain a job with an hqe with 'complete=1'
1448*9c5db199SXin Li    # or 'active=1'.'
1449*9c5db199SXin Li    # This happens when a job has two (or more) hqes and at least
1450*9c5db199SXin Li    # one hqe has different bits than others.
1451*9c5db199SXin Li    # We use a second sql to ensure we exclude all un-desired jobs.
1452*9c5db199SXin Li    SQL_JOBS_TO_EXCLUDE = '''
1453*9c5db199SXin Li        SELECT afe_jobs.id FROM afe_jobs
1454*9c5db199SXin Li        INNER JOIN afe_host_queue_entries
1455*9c5db199SXin Li          ON (afe_jobs.id = afe_host_queue_entries.job_id)
1456*9c5db199SXin Li        WHERE (afe_jobs.id in (%(candidates)s)
1457*9c5db199SXin Li               AND (afe_host_queue_entries.complete=1
1458*9c5db199SXin Li                    OR afe_host_queue_entries.active=1))
1459*9c5db199SXin Li    '''
1460*9c5db199SXin Li
1461*9c5db199SXin Li    def _deserialize_relation(self, link, data):
1462*9c5db199SXin Li        if link in ['hostqueueentry_set', 'jobkeyval_set']:
1463*9c5db199SXin Li            for obj in data:
1464*9c5db199SXin Li                obj['job_id'] = self.id
1465*9c5db199SXin Li
1466*9c5db199SXin Li        super(Job, self)._deserialize_relation(link, data)
1467*9c5db199SXin Li
1468*9c5db199SXin Li
1469*9c5db199SXin Li    def custom_deserialize_relation(self, link, data):
1470*9c5db199SXin Li        assert link == 'shard', 'Link %s should not be deserialized' % link
1471*9c5db199SXin Li        self.shard = Shard.deserialize(data)
1472*9c5db199SXin Li
1473*9c5db199SXin Li
1474*9c5db199SXin Li    def _check_update_from_shard(self, shard, updated_serialized):
1475*9c5db199SXin Li        # If the job got aborted on the main after the client fetched it
1476*9c5db199SXin Li        # no shard_id will be set. The shard might still push updates though,
1477*9c5db199SXin Li        # as the job might complete before the abort bit syncs to the shard.
1478*9c5db199SXin Li        # Alternative considered: The main scheduler could be changed to not
1479*9c5db199SXin Li        # set aborted jobs to completed that are sharded out. But that would
1480*9c5db199SXin Li        # require database queries and seemed more complicated to implement.
1481*9c5db199SXin Li        # This seems safe to do, as there won't be updates pushed from the wrong
1482*9c5db199SXin Li        # shards should be powered off and wiped hen they are removed from the
1483*9c5db199SXin Li        # main.
1484*9c5db199SXin Li        if self.shard_id and self.shard_id != shard.id:
1485*9c5db199SXin Li            raise error.IgnorableUnallowedRecordsSentToMain(
1486*9c5db199SXin Li                'Job id=%s is assigned to shard (%s). Cannot update it with %s '
1487*9c5db199SXin Li                'from shard %s.' % (self.id, self.shard_id, updated_serialized,
1488*9c5db199SXin Li                                    shard.id))
1489*9c5db199SXin Li
1490*9c5db199SXin Li
1491*9c5db199SXin Li    RebootBefore = model_attributes.RebootBefore
1492*9c5db199SXin Li    RebootAfter = model_attributes.RebootAfter
1493*9c5db199SXin Li    # TIMEOUT is deprecated.
1494*9c5db199SXin Li    DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
1495*9c5db199SXin Li        'AUTOTEST_WEB', 'job_timeout_default', default=24)
1496*9c5db199SXin Li    DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value(
1497*9c5db199SXin Li        'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60)
1498*9c5db199SXin Li    # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is
1499*9c5db199SXin Li    # completed.
1500*9c5db199SXin Li    DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value(
1501*9c5db199SXin Li        'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72)
1502*9c5db199SXin Li    DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value(
1503*9c5db199SXin Li        'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60)
1504*9c5db199SXin Li    DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value(
1505*9c5db199SXin Li        'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool, default=False)
1506*9c5db199SXin Li    FETCH_READONLY_JOBS = global_config.global_config.get_config_value(
1507*9c5db199SXin Li        'AUTOTEST_WEB','readonly_heartbeat', type=bool, default=False)
1508*9c5db199SXin Li    # TODO(ayatane): Deprecated, not removed due to difficulty untangling imports
1509*9c5db199SXin Li    SKIP_JOBS_CREATED_BEFORE = 0
1510*9c5db199SXin Li
1511*9c5db199SXin Li
1512*9c5db199SXin Li
1513*9c5db199SXin Li    owner = dbmodels.CharField(max_length=255)
1514*9c5db199SXin Li    name = dbmodels.CharField(max_length=255)
1515*9c5db199SXin Li    priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT)
1516*9c5db199SXin Li    control_file = dbmodels.TextField(null=True, blank=True)
1517*9c5db199SXin Li    control_type = dbmodels.SmallIntegerField(
1518*9c5db199SXin Li        choices=control_data.CONTROL_TYPE.choices(),
1519*9c5db199SXin Li        blank=True, # to allow 0
1520*9c5db199SXin Li        default=control_data.CONTROL_TYPE.CLIENT)
1521*9c5db199SXin Li    created_on = dbmodels.DateTimeField()
1522*9c5db199SXin Li    synch_count = dbmodels.IntegerField(blank=True, default=0)
1523*9c5db199SXin Li    timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
1524*9c5db199SXin Li    run_verify = dbmodels.BooleanField(default=False)
1525*9c5db199SXin Li    email_list = dbmodels.CharField(max_length=250, blank=True)
1526*9c5db199SXin Li    dependency_labels = (
1527*9c5db199SXin Li            dbmodels.ManyToManyField(Label, blank=True,
1528*9c5db199SXin Li                                     db_table='afe_jobs_dependency_labels'))
1529*9c5db199SXin Li    reboot_before = dbmodels.SmallIntegerField(
1530*9c5db199SXin Li        choices=model_attributes.RebootBefore.choices(), blank=True,
1531*9c5db199SXin Li        default=DEFAULT_REBOOT_BEFORE)
1532*9c5db199SXin Li    reboot_after = dbmodels.SmallIntegerField(
1533*9c5db199SXin Li        choices=model_attributes.RebootAfter.choices(), blank=True,
1534*9c5db199SXin Li        default=DEFAULT_REBOOT_AFTER)
1535*9c5db199SXin Li    parse_failed_repair = dbmodels.BooleanField(
1536*9c5db199SXin Li        default=DEFAULT_PARSE_FAILED_REPAIR)
1537*9c5db199SXin Li    # max_runtime_hrs is deprecated. Will be removed after switch to mins is
1538*9c5db199SXin Li    # completed.
1539*9c5db199SXin Li    max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
1540*9c5db199SXin Li    max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS)
1541*9c5db199SXin Li    drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
1542*9c5db199SXin Li
1543*9c5db199SXin Li    # TODO(jrbarnette)  We have to keep `parameterized_job` around or it
1544*9c5db199SXin Li    # breaks the scheduler_models unit tests (and fixing the unit tests
1545*9c5db199SXin Li    # will break the scheduler, so don't do that).
1546*9c5db199SXin Li    #
1547*9c5db199SXin Li    # The ultimate fix is to delete the column from the database table
1548*9c5db199SXin Li    # at which point, you _must_ delete this.  Until you're ready to do
1549*9c5db199SXin Li    # that, DON'T MUCK WITH IT.
1550*9c5db199SXin Li    parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True,
1551*9c5db199SXin Li                                            blank=True)
1552*9c5db199SXin Li
1553*9c5db199SXin Li    parent_job = dbmodels.ForeignKey('self', blank=True, null=True)
1554*9c5db199SXin Li
1555*9c5db199SXin Li    test_retry = dbmodels.IntegerField(blank=True, default=0)
1556*9c5db199SXin Li
1557*9c5db199SXin Li    run_reset = dbmodels.BooleanField(default=True)
1558*9c5db199SXin Li
1559*9c5db199SXin Li    timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS)
1560*9c5db199SXin Li
1561*9c5db199SXin Li    # If this is None on the main, a shard should be found.
1562*9c5db199SXin Li    # If this is None on a shard, it should be synced back to the main
1563*9c5db199SXin Li    shard = dbmodels.ForeignKey(Shard, blank=True, null=True)
1564*9c5db199SXin Li
1565*9c5db199SXin Li    # If this is None, server-side packaging will be used for server side test.
1566*9c5db199SXin Li    require_ssp = dbmodels.NullBooleanField(default=None, blank=True, null=True)
1567*9c5db199SXin Li
1568*9c5db199SXin Li    # custom manager
1569*9c5db199SXin Li    objects = JobManager()
1570*9c5db199SXin Li
1571*9c5db199SXin Li
1572*9c5db199SXin Li    @decorators.cached_property
1573*9c5db199SXin Li    def labels(self):
1574*9c5db199SXin Li        """All the labels of this job"""
1575*9c5db199SXin Li        # We need to convert dependency_labels to a list, because all() gives us
1576*9c5db199SXin Li        # back an iterator, and storing/caching an iterator means we'd only be
1577*9c5db199SXin Li        # able to read from it once.
1578*9c5db199SXin Li        return list(self.dependency_labels.all())
1579*9c5db199SXin Li
1580*9c5db199SXin Li
1581*9c5db199SXin Li    def is_server_job(self):
1582*9c5db199SXin Li        """Returns whether this job is of type server."""
1583*9c5db199SXin Li        return self.control_type == control_data.CONTROL_TYPE.SERVER
1584*9c5db199SXin Li
1585*9c5db199SXin Li
1586*9c5db199SXin Li    @classmethod
1587*9c5db199SXin Li    def create(cls, owner, options, hosts):
1588*9c5db199SXin Li        """Creates a job.
1589*9c5db199SXin Li
1590*9c5db199SXin Li        The job is created by taking some information (the listed args) and
1591*9c5db199SXin Li        filling in the rest of the necessary information.
1592*9c5db199SXin Li
1593*9c5db199SXin Li        @param cls: Implicit class object.
1594*9c5db199SXin Li        @param owner: The owner for the job.
1595*9c5db199SXin Li        @param options: An options object.
1596*9c5db199SXin Li        @param hosts: The hosts to use.
1597*9c5db199SXin Li        """
1598*9c5db199SXin Li        AclGroup.check_for_acl_violation_hosts(hosts)
1599*9c5db199SXin Li
1600*9c5db199SXin Li        control_file = options.get('control_file')
1601*9c5db199SXin Li
1602*9c5db199SXin Li        user = User.current_user()
1603*9c5db199SXin Li        if options.get('reboot_before') is None:
1604*9c5db199SXin Li            options['reboot_before'] = user.get_reboot_before_display()
1605*9c5db199SXin Li        if options.get('reboot_after') is None:
1606*9c5db199SXin Li            options['reboot_after'] = user.get_reboot_after_display()
1607*9c5db199SXin Li
1608*9c5db199SXin Li        drone_set = DroneSet.resolve_name(options.get('drone_set'))
1609*9c5db199SXin Li
1610*9c5db199SXin Li        if options.get('timeout_mins') is None and options.get('timeout'):
1611*9c5db199SXin Li            options['timeout_mins'] = options['timeout'] * 60
1612*9c5db199SXin Li
1613*9c5db199SXin Li        job = cls.add_object(
1614*9c5db199SXin Li            owner=owner,
1615*9c5db199SXin Li            name=options['name'],
1616*9c5db199SXin Li            priority=options['priority'],
1617*9c5db199SXin Li            control_file=control_file,
1618*9c5db199SXin Li            control_type=options['control_type'],
1619*9c5db199SXin Li            synch_count=options.get('synch_count'),
1620*9c5db199SXin Li            # timeout needs to be deleted in the future.
1621*9c5db199SXin Li            timeout=options.get('timeout'),
1622*9c5db199SXin Li            timeout_mins=options.get('timeout_mins'),
1623*9c5db199SXin Li            max_runtime_mins=options.get('max_runtime_mins'),
1624*9c5db199SXin Li            run_verify=options.get('run_verify'),
1625*9c5db199SXin Li            email_list=options.get('email_list'),
1626*9c5db199SXin Li            reboot_before=options.get('reboot_before'),
1627*9c5db199SXin Li            reboot_after=options.get('reboot_after'),
1628*9c5db199SXin Li            parse_failed_repair=options.get('parse_failed_repair'),
1629*9c5db199SXin Li            created_on=datetime.now(),
1630*9c5db199SXin Li            drone_set=drone_set,
1631*9c5db199SXin Li            parent_job=options.get('parent_job_id'),
1632*9c5db199SXin Li            test_retry=options.get('test_retry'),
1633*9c5db199SXin Li            run_reset=options.get('run_reset'),
1634*9c5db199SXin Li            require_ssp=options.get('require_ssp'))
1635*9c5db199SXin Li
1636*9c5db199SXin Li        job.dependency_labels = options['dependencies']
1637*9c5db199SXin Li
1638*9c5db199SXin Li        if options.get('keyvals'):
1639*9c5db199SXin Li            for key, value in six.iteritems(options['keyvals']):
1640*9c5db199SXin Li                # None (or NULL) is not acceptable by DB, so change it to an
1641*9c5db199SXin Li                # empty string in case.
1642*9c5db199SXin Li                JobKeyval.objects.create(job=job, key=key,
1643*9c5db199SXin Li                                         value='' if value is None else value)
1644*9c5db199SXin Li
1645*9c5db199SXin Li        return job
1646*9c5db199SXin Li
1647*9c5db199SXin Li
1648*9c5db199SXin Li    @classmethod
1649*9c5db199SXin Li    def assign_to_shard(cls, shard, known_ids):
1650*9c5db199SXin Li        """Assigns unassigned jobs to a shard.
1651*9c5db199SXin Li
1652*9c5db199SXin Li        For all labels that have been assigned to this shard, all jobs that
1653*9c5db199SXin Li        have this label are assigned to this shard.
1654*9c5db199SXin Li
1655*9c5db199SXin Li        @param shard: The shard to assign jobs to.
1656*9c5db199SXin Li        @param known_ids: List of all ids of incomplete jobs the shard already
1657*9c5db199SXin Li                          knows about.
1658*9c5db199SXin Li
1659*9c5db199SXin Li        @returns The job objects that should be sent to the shard.
1660*9c5db199SXin Li        """
1661*9c5db199SXin Li        with cls._readonly_job_query_context():
1662*9c5db199SXin Li            job_ids = cls._get_new_jobs_for_shard(shard, known_ids)
1663*9c5db199SXin Li        if not job_ids:
1664*9c5db199SXin Li            return []
1665*9c5db199SXin Li        cls._assign_jobs_to_shard(job_ids, shard)
1666*9c5db199SXin Li        return cls._jobs_with_ids(job_ids)
1667*9c5db199SXin Li
1668*9c5db199SXin Li
1669*9c5db199SXin Li    @classmethod
1670*9c5db199SXin Li    @contextlib.contextmanager
1671*9c5db199SXin Li    def _readonly_job_query_context(cls):
1672*9c5db199SXin Li        #TODO: Get rid of this kludge if/when we update Django to >=1.7
1673*9c5db199SXin Li        #correct usage would be .raw(..., using='readonly')
1674*9c5db199SXin Li        old_db = Job.objects._db
1675*9c5db199SXin Li        try:
1676*9c5db199SXin Li            if cls.FETCH_READONLY_JOBS:
1677*9c5db199SXin Li                Job.objects._db = 'readonly'
1678*9c5db199SXin Li            yield
1679*9c5db199SXin Li        finally:
1680*9c5db199SXin Li            Job.objects._db = old_db
1681*9c5db199SXin Li
1682*9c5db199SXin Li
1683*9c5db199SXin Li    @classmethod
1684*9c5db199SXin Li    def _assign_jobs_to_shard(cls, job_ids, shard):
1685*9c5db199SXin Li        Job.objects.filter(pk__in=job_ids).update(shard=shard)
1686*9c5db199SXin Li
1687*9c5db199SXin Li
1688*9c5db199SXin Li    @classmethod
1689*9c5db199SXin Li    def _jobs_with_ids(cls, job_ids):
1690*9c5db199SXin Li        return list(Job.objects.filter(pk__in=job_ids).all())
1691*9c5db199SXin Li
1692*9c5db199SXin Li
1693*9c5db199SXin Li    @classmethod
1694*9c5db199SXin Li    def _get_new_jobs_for_shard(cls, shard, known_ids):
1695*9c5db199SXin Li        job_ids = cls._get_jobs_without_hosts(shard, known_ids)
1696*9c5db199SXin Li        job_ids |= cls._get_jobs_with_hosts(shard, known_ids)
1697*9c5db199SXin Li        if job_ids:
1698*9c5db199SXin Li            job_ids -= cls._filter_finished_jobs(job_ids)
1699*9c5db199SXin Li        return job_ids
1700*9c5db199SXin Li
1701*9c5db199SXin Li
1702*9c5db199SXin Li    @classmethod
1703*9c5db199SXin Li    def _filter_finished_jobs(cls, job_ids):
1704*9c5db199SXin Li        query = Job.objects.raw(
1705*9c5db199SXin Li                cls.SQL_JOBS_TO_EXCLUDE %
1706*9c5db199SXin Li                {'candidates': ','.join([str(i) for i in job_ids])})
1707*9c5db199SXin Li        return set([j.id for j in query])
1708*9c5db199SXin Li
1709*9c5db199SXin Li
1710*9c5db199SXin Li    @classmethod
1711*9c5db199SXin Li    def _get_jobs_without_hosts(cls, shard, known_ids):
1712*9c5db199SXin Li        raw_sql = cls.SQL_SHARD_JOBS % {
1713*9c5db199SXin Li            'exclude_known_jobs': cls._exclude_known_jobs_clause(known_ids),
1714*9c5db199SXin Li            'exclude_old_jobs': cls._exclude_old_jobs_clause(),
1715*9c5db199SXin Li            'shard_id': shard.id
1716*9c5db199SXin Li        }
1717*9c5db199SXin Li        return set([j.id for j in Job.objects.raw(raw_sql)])
1718*9c5db199SXin Li
1719*9c5db199SXin Li
1720*9c5db199SXin Li    @classmethod
1721*9c5db199SXin Li    def _get_jobs_with_hosts(cls, shard, known_ids):
1722*9c5db199SXin Li        job_ids = set([])
1723*9c5db199SXin Li        static_labels, non_static_labels = Host.classify_label_objects(
1724*9c5db199SXin Li                shard.labels.all())
1725*9c5db199SXin Li        if static_labels:
1726*9c5db199SXin Li            label_ids = [str(l.id) for l in static_labels]
1727*9c5db199SXin Li            query = Job.objects.raw(cls.SQL_SHARD_JOBS_WITH_HOSTS % {
1728*9c5db199SXin Li                'exclude_known_jobs': cls._exclude_known_jobs_clause(known_ids),
1729*9c5db199SXin Li                'exclude_old_jobs': cls._exclude_old_jobs_clause(),
1730*9c5db199SXin Li                'host_label_table': 'afe_static_hosts_labels',
1731*9c5db199SXin Li                'host_label_column': 'staticlabel_id',
1732*9c5db199SXin Li                'label_ids': '(%s)' % ','.join(label_ids)})
1733*9c5db199SXin Li            job_ids |= set([j.id for j in query])
1734*9c5db199SXin Li        if non_static_labels:
1735*9c5db199SXin Li            label_ids = [str(l.id) for l in non_static_labels]
1736*9c5db199SXin Li            query = Job.objects.raw(cls.SQL_SHARD_JOBS_WITH_HOSTS % {
1737*9c5db199SXin Li                'exclude_known_jobs': cls._exclude_known_jobs_clause(known_ids),
1738*9c5db199SXin Li                'exclude_old_jobs': cls._exclude_old_jobs_clause(),
1739*9c5db199SXin Li                'host_label_table': 'afe_hosts_labels',
1740*9c5db199SXin Li                'host_label_column': 'label_id',
1741*9c5db199SXin Li                'label_ids': '(%s)' % ','.join(label_ids)})
1742*9c5db199SXin Li            job_ids |= set([j.id for j in query])
1743*9c5db199SXin Li        return job_ids
1744*9c5db199SXin Li
1745*9c5db199SXin Li
1746*9c5db199SXin Li    @classmethod
1747*9c5db199SXin Li    def _exclude_known_jobs_clause(cls, known_ids):
1748*9c5db199SXin Li        if not known_ids:
1749*9c5db199SXin Li            return ''
1750*9c5db199SXin Li        return (cls.EXCLUDE_KNOWN_JOBS_CLAUSE %
1751*9c5db199SXin Li                {'known_ids': ','.join([str(i) for i in known_ids])})
1752*9c5db199SXin Li
1753*9c5db199SXin Li
1754*9c5db199SXin Li    @classmethod
1755*9c5db199SXin Li    def _exclude_old_jobs_clause(cls):
1756*9c5db199SXin Li        """Filter queried jobs to be created within a few hours in the past.
1757*9c5db199SXin Li
1758*9c5db199SXin Li        With this clause, any jobs older than a configurable number of hours are
1759*9c5db199SXin Li        skipped in the jobs query.
1760*9c5db199SXin Li        The job creation window affects the overall query performance. Longer
1761*9c5db199SXin Li        creation windows require a range query over more Job table rows using
1762*9c5db199SXin Li        the created_on column index. c.f. http://crbug.com/966872#c35
1763*9c5db199SXin Li        """
1764*9c5db199SXin Li        if cls.SKIP_JOBS_CREATED_BEFORE <= 0:
1765*9c5db199SXin Li            return ''
1766*9c5db199SXin Li        cutoff = datetime.now()- timedelta(hours=cls.SKIP_JOBS_CREATED_BEFORE)
1767*9c5db199SXin Li        return (cls.EXCLUDE_OLD_JOBS_CLAUSE %
1768*9c5db199SXin Li                {'cutoff': cutoff.strftime('%Y-%m-%d %H:%M:%S')})
1769*9c5db199SXin Li
1770*9c5db199SXin Li
1771*9c5db199SXin Li    def queue(self, hosts, is_template=False):
1772*9c5db199SXin Li        """Enqueue a job on the given hosts.
1773*9c5db199SXin Li
1774*9c5db199SXin Li        @param hosts: The hosts to use.
1775*9c5db199SXin Li        @param is_template: Whether the status should be "Template".
1776*9c5db199SXin Li        """
1777*9c5db199SXin Li        if not hosts:
1778*9c5db199SXin Li            # hostless job
1779*9c5db199SXin Li            entry = HostQueueEntry.create(job=self, is_template=is_template)
1780*9c5db199SXin Li            entry.save()
1781*9c5db199SXin Li            return
1782*9c5db199SXin Li
1783*9c5db199SXin Li        for host in hosts:
1784*9c5db199SXin Li            host.enqueue_job(self, is_template=is_template)
1785*9c5db199SXin Li
1786*9c5db199SXin Li
1787*9c5db199SXin Li    def user(self):
1788*9c5db199SXin Li        """Gets the user of this job, or None if it doesn't exist."""
1789*9c5db199SXin Li        try:
1790*9c5db199SXin Li            return User.objects.get(login=self.owner)
1791*9c5db199SXin Li        except self.DoesNotExist:
1792*9c5db199SXin Li            return None
1793*9c5db199SXin Li
1794*9c5db199SXin Li
1795*9c5db199SXin Li    def abort(self):
1796*9c5db199SXin Li        """Aborts this job."""
1797*9c5db199SXin Li        for queue_entry in self.hostqueueentry_set.all():
1798*9c5db199SXin Li            queue_entry.abort()
1799*9c5db199SXin Li
1800*9c5db199SXin Li
1801*9c5db199SXin Li    def tag(self):
1802*9c5db199SXin Li        """Returns a string tag for this job."""
1803*9c5db199SXin Li        return server_utils.get_job_tag(self.id, self.owner)
1804*9c5db199SXin Li
1805*9c5db199SXin Li
1806*9c5db199SXin Li    def keyval_dict(self):
1807*9c5db199SXin Li        """Returns all keyvals for this job as a dictionary."""
1808*9c5db199SXin Li        return dict((keyval.key, keyval.value)
1809*9c5db199SXin Li                    for keyval in self.jobkeyval_set.all())
1810*9c5db199SXin Li
1811*9c5db199SXin Li
1812*9c5db199SXin Li    @classmethod
1813*9c5db199SXin Li    def get_attribute_model(cls):
1814*9c5db199SXin Li        """Return the attribute model.
1815*9c5db199SXin Li
1816*9c5db199SXin Li        Override method in parent class. This class is called when
1817*9c5db199SXin Li        deserializing the one-to-many relationship betwen Job and JobKeyval.
1818*9c5db199SXin Li        On deserialization, we will try to clear any existing job keyvals
1819*9c5db199SXin Li        associated with a job to avoid any inconsistency.
1820*9c5db199SXin Li        Though Job doesn't implement ModelWithAttribute, we still treat
1821*9c5db199SXin Li        it as an attribute model for this purpose.
1822*9c5db199SXin Li
1823*9c5db199SXin Li        @returns: The attribute model of Job.
1824*9c5db199SXin Li        """
1825*9c5db199SXin Li        return JobKeyval
1826*9c5db199SXin Li
1827*9c5db199SXin Li
1828*9c5db199SXin Li    class Meta:
1829*9c5db199SXin Li        """Metadata for class Job."""
1830*9c5db199SXin Li        db_table = 'afe_jobs'
1831*9c5db199SXin Li
1832*9c5db199SXin Li    def __unicode__(self):
1833*9c5db199SXin Li        return u'%s (%s-%s)' % (self.name, self.id, self.owner)
1834*9c5db199SXin Li
1835*9c5db199SXin Li
1836*9c5db199SXin Liclass JobHandoff(dbmodels.Model, model_logic.ModelExtensions):
1837*9c5db199SXin Li    """Jobs that have been handed off to lucifer."""
1838*9c5db199SXin Li
1839*9c5db199SXin Li    job = dbmodels.OneToOneField(Job, on_delete=dbmodels.CASCADE,
1840*9c5db199SXin Li                                 primary_key=True)
1841*9c5db199SXin Li    created = dbmodels.DateTimeField(auto_now_add=True)
1842*9c5db199SXin Li    completed = dbmodels.BooleanField(default=False)
1843*9c5db199SXin Li    drone = dbmodels.CharField(
1844*9c5db199SXin Li        max_length=128, null=True,
1845*9c5db199SXin Li        help_text='''
1846*9c5db199SXin LiThe hostname of the drone the job is running on and whose job_aborter
1847*9c5db199SXin Lishould be responsible for aborting the job if the job process dies.
1848*9c5db199SXin LiNULL means any drone's job_aborter has free reign to abort the job.
1849*9c5db199SXin Li''')
1850*9c5db199SXin Li
1851*9c5db199SXin Li    class Meta:
1852*9c5db199SXin Li        """Metadata for class Job."""
1853*9c5db199SXin Li        db_table = 'afe_job_handoffs'
1854*9c5db199SXin Li
1855*9c5db199SXin Li
1856*9c5db199SXin Liclass JobKeyval(dbmodels.Model, model_logic.ModelExtensions):
1857*9c5db199SXin Li    """Keyvals associated with jobs"""
1858*9c5db199SXin Li
1859*9c5db199SXin Li    SERIALIZATION_LINKS_TO_KEEP = set(['job'])
1860*9c5db199SXin Li    SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value'])
1861*9c5db199SXin Li
1862*9c5db199SXin Li    job = dbmodels.ForeignKey(Job)
1863*9c5db199SXin Li    key = dbmodels.CharField(max_length=90)
1864*9c5db199SXin Li    value = dbmodels.CharField(max_length=300)
1865*9c5db199SXin Li
1866*9c5db199SXin Li    objects = model_logic.ExtendedManager()
1867*9c5db199SXin Li
1868*9c5db199SXin Li
1869*9c5db199SXin Li    @classmethod
1870*9c5db199SXin Li    def get_record(cls, data):
1871*9c5db199SXin Li        """Check the database for an identical record.
1872*9c5db199SXin Li
1873*9c5db199SXin Li        Use job_id and key to search for a existing record.
1874*9c5db199SXin Li
1875*9c5db199SXin Li        @raises: DoesNotExist, if no record found
1876*9c5db199SXin Li        @raises: MultipleObjectsReturned if multiple records found.
1877*9c5db199SXin Li        """
1878*9c5db199SXin Li        # TODO(fdeng): We should use job_id and key together as
1879*9c5db199SXin Li        #              a primary key in the db.
1880*9c5db199SXin Li        return cls.objects.get(job_id=data['job_id'], key=data['key'])
1881*9c5db199SXin Li
1882*9c5db199SXin Li
1883*9c5db199SXin Li    @classmethod
1884*9c5db199SXin Li    def deserialize(cls, data):
1885*9c5db199SXin Li        """Override deserialize in parent class.
1886*9c5db199SXin Li
1887*9c5db199SXin Li        Do not deserialize id as id is not kept consistent on main and shards.
1888*9c5db199SXin Li
1889*9c5db199SXin Li        @param data: A dictionary of data to deserialize.
1890*9c5db199SXin Li
1891*9c5db199SXin Li        @returns: A JobKeyval object.
1892*9c5db199SXin Li        """
1893*9c5db199SXin Li        if data:
1894*9c5db199SXin Li            data.pop('id')
1895*9c5db199SXin Li        return super(JobKeyval, cls).deserialize(data)
1896*9c5db199SXin Li
1897*9c5db199SXin Li
1898*9c5db199SXin Li    class Meta:
1899*9c5db199SXin Li        """Metadata for class JobKeyval."""
1900*9c5db199SXin Li        db_table = 'afe_job_keyvals'
1901*9c5db199SXin Li
1902*9c5db199SXin Li
1903*9c5db199SXin Liclass IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
1904*9c5db199SXin Li    """Represents an ineligible host queue."""
1905*9c5db199SXin Li    job = dbmodels.ForeignKey(Job)
1906*9c5db199SXin Li    host = dbmodels.ForeignKey(Host)
1907*9c5db199SXin Li
1908*9c5db199SXin Li    objects = model_logic.ExtendedManager()
1909*9c5db199SXin Li
1910*9c5db199SXin Li    class Meta:
1911*9c5db199SXin Li        """Metadata for class IneligibleHostQueue."""
1912*9c5db199SXin Li        db_table = 'afe_ineligible_host_queues'
1913*9c5db199SXin Li
1914*9c5db199SXin Li
1915*9c5db199SXin Liclass HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
1916*9c5db199SXin Li    """Represents a host queue entry."""
1917*9c5db199SXin Li
1918*9c5db199SXin Li    SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host'])
1919*9c5db199SXin Li    SERIALIZATION_LINKS_TO_KEEP = set(['host'])
1920*9c5db199SXin Li    SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted'])
1921*9c5db199SXin Li
1922*9c5db199SXin Li
1923*9c5db199SXin Li    def custom_deserialize_relation(self, link, data):
1924*9c5db199SXin Li        assert link == 'meta_host'
1925*9c5db199SXin Li        self.meta_host = Label.deserialize(data)
1926*9c5db199SXin Li
1927*9c5db199SXin Li
1928*9c5db199SXin Li    def _check_update_from_shard(self, shard, updated_serialized,
1929*9c5db199SXin Li                                       job_ids_sent):
1930*9c5db199SXin Li        if self.job_id not in job_ids_sent:
1931*9c5db199SXin Li            raise error.IgnorableUnallowedRecordsSentToMain(
1932*9c5db199SXin Li                'Sent HostQueueEntry without corresponding '
1933*9c5db199SXin Li                'job entry: %s' % updated_serialized)
1934*9c5db199SXin Li
1935*9c5db199SXin Li
1936*9c5db199SXin Li    Status = host_queue_entry_states.Status
1937*9c5db199SXin Li    ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES
1938*9c5db199SXin Li    COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES
1939*9c5db199SXin Li    PRE_JOB_STATUSES = host_queue_entry_states.PRE_JOB_STATUSES
1940*9c5db199SXin Li    IDLE_PRE_JOB_STATUSES = host_queue_entry_states.IDLE_PRE_JOB_STATUSES
1941*9c5db199SXin Li
1942*9c5db199SXin Li    job = dbmodels.ForeignKey(Job)
1943*9c5db199SXin Li    host = dbmodels.ForeignKey(Host, blank=True, null=True)
1944*9c5db199SXin Li    status = dbmodels.CharField(max_length=255)
1945*9c5db199SXin Li    meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
1946*9c5db199SXin Li                                    db_column='meta_host')
1947*9c5db199SXin Li    active = dbmodels.BooleanField(default=False)
1948*9c5db199SXin Li    complete = dbmodels.BooleanField(default=False)
1949*9c5db199SXin Li    deleted = dbmodels.BooleanField(default=False)
1950*9c5db199SXin Li    execution_subdir = dbmodels.CharField(max_length=255, blank=True,
1951*9c5db199SXin Li                                          default='')
1952*9c5db199SXin Li    # If atomic_group is set, this is a virtual HostQueueEntry that will
1953*9c5db199SXin Li    # be expanded into many actual hosts within the group at schedule time.
1954*9c5db199SXin Li    atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True)
1955*9c5db199SXin Li    aborted = dbmodels.BooleanField(default=False)
1956*9c5db199SXin Li    started_on = dbmodels.DateTimeField(null=True, blank=True)
1957*9c5db199SXin Li    finished_on = dbmodels.DateTimeField(null=True, blank=True)
1958*9c5db199SXin Li
1959*9c5db199SXin Li    objects = model_logic.ExtendedManager()
1960*9c5db199SXin Li
1961*9c5db199SXin Li
1962*9c5db199SXin Li    def __init__(self, *args, **kwargs):
1963*9c5db199SXin Li        super(HostQueueEntry, self).__init__(*args, **kwargs)
1964*9c5db199SXin Li        self._record_attributes(['status'])
1965*9c5db199SXin Li
1966*9c5db199SXin Li
1967*9c5db199SXin Li    @classmethod
1968*9c5db199SXin Li    def create(cls, job, host=None, meta_host=None,
1969*9c5db199SXin Li                 is_template=False):
1970*9c5db199SXin Li        """Creates a new host queue entry.
1971*9c5db199SXin Li
1972*9c5db199SXin Li        @param cls: Implicit class object.
1973*9c5db199SXin Li        @param job: The associated job.
1974*9c5db199SXin Li        @param host: The associated host.
1975*9c5db199SXin Li        @param meta_host: The associated meta host.
1976*9c5db199SXin Li        @param is_template: Whether the status should be "Template".
1977*9c5db199SXin Li        """
1978*9c5db199SXin Li        if is_template:
1979*9c5db199SXin Li            status = cls.Status.TEMPLATE
1980*9c5db199SXin Li        else:
1981*9c5db199SXin Li            status = cls.Status.QUEUED
1982*9c5db199SXin Li
1983*9c5db199SXin Li        return cls(job=job, host=host, meta_host=meta_host, status=status)
1984*9c5db199SXin Li
1985*9c5db199SXin Li
1986*9c5db199SXin Li    def save(self, *args, **kwargs):
1987*9c5db199SXin Li        self._set_active_and_complete()
1988*9c5db199SXin Li        super(HostQueueEntry, self).save(*args, **kwargs)
1989*9c5db199SXin Li        self._check_for_updated_attributes()
1990*9c5db199SXin Li
1991*9c5db199SXin Li
1992*9c5db199SXin Li    def execution_path(self):
1993*9c5db199SXin Li        """
1994*9c5db199SXin Li        Path to this entry's results (relative to the base results directory).
1995*9c5db199SXin Li        """
1996*9c5db199SXin Li        return server_utils.get_hqe_exec_path(self.job.tag(),
1997*9c5db199SXin Li                                              self.execution_subdir)
1998*9c5db199SXin Li
1999*9c5db199SXin Li
2000*9c5db199SXin Li    def host_or_metahost_name(self):
2001*9c5db199SXin Li        """Returns the first non-None name found in priority order.
2002*9c5db199SXin Li
2003*9c5db199SXin Li        The priority order checked is: (1) host name; (2) meta host name
2004*9c5db199SXin Li        """
2005*9c5db199SXin Li        if self.host:
2006*9c5db199SXin Li            return self.host.hostname
2007*9c5db199SXin Li        else:
2008*9c5db199SXin Li            assert self.meta_host
2009*9c5db199SXin Li            return self.meta_host.name
2010*9c5db199SXin Li
2011*9c5db199SXin Li
2012*9c5db199SXin Li    def _set_active_and_complete(self):
2013*9c5db199SXin Li        if self.status in self.ACTIVE_STATUSES:
2014*9c5db199SXin Li            self.active, self.complete = True, False
2015*9c5db199SXin Li        elif self.status in self.COMPLETE_STATUSES:
2016*9c5db199SXin Li            self.active, self.complete = False, True
2017*9c5db199SXin Li        else:
2018*9c5db199SXin Li            self.active, self.complete = False, False
2019*9c5db199SXin Li
2020*9c5db199SXin Li
2021*9c5db199SXin Li    def on_attribute_changed(self, attribute, old_value):
2022*9c5db199SXin Li        assert attribute == 'status'
2023*9c5db199SXin Li        logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id,
2024*9c5db199SXin Li                     self.status)
2025*9c5db199SXin Li
2026*9c5db199SXin Li
2027*9c5db199SXin Li    def is_meta_host_entry(self):
2028*9c5db199SXin Li        'True if this is a entry has a meta_host instead of a host.'
2029*9c5db199SXin Li        return self.host is None and self.meta_host is not None
2030*9c5db199SXin Li
2031*9c5db199SXin Li
2032*9c5db199SXin Li    # This code is shared between rpc_interface and models.HostQueueEntry.
2033*9c5db199SXin Li    # Sadly due to circular imports between the 2 (crbug.com/230100) making it
2034*9c5db199SXin Li    # a class method was the best way to refactor it. Attempting to put it in
2035*9c5db199SXin Li    # rpc_utils or a new utils module failed as that would require us to import
2036*9c5db199SXin Li    # models.py but to call it from here we would have to import the utils.py
2037*9c5db199SXin Li    # thus creating a cycle.
2038*9c5db199SXin Li    @classmethod
2039*9c5db199SXin Li    def abort_host_queue_entries(cls, host_queue_entries):
2040*9c5db199SXin Li        """Aborts a collection of host_queue_entries.
2041*9c5db199SXin Li
2042*9c5db199SXin Li        Abort these host queue entry and all host queue entries of jobs created
2043*9c5db199SXin Li        by them.
2044*9c5db199SXin Li
2045*9c5db199SXin Li        @param host_queue_entries: List of host queue entries we want to abort.
2046*9c5db199SXin Li        """
2047*9c5db199SXin Li        # This isn't completely immune to race conditions since it's not atomic,
2048*9c5db199SXin Li        # but it should be safe given the scheduler's behavior.
2049*9c5db199SXin Li
2050*9c5db199SXin Li        # TODO(milleral): crbug.com/230100
2051*9c5db199SXin Li        # The |abort_host_queue_entries| rpc does nearly exactly this,
2052*9c5db199SXin Li        # however, trying to re-use the code generates some horrible
2053*9c5db199SXin Li        # circular import error.  I'd be nice to refactor things around
2054*9c5db199SXin Li        # sometime so the code could be reused.
2055*9c5db199SXin Li
2056*9c5db199SXin Li        # Fixpoint algorithm to find the whole tree of HQEs to abort to
2057*9c5db199SXin Li        # minimize the total number of database queries:
2058*9c5db199SXin Li        children = set()
2059*9c5db199SXin Li        new_children = set(host_queue_entries)
2060*9c5db199SXin Li        while new_children:
2061*9c5db199SXin Li            children.update(new_children)
2062*9c5db199SXin Li            new_child_ids = [hqe.job_id for hqe in new_children]
2063*9c5db199SXin Li            new_children = HostQueueEntry.objects.filter(
2064*9c5db199SXin Li                    job__parent_job__in=new_child_ids,
2065*9c5db199SXin Li                    complete=False, aborted=False).all()
2066*9c5db199SXin Li            # To handle circular parental relationships
2067*9c5db199SXin Li            new_children = set(new_children) - children
2068*9c5db199SXin Li
2069*9c5db199SXin Li        # Associate a user with the host queue entries that we're about
2070*9c5db199SXin Li        # to abort so that we can look up who to blame for the aborts.
2071*9c5db199SXin Li        child_ids = [hqe.id for hqe in children]
2072*9c5db199SXin Li        # Get a list of hqe ids that already exists, so we can exclude them when
2073*9c5db199SXin Li        # we do bulk_create later to avoid IntegrityError.
2074*9c5db199SXin Li        existing_hqe_ids = set(AbortedHostQueueEntry.objects.
2075*9c5db199SXin Li                               filter(queue_entry_id__in=child_ids).
2076*9c5db199SXin Li                               values_list('queue_entry_id', flat=True))
2077*9c5db199SXin Li        now = datetime.now()
2078*9c5db199SXin Li        user = User.current_user()
2079*9c5db199SXin Li        aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe,
2080*9c5db199SXin Li                aborted_by=user, aborted_on=now) for hqe in children
2081*9c5db199SXin Li                        if hqe.id not in existing_hqe_ids]
2082*9c5db199SXin Li        AbortedHostQueueEntry.objects.bulk_create(aborted_hqes)
2083*9c5db199SXin Li        # Bulk update all of the HQEs to set the abort bit.
2084*9c5db199SXin Li        HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True)
2085*9c5db199SXin Li
2086*9c5db199SXin Li
2087*9c5db199SXin Li    def abort(self):
2088*9c5db199SXin Li        """ Aborts this host queue entry.
2089*9c5db199SXin Li
2090*9c5db199SXin Li        Abort this host queue entry and all host queue entries of jobs created by
2091*9c5db199SXin Li        this one.
2092*9c5db199SXin Li
2093*9c5db199SXin Li        """
2094*9c5db199SXin Li        if not self.complete and not self.aborted:
2095*9c5db199SXin Li            HostQueueEntry.abort_host_queue_entries([self])
2096*9c5db199SXin Li
2097*9c5db199SXin Li
2098*9c5db199SXin Li    @classmethod
2099*9c5db199SXin Li    def compute_full_status(cls, status, aborted, complete):
2100*9c5db199SXin Li        """Returns a modified status msg if the host queue entry was aborted.
2101*9c5db199SXin Li
2102*9c5db199SXin Li        @param cls: Implicit class object.
2103*9c5db199SXin Li        @param status: The original status message.
2104*9c5db199SXin Li        @param aborted: Whether the host queue entry was aborted.
2105*9c5db199SXin Li        @param complete: Whether the host queue entry was completed.
2106*9c5db199SXin Li        """
2107*9c5db199SXin Li        if aborted and not complete:
2108*9c5db199SXin Li            return 'Aborted (%s)' % status
2109*9c5db199SXin Li        return status
2110*9c5db199SXin Li
2111*9c5db199SXin Li
2112*9c5db199SXin Li    def full_status(self):
2113*9c5db199SXin Li        """Returns the full status of this host queue entry, as a string."""
2114*9c5db199SXin Li        return self.compute_full_status(self.status, self.aborted,
2115*9c5db199SXin Li                                        self.complete)
2116*9c5db199SXin Li
2117*9c5db199SXin Li
2118*9c5db199SXin Li    def _postprocess_object_dict(self, object_dict):
2119*9c5db199SXin Li        object_dict['full_status'] = self.full_status()
2120*9c5db199SXin Li
2121*9c5db199SXin Li
2122*9c5db199SXin Li    class Meta:
2123*9c5db199SXin Li        """Metadata for class HostQueueEntry."""
2124*9c5db199SXin Li        db_table = 'afe_host_queue_entries'
2125*9c5db199SXin Li
2126*9c5db199SXin Li
2127*9c5db199SXin Li
2128*9c5db199SXin Li    def __unicode__(self):
2129*9c5db199SXin Li        hostname = None
2130*9c5db199SXin Li        if self.host:
2131*9c5db199SXin Li            hostname = self.host.hostname
2132*9c5db199SXin Li        return u"%s/%d (%d)" % (hostname, self.job.id, self.id)
2133*9c5db199SXin Li
2134*9c5db199SXin Li
2135*9c5db199SXin Liclass HostQueueEntryStartTimes(dbmodels.Model):
2136*9c5db199SXin Li    """An auxilary table to HostQueueEntry to index by start time."""
2137*9c5db199SXin Li    insert_time = dbmodels.DateTimeField()
2138*9c5db199SXin Li    highest_hqe_id = dbmodels.IntegerField()
2139*9c5db199SXin Li
2140*9c5db199SXin Li    class Meta:
2141*9c5db199SXin Li        """Metadata for class HostQueueEntryStartTimes."""
2142*9c5db199SXin Li        db_table = 'afe_host_queue_entry_start_times'
2143*9c5db199SXin Li
2144*9c5db199SXin Li
2145*9c5db199SXin Liclass AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
2146*9c5db199SXin Li    """Represents an aborted host queue entry."""
2147*9c5db199SXin Li    queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
2148*9c5db199SXin Li    aborted_by = dbmodels.ForeignKey(User)
2149*9c5db199SXin Li    aborted_on = dbmodels.DateTimeField()
2150*9c5db199SXin Li
2151*9c5db199SXin Li    objects = model_logic.ExtendedManager()
2152*9c5db199SXin Li
2153*9c5db199SXin Li
2154*9c5db199SXin Li    def save(self, *args, **kwargs):
2155*9c5db199SXin Li        self.aborted_on = datetime.now()
2156*9c5db199SXin Li        super(AbortedHostQueueEntry, self).save(*args, **kwargs)
2157*9c5db199SXin Li
2158*9c5db199SXin Li    class Meta:
2159*9c5db199SXin Li        """Metadata for class AbortedHostQueueEntry."""
2160*9c5db199SXin Li        db_table = 'afe_aborted_host_queue_entries'
2161*9c5db199SXin Li
2162*9c5db199SXin Li
2163*9c5db199SXin Liclass SpecialTask(dbmodels.Model, model_logic.ModelExtensions):
2164*9c5db199SXin Li    """\
2165*9c5db199SXin Li    Tasks to run on hosts at the next time they are in the Ready state. Use this
2166*9c5db199SXin Li    for high-priority tasks, such as forced repair or forced reinstall.
2167*9c5db199SXin Li
2168*9c5db199SXin Li    host: host to run this task on
2169*9c5db199SXin Li    task: special task to run
2170*9c5db199SXin Li    time_requested: date and time the request for this task was made
2171*9c5db199SXin Li    is_active: task is currently running
2172*9c5db199SXin Li    is_complete: task has finished running
2173*9c5db199SXin Li    is_aborted: task was aborted
2174*9c5db199SXin Li    time_started: date and time the task started
2175*9c5db199SXin Li    time_finished: date and time the task finished
2176*9c5db199SXin Li    queue_entry: Host queue entry waiting on this task (or None, if task was not
2177*9c5db199SXin Li                 started in preparation of a job)
2178*9c5db199SXin Li    """
2179*9c5db199SXin Li    Task = autotest_enum.AutotestEnum('Verify', 'Cleanup', 'Repair', 'Reset',
2180*9c5db199SXin Li                                      'Provision', string_values=True)
2181*9c5db199SXin Li
2182*9c5db199SXin Li    host = dbmodels.ForeignKey(Host, blank=False, null=False)
2183*9c5db199SXin Li    task = dbmodels.CharField(max_length=64, choices=Task.choices(),
2184*9c5db199SXin Li                              blank=False, null=False)
2185*9c5db199SXin Li    requested_by = dbmodels.ForeignKey(User)
2186*9c5db199SXin Li    time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
2187*9c5db199SXin Li                                            null=False)
2188*9c5db199SXin Li    is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
2189*9c5db199SXin Li    is_complete = dbmodels.BooleanField(default=False, blank=False, null=False)
2190*9c5db199SXin Li    is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False)
2191*9c5db199SXin Li    time_started = dbmodels.DateTimeField(null=True, blank=True)
2192*9c5db199SXin Li    queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True)
2193*9c5db199SXin Li    success = dbmodels.BooleanField(default=False, blank=False, null=False)
2194*9c5db199SXin Li    time_finished = dbmodels.DateTimeField(null=True, blank=True)
2195*9c5db199SXin Li
2196*9c5db199SXin Li    objects = model_logic.ExtendedManager()
2197*9c5db199SXin Li
2198*9c5db199SXin Li
2199*9c5db199SXin Li    def save(self, **kwargs):
2200*9c5db199SXin Li        if self.queue_entry:
2201*9c5db199SXin Li            self.requested_by = User.objects.get(
2202*9c5db199SXin Li                    login=self.queue_entry.job.owner)
2203*9c5db199SXin Li        super(SpecialTask, self).save(**kwargs)
2204*9c5db199SXin Li
2205*9c5db199SXin Li
2206*9c5db199SXin Li    def execution_path(self):
2207*9c5db199SXin Li        """Returns the execution path for a special task."""
2208*9c5db199SXin Li        return server_utils.get_special_task_exec_path(
2209*9c5db199SXin Li                self.host.hostname, self.id, self.task, self.time_requested)
2210*9c5db199SXin Li
2211*9c5db199SXin Li
2212*9c5db199SXin Li    # property to emulate HostQueueEntry.status
2213*9c5db199SXin Li    @property
2214*9c5db199SXin Li    def status(self):
2215*9c5db199SXin Li        """Returns a host queue entry status appropriate for a speical task."""
2216*9c5db199SXin Li        return server_utils.get_special_task_status(
2217*9c5db199SXin Li                self.is_complete, self.success, self.is_active)
2218*9c5db199SXin Li
2219*9c5db199SXin Li
2220*9c5db199SXin Li    # property to emulate HostQueueEntry.started_on
2221*9c5db199SXin Li    @property
2222*9c5db199SXin Li    def started_on(self):
2223*9c5db199SXin Li        """Returns the time at which this special task started."""
2224*9c5db199SXin Li        return self.time_started
2225*9c5db199SXin Li
2226*9c5db199SXin Li
2227*9c5db199SXin Li    @classmethod
2228*9c5db199SXin Li    def schedule_special_task(cls, host, task):
2229*9c5db199SXin Li        """Schedules a special task on a host if not already scheduled.
2230*9c5db199SXin Li
2231*9c5db199SXin Li        @param cls: Implicit class object.
2232*9c5db199SXin Li        @param host: The host to use.
2233*9c5db199SXin Li        @param task: The task to schedule.
2234*9c5db199SXin Li        """
2235*9c5db199SXin Li        existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task,
2236*9c5db199SXin Li                                                    is_active=False,
2237*9c5db199SXin Li                                                    is_complete=False)
2238*9c5db199SXin Li        if existing_tasks:
2239*9c5db199SXin Li            return existing_tasks[0]
2240*9c5db199SXin Li
2241*9c5db199SXin Li        special_task = SpecialTask(host=host, task=task,
2242*9c5db199SXin Li                                   requested_by=User.current_user())
2243*9c5db199SXin Li        special_task.save()
2244*9c5db199SXin Li        return special_task
2245*9c5db199SXin Li
2246*9c5db199SXin Li
2247*9c5db199SXin Li    def abort(self):
2248*9c5db199SXin Li        """ Abort this special task."""
2249*9c5db199SXin Li        self.is_aborted = True
2250*9c5db199SXin Li        self.save()
2251*9c5db199SXin Li
2252*9c5db199SXin Li
2253*9c5db199SXin Li    def activate(self):
2254*9c5db199SXin Li        """
2255*9c5db199SXin Li        Sets a task as active and sets the time started to the current time.
2256*9c5db199SXin Li        """
2257*9c5db199SXin Li        logging.info('Starting: %s', self)
2258*9c5db199SXin Li        self.is_active = True
2259*9c5db199SXin Li        self.time_started = datetime.now()
2260*9c5db199SXin Li        self.save()
2261*9c5db199SXin Li
2262*9c5db199SXin Li
2263*9c5db199SXin Li    def finish(self, success):
2264*9c5db199SXin Li        """Sets a task as completed.
2265*9c5db199SXin Li
2266*9c5db199SXin Li        @param success: Whether or not the task was successful.
2267*9c5db199SXin Li        """
2268*9c5db199SXin Li        logging.info('Finished: %s', self)
2269*9c5db199SXin Li        self.is_active = False
2270*9c5db199SXin Li        self.is_complete = True
2271*9c5db199SXin Li        self.success = success
2272*9c5db199SXin Li        if self.time_started:
2273*9c5db199SXin Li            self.time_finished = datetime.now()
2274*9c5db199SXin Li        self.save()
2275*9c5db199SXin Li
2276*9c5db199SXin Li
2277*9c5db199SXin Li    class Meta:
2278*9c5db199SXin Li        """Metadata for class SpecialTask."""
2279*9c5db199SXin Li        db_table = 'afe_special_tasks'
2280*9c5db199SXin Li
2281*9c5db199SXin Li
2282*9c5db199SXin Li    def __unicode__(self):
2283*9c5db199SXin Li        result = u'Special Task %s (host %s, task %s, time %s)' % (
2284*9c5db199SXin Li            self.id, self.host, self.task, self.time_requested)
2285*9c5db199SXin Li        if self.is_complete:
2286*9c5db199SXin Li            result += u' (completed)'
2287*9c5db199SXin Li        elif self.is_active:
2288*9c5db199SXin Li            result += u' (active)'
2289*9c5db199SXin Li
2290*9c5db199SXin Li        return result
2291*9c5db199SXin Li
2292*9c5db199SXin Li
2293*9c5db199SXin Liclass StableVersion(dbmodels.Model, model_logic.ModelExtensions):
2294*9c5db199SXin Li
2295*9c5db199SXin Li    board = dbmodels.CharField(max_length=255, unique=True)
2296*9c5db199SXin Li    version = dbmodels.CharField(max_length=255)
2297*9c5db199SXin Li
2298*9c5db199SXin Li    class Meta:
2299*9c5db199SXin Li        """Metadata for class StableVersion."""
2300*9c5db199SXin Li        db_table = 'afe_stable_versions'
2301*9c5db199SXin Li
2302*9c5db199SXin Li    def save(self, *args, **kwargs):
2303*9c5db199SXin Li        if os.getenv("OVERRIDE_STABLE_VERSION_BAN"):
2304*9c5db199SXin Li            super(StableVersion, self).save(*args, **kwargs)
2305*9c5db199SXin Li        else:
2306*9c5db199SXin Li            raise RuntimeError("the ability to save StableVersions has been intentionally removed")
2307*9c5db199SXin Li
2308*9c5db199SXin Li    # pylint:disable=undefined-variable
2309*9c5db199SXin Li    def delete(self):
2310*9c5db199SXin Li        if os.getenv("OVERRIDE_STABLE_VERSION_BAN"):
2311*9c5db199SXin Li            super(StableVersion, self).delete(*args, **kwargs)
2312*9c5db199SXin Li        else:
2313*9c5db199SXin Li            raise RuntimeError("the ability to delete StableVersions has been intentionally removed")
2314