models.py revision 67831ae7a70ab35b48f98fad7f14d285b69d9bd9
1from datetime import datetime
2from django.db import models as dbmodels, connection
3from frontend.afe import model_logic
4from frontend import settings, thread_local
5from autotest_lib.client.common_lib import enum, host_protections, global_config
6from autotest_lib.client.common_lib import debug
7
8logger = debug.get_logger()
9
10# job options and user preferences
11RebootBefore = enum.Enum('Never', 'If dirty', 'Always')
12DEFAULT_REBOOT_BEFORE = RebootBefore.IF_DIRTY
13RebootAfter = enum.Enum('Never', 'If all tests passed', 'Always')
14DEFAULT_REBOOT_AFTER = RebootBefore.ALWAYS
15
16class AclAccessViolation(Exception):
17    """\
18    Raised when an operation is attempted with proper permissions as
19    dictated by ACLs.
20    """
21
22
23class Label(model_logic.ModelWithInvalid, dbmodels.Model):
24    """\
25    Required:
26    name: label name
27
28    Optional:
29    kernel_config: url/path to kernel config to use for jobs run on this
30                   label
31    platform: if True, this is a platform label (defaults to False)
32    only_if_needed: if True, a machine with this label can only be used if that
33                    that label is requested by the job/test.
34    """
35    name = dbmodels.CharField(maxlength=255, unique=True)
36    kernel_config = dbmodels.CharField(maxlength=255, blank=True)
37    platform = dbmodels.BooleanField(default=False)
38    invalid = dbmodels.BooleanField(default=False,
39                                    editable=settings.FULL_ADMIN)
40    only_if_needed = dbmodels.BooleanField(default=False)
41
42    name_field = 'name'
43    objects = model_logic.ExtendedManager()
44    valid_objects = model_logic.ValidObjectsManager()
45
46    def clean_object(self):
47        self.host_set.clear()
48
49
50    def enqueue_job(self, job):
51        'Enqueue a job on any host of this label.'
52        queue_entry = HostQueueEntry(meta_host=self, job=job,
53                                     status=HostQueueEntry.Status.QUEUED,
54                                     priority=job.priority)
55        queue_entry.save()
56
57
58    class Meta:
59        db_table = 'labels'
60
61    class Admin:
62        list_display = ('name', 'kernel_config')
63        # see Host.Admin
64        manager = model_logic.ValidObjectsManager()
65
66    def __str__(self):
67        return self.name
68
69
70class User(dbmodels.Model, model_logic.ModelExtensions):
71    """\
72    Required:
73    login :user login name
74
75    Optional:
76    access_level: 0=User (default), 1=Admin, 100=Root
77    """
78    ACCESS_ROOT = 100
79    ACCESS_ADMIN = 1
80    ACCESS_USER = 0
81
82    login = dbmodels.CharField(maxlength=255, unique=True)
83    access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True)
84
85    # user preferences
86    reboot_before = dbmodels.SmallIntegerField(choices=RebootBefore.choices(),
87                                               blank=True,
88                                               default=DEFAULT_REBOOT_BEFORE)
89    reboot_after = dbmodels.SmallIntegerField(choices=RebootAfter.choices(),
90                                              blank=True,
91                                              default=DEFAULT_REBOOT_AFTER)
92    show_experimental = dbmodels.BooleanField(default=False)
93
94    name_field = 'login'
95    objects = model_logic.ExtendedManager()
96
97
98    def save(self):
99        # is this a new object being saved for the first time?
100        first_time = (self.id is None)
101        user = thread_local.get_user()
102        if user and not user.is_superuser() and user.login != self.login:
103            raise AclAccessViolation("You cannot modify user " + self.login)
104        super(User, self).save()
105        if first_time:
106            everyone = AclGroup.objects.get(name='Everyone')
107            everyone.users.add(self)
108
109
110    def is_superuser(self):
111        return self.access_level >= self.ACCESS_ROOT
112
113
114    class Meta:
115        db_table = 'users'
116
117    class Admin:
118        list_display = ('login', 'access_level')
119        search_fields = ('login',)
120
121    def __str__(self):
122        return self.login
123
124
125class Host(model_logic.ModelWithInvalid, dbmodels.Model):
126    """\
127    Required:
128    hostname
129
130    optional:
131    locked: if true, host is locked and will not be queued
132
133    Internal:
134    synch_id: currently unused
135    status: string describing status of host
136    invalid: true if the host has been deleted
137    protection: indicates what can be done to this host during repair
138    locked_by: user that locked the host, or null if the host is unlocked
139    lock_time: DateTime at which the host was locked
140    dirty: true if the host has been used without being rebooted
141    """
142    Status = enum.Enum('Verifying', 'Running', 'Ready', 'Repairing',
143                       'Repair Failed', 'Dead', 'Cleaning', 'Pending',
144                        string_values=True)
145
146    hostname = dbmodels.CharField(maxlength=255, unique=True)
147    labels = dbmodels.ManyToManyField(Label, blank=True,
148                                      filter_interface=dbmodels.HORIZONTAL)
149    locked = dbmodels.BooleanField(default=False)
150    synch_id = dbmodels.IntegerField(blank=True, null=True,
151                                     editable=settings.FULL_ADMIN)
152    status = dbmodels.CharField(maxlength=255, default=Status.READY,
153                                choices=Status.choices(),
154                                editable=settings.FULL_ADMIN)
155    invalid = dbmodels.BooleanField(default=False,
156                                    editable=settings.FULL_ADMIN)
157    protection = dbmodels.SmallIntegerField(null=False, blank=True,
158                                            choices=host_protections.choices,
159                                            default=host_protections.default)
160    locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False)
161    lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False)
162    dirty = dbmodels.BooleanField(default=True, editable=settings.FULL_ADMIN)
163
164    name_field = 'hostname'
165    objects = model_logic.ExtendedManager()
166    valid_objects = model_logic.ValidObjectsManager()
167
168
169    def __init__(self, *args, **kwargs):
170        super(Host, self).__init__(*args, **kwargs)
171        self._record_attributes(['status'])
172
173
174    @staticmethod
175    def create_one_time_host(hostname):
176        query = Host.objects.filter(hostname=hostname)
177        if query.count() == 0:
178            host = Host(hostname=hostname, invalid=True)
179            host.do_validate()
180        else:
181            host = query[0]
182            if not host.invalid:
183                raise model_logic.ValidationError({
184                    'hostname' : '%s already exists!' % hostname
185                    })
186            host.clean_object()
187            AclGroup.objects.get(name='Everyone').hosts.add(host)
188            host.status = Host.Status.READY
189        host.protection = host_protections.Protection.DO_NOT_REPAIR
190        host.save()
191        return host
192
193    def clean_object(self):
194        self.aclgroup_set.clear()
195        self.labels.clear()
196
197
198    def save(self):
199        # extra spaces in the hostname can be a sneaky source of errors
200        self.hostname = self.hostname.strip()
201        # is this a new object being saved for the first time?
202        first_time = (self.id is None)
203        if not first_time:
204            AclGroup.check_for_acl_violation_hosts([self])
205        if self.locked and not self.locked_by:
206            self.locked_by = thread_local.get_user()
207            self.lock_time = datetime.now()
208            self.dirty = True
209        elif not self.locked and self.locked_by:
210            self.locked_by = None
211            self.lock_time = None
212        super(Host, self).save()
213        if first_time:
214            everyone = AclGroup.objects.get(name='Everyone')
215            everyone.hosts.add(self)
216        self._check_for_updated_attributes()
217
218
219    def delete(self):
220        AclGroup.check_for_acl_violation_hosts([self])
221        for queue_entry in self.hostqueueentry_set.all():
222            queue_entry.deleted = True
223            queue_entry.abort(thread_local.get_user())
224        super(Host, self).delete()
225
226
227    def on_attribute_changed(self, attribute, old_value):
228        assert attribute == 'status'
229        logger.info(self.hostname + ' -> ' + self.status)
230
231
232    def enqueue_job(self, job):
233        ' Enqueue a job on this host.'
234        queue_entry = HostQueueEntry(host=self, job=job,
235                                     status=HostQueueEntry.Status.QUEUED,
236                                     priority=job.priority)
237        # allow recovery of dead hosts from the frontend
238        if not self.active_queue_entry() and self.is_dead():
239            self.status = Host.Status.READY
240            self.save()
241        queue_entry.save()
242
243        block = IneligibleHostQueue(job=job, host=self)
244        block.save()
245
246
247    def platform(self):
248        # TODO(showard): slighly hacky?
249        platforms = self.labels.filter(platform=True)
250        if len(platforms) == 0:
251            return None
252        return platforms[0]
253    platform.short_description = 'Platform'
254
255
256    def is_dead(self):
257        return self.status == Host.Status.REPAIR_FAILED
258
259
260    def active_queue_entry(self):
261        active = list(self.hostqueueentry_set.filter(active=True))
262        if not active:
263            return None
264        assert len(active) == 1, ('More than one active entry for '
265                                  'host ' + self.hostname)
266        return active[0]
267
268
269    class Meta:
270        db_table = 'hosts'
271
272    class Admin:
273        # TODO(showard) - showing platform requires a SQL query for
274        # each row (since labels are many-to-many) - should we remove
275        # it?
276        list_display = ('hostname', 'platform', 'locked', 'status')
277        list_filter = ('labels', 'locked', 'protection')
278        search_fields = ('hostname', 'status')
279        # undocumented Django feature - if you set manager here, the
280        # admin code will use it, otherwise it'll use a default Manager
281        manager = model_logic.ValidObjectsManager()
282
283    def __str__(self):
284        return self.hostname
285
286
287class Test(dbmodels.Model, model_logic.ModelExtensions):
288    """\
289    Required:
290    author: author name
291    description: description of the test
292    name: test name
293    time: short, medium, long
294    test_class: This describes the class for your the test belongs in.
295    test_category: This describes the category for your tests
296    test_type: Client or Server
297    path: path to pass to run_test()
298    sync_count:  is a number >=1 (1 being the default). If it's 1, then it's an
299                 async job. If it's >1 it's sync job for that number of machines
300                 i.e. if sync_count = 2 it is a sync job that requires two
301                 machines.
302    Optional:
303    dependencies: What the test requires to run. Comma deliminated list
304    dependency_labels: many-to-many relationship with labels corresponding to
305                       test dependencies.
306    experimental: If this is set to True production servers will ignore the test
307    run_verify: Whether or not the scheduler should run the verify stage
308    """
309    TestTime = enum.Enum('SHORT', 'MEDIUM', 'LONG', start_value=1)
310    # TODO(showard) - this should be merged with Job.ControlType (but right
311    # now they use opposite values)
312    Types = enum.Enum('Client', 'Server', start_value=1)
313
314    name = dbmodels.CharField(maxlength=255, unique=True)
315    author = dbmodels.CharField(maxlength=255)
316    test_class = dbmodels.CharField(maxlength=255)
317    test_category = dbmodels.CharField(maxlength=255)
318    dependencies = dbmodels.CharField(maxlength=255, blank=True)
319    description = dbmodels.TextField(blank=True)
320    experimental = dbmodels.BooleanField(default=True)
321    run_verify = dbmodels.BooleanField(default=True)
322    test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(),
323                                           default=TestTime.MEDIUM)
324    test_type = dbmodels.SmallIntegerField(choices=Types.choices())
325    sync_count = dbmodels.IntegerField(default=1)
326    path = dbmodels.CharField(maxlength=255, unique=True)
327    dependency_labels = dbmodels.ManyToManyField(
328        Label, blank=True, filter_interface=dbmodels.HORIZONTAL)
329
330    name_field = 'name'
331    objects = model_logic.ExtendedManager()
332
333
334    class Meta:
335        db_table = 'autotests'
336
337    class Admin:
338        fields = (
339            (None, {'fields' :
340                    ('name', 'author', 'test_category', 'test_class',
341                     'test_time', 'sync_count', 'test_type', 'sync_count',
342                     'path', 'dependencies', 'experimental', 'run_verify',
343                     'description')}),
344            )
345        list_display = ('name', 'test_type', 'description', 'sync_count')
346        search_fields = ('name',)
347
348    def __str__(self):
349        return self.name
350
351
352class Profiler(dbmodels.Model, model_logic.ModelExtensions):
353    """\
354    Required:
355    name: profiler name
356    test_type: Client or Server
357
358    Optional:
359    description: arbirary text description
360    """
361    name = dbmodels.CharField(maxlength=255, unique=True)
362    description = dbmodels.TextField(blank=True)
363
364    name_field = 'name'
365    objects = model_logic.ExtendedManager()
366
367
368    class Meta:
369        db_table = 'profilers'
370
371    class Admin:
372        list_display = ('name', 'description')
373        search_fields = ('name',)
374
375    def __str__(self):
376        return self.name
377
378
379class AclGroup(dbmodels.Model, model_logic.ModelExtensions):
380    """\
381    Required:
382    name: name of ACL group
383
384    Optional:
385    description: arbitrary description of group
386    """
387    name = dbmodels.CharField(maxlength=255, unique=True)
388    description = dbmodels.CharField(maxlength=255, blank=True)
389    users = dbmodels.ManyToManyField(User, blank=True,
390                                     filter_interface=dbmodels.HORIZONTAL)
391    hosts = dbmodels.ManyToManyField(Host,
392                                     filter_interface=dbmodels.HORIZONTAL)
393
394    name_field = 'name'
395    objects = model_logic.ExtendedManager()
396
397    @staticmethod
398    def check_for_acl_violation_hosts(hosts):
399        user = thread_local.get_user()
400        if user.is_superuser():
401            return
402        accessible_host_ids = set(
403            host.id for host in Host.objects.filter(acl_group__users=user))
404        for host in hosts:
405            # Check if the user has access to this host,
406            # but only if it is not a metahost
407            if (isinstance(host, Host)
408                and int(host.id) not in accessible_host_ids):
409                raise AclAccessViolation("You do not have access to %s"
410                                         % str(host))
411
412
413    @staticmethod
414    def check_abort_permissions(queue_entries):
415        """
416        look for queue entries that aren't abortable, meaning
417         * the job isn't owned by this user, and
418           * the machine isn't ACL-accessible, or
419           * the machine is in the "Everyone" ACL
420        """
421        user = thread_local.get_user()
422        if user.is_superuser():
423            return
424        not_owned = queue_entries.exclude(job__owner=user.login)
425        # I do this using ID sets instead of just Django filters because
426        # filtering on M2M fields is broken in Django 0.96.  It's better in 1.0.
427        accessible_ids = set(
428            entry.id for entry
429            in not_owned.filter(host__acl_group__users__login=user.login))
430        public_ids = set(entry.id for entry
431                         in not_owned.filter(host__acl_group__name='Everyone'))
432        cannot_abort = [entry for entry in not_owned.select_related()
433                        if entry.id not in accessible_ids
434                        or entry.id in public_ids]
435        if len(cannot_abort) == 0:
436            return
437        entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner,
438                                              entry.host_or_metahost_name())
439                                for entry in cannot_abort)
440        raise AclAccessViolation('You cannot abort the following job entries: '
441                                 + entry_names)
442
443
444    def check_for_acl_violation_acl_group(self):
445        user = thread_local.get_user()
446        if user.is_superuser():
447            return None
448        if not user in self.users.all():
449            raise AclAccessViolation("You do not have access to %s"
450                                     % self.name)
451
452    @staticmethod
453    def on_host_membership_change():
454        everyone = AclGroup.objects.get(name='Everyone')
455
456        # find hosts that aren't in any ACL group and add them to Everyone
457        # TODO(showard): this is a bit of a hack, since the fact that this query
458        # works is kind of a coincidence of Django internals.  This trick
459        # doesn't work in general (on all foreign key relationships).  I'll
460        # replace it with a better technique when the need arises.
461        orphaned_hosts = Host.valid_objects.filter(acl_group__id__isnull=True)
462        everyone.hosts.add(*orphaned_hosts.distinct())
463
464        # find hosts in both Everyone and another ACL group, and remove them
465        # from Everyone
466        hosts_in_everyone = Host.valid_objects.filter_custom_join(
467            '_everyone', acl_group__name='Everyone')
468        acled_hosts = hosts_in_everyone.exclude(acl_group__name='Everyone')
469        everyone.hosts.remove(*acled_hosts.distinct())
470
471
472    def delete(self):
473        if (self.name == 'Everyone'):
474            raise AclAccessViolation("You cannot delete 'Everyone'!")
475        self.check_for_acl_violation_acl_group()
476        super(AclGroup, self).delete()
477        self.on_host_membership_change()
478
479
480    def add_current_user_if_empty(self):
481        if not self.users.count():
482            self.users.add(thread_local.get_user())
483
484
485    # if you have a model attribute called "Manipulator", Django will
486    # automatically insert it into the beginning of the superclass list
487    # for the model's manipulators
488    class Manipulator(object):
489        """
490        Custom manipulator to get notification when ACLs are changed through
491        the admin interface.
492        """
493        def save(self, new_data):
494            user = thread_local.get_user()
495            if hasattr(self, 'original_object'):
496                if (not user.is_superuser()
497                    and self.original_object.name == 'Everyone'):
498                    raise AclAccessViolation("You cannot modify 'Everyone'!")
499                self.original_object.check_for_acl_violation_acl_group()
500            obj = super(AclGroup.Manipulator, self).save(new_data)
501            if not hasattr(self, 'original_object'):
502                obj.users.add(thread_local.get_user())
503            obj.add_current_user_if_empty()
504            obj.on_host_membership_change()
505            return obj
506
507    class Meta:
508        db_table = 'acl_groups'
509
510    class Admin:
511        list_display = ('name', 'description')
512        search_fields = ('name',)
513
514    def __str__(self):
515        return self.name
516
517# hack to make the column name in the many-to-many DB tables match the one
518# generated by ruby
519AclGroup._meta.object_name = 'acl_group'
520
521
522class JobManager(model_logic.ExtendedManager):
523    'Custom manager to provide efficient status counts querying.'
524    def get_status_counts(self, job_ids):
525        """\
526        Returns a dictionary mapping the given job IDs to their status
527        count dictionaries.
528        """
529        if not job_ids:
530            return {}
531        id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids)
532        cursor = connection.cursor()
533        cursor.execute("""
534            SELECT job_id, status, COUNT(*)
535            FROM host_queue_entries
536            WHERE job_id IN %s
537            GROUP BY job_id, status
538            """ % id_list)
539        all_job_counts = {}
540        for job_id in job_ids:
541            all_job_counts[job_id] = {}
542        for job_id, status, count in cursor.fetchall():
543            all_job_counts[job_id][status] = count
544        return all_job_counts
545
546
547    def populate_dependencies(self, jobs):
548        if not jobs:
549            return
550        job_ids = ','.join(str(job['id']) for job in jobs)
551        cursor = connection.cursor()
552        cursor.execute("""
553            SELECT jobs.id, labels.name
554            FROM jobs
555            INNER JOIN jobs_dependency_labels
556              ON jobs.id = jobs_dependency_labels.job_id
557            INNER JOIN labels ON jobs_dependency_labels.label_id = labels.id
558            WHERE jobs.id IN (%s)
559            """ % job_ids)
560        job_dependencies = {}
561        for job_id, dependency in cursor.fetchall():
562            job_dependencies.setdefault(job_id, []).append(dependency)
563        for job in jobs:
564            dependencies = ','.join(job_dependencies.get(job['id'], []))
565            job['dependencies'] = dependencies
566
567
568class Job(dbmodels.Model, model_logic.ModelExtensions):
569    """\
570    owner: username of job owner
571    name: job name (does not have to be unique)
572    priority: Low, Medium, High, Urgent (or 0-3)
573    control_file: contents of control file
574    control_type: Client or Server
575    created_on: date of job creation
576    submitted_on: date of job submission
577    synch_count: how many hosts should be used per autoserv execution
578    run_verify: Whether or not to run the verify phase
579    timeout: hours until job times out
580    email_list: list of people to email on completion delimited by any of:
581                white space, ',', ':', ';'
582    dependency_labels: many-to-many relationship with labels corresponding to
583                       job dependencies
584    reboot_before: Never, If dirty, or Always
585    reboot_after: Never, If all tests passed, or Always
586    """
587    DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
588        'AUTOTEST_WEB', 'job_timeout_default', default=240)
589
590    Priority = enum.Enum('Low', 'Medium', 'High', 'Urgent')
591    ControlType = enum.Enum('Server', 'Client', start_value=1)
592
593    owner = dbmodels.CharField(maxlength=255)
594    name = dbmodels.CharField(maxlength=255)
595    priority = dbmodels.SmallIntegerField(choices=Priority.choices(),
596                                          blank=True, # to allow 0
597                                          default=Priority.MEDIUM)
598    control_file = dbmodels.TextField()
599    control_type = dbmodels.SmallIntegerField(choices=ControlType.choices(),
600                                              blank=True, # to allow 0
601                                              default=ControlType.CLIENT)
602    created_on = dbmodels.DateTimeField()
603    synch_count = dbmodels.IntegerField(null=True, default=1)
604    timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT)
605    run_verify = dbmodels.BooleanField(default=True)
606    email_list = dbmodels.CharField(maxlength=250, blank=True)
607    dependency_labels = dbmodels.ManyToManyField(
608        Label, blank=True, filter_interface=dbmodels.HORIZONTAL)
609    reboot_before = dbmodels.SmallIntegerField(choices=RebootBefore.choices(),
610                                               blank=True,
611                                               default=DEFAULT_REBOOT_BEFORE)
612    reboot_after = dbmodels.SmallIntegerField(choices=RebootAfter.choices(),
613                                              blank=True,
614                                              default=DEFAULT_REBOOT_AFTER)
615
616
617    # custom manager
618    objects = JobManager()
619
620
621    def is_server_job(self):
622        return self.control_type == self.ControlType.SERVER
623
624
625    @classmethod
626    def create(cls, owner, name, priority, control_file, control_type,
627               hosts, synch_count, timeout, run_verify, email_list,
628               dependencies, reboot_before, reboot_after):
629        """\
630        Creates a job by taking some information (the listed args)
631        and filling in the rest of the necessary information.
632        """
633        AclGroup.check_for_acl_violation_hosts(hosts)
634        job = cls.add_object(
635            owner=owner, name=name, priority=priority,
636            control_file=control_file, control_type=control_type,
637            synch_count=synch_count, timeout=timeout,
638            run_verify=run_verify, email_list=email_list,
639            reboot_before=reboot_before, reboot_after=reboot_after,
640            created_on=datetime.now())
641
642        if job.synch_count > len(hosts):
643            raise model_logic.ValidationError(
644                {'hosts': 'only %d hosts provided for job with synch_count = %d'
645                          % (len(hosts), job.synch_count)})
646        job.save()
647        job.dependency_labels = dependencies
648        return job
649
650
651    def queue(self, hosts):
652        'Enqueue a job on the given hosts.'
653        for host in hosts:
654            host.enqueue_job(self)
655
656
657    def user(self):
658        try:
659            return User.objects.get(login=self.owner)
660        except self.DoesNotExist:
661            return None
662
663
664    def abort(self, aborted_by):
665        for queue_entry in self.hostqueueentry_set.all():
666            queue_entry.abort(aborted_by)
667
668
669    class Meta:
670        db_table = 'jobs'
671
672    if settings.FULL_ADMIN:
673        class Admin:
674            list_display = ('id', 'owner', 'name', 'control_type')
675
676    def __str__(self):
677        return '%s (%s-%s)' % (self.name, self.id, self.owner)
678
679
680class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions):
681    job = dbmodels.ForeignKey(Job)
682    host = dbmodels.ForeignKey(Host)
683
684    objects = model_logic.ExtendedManager()
685
686    class Meta:
687        db_table = 'ineligible_host_queues'
688
689    if settings.FULL_ADMIN:
690        class Admin:
691            list_display = ('id', 'job', 'host')
692
693
694class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
695    Status = enum.Enum('Queued', 'Starting', 'Verifying', 'Pending', 'Running',
696                       'Parsing', 'Abort', 'Aborting', 'Aborted', 'Completed',
697                       'Failed', 'Stopped', string_values=True)
698    ABORT_STATUSES = (Status.ABORT, Status.ABORTING, Status.ABORTED)
699    ACTIVE_STATUSES = (Status.STARTING, Status.VERIFYING, Status.PENDING,
700                       Status.RUNNING, Status.ABORTING)
701    COMPLETE_STATUSES = (Status.ABORTED, Status.COMPLETED, Status.FAILED,
702                         Status.STOPPED)
703
704    job = dbmodels.ForeignKey(Job)
705    host = dbmodels.ForeignKey(Host, blank=True, null=True)
706    priority = dbmodels.SmallIntegerField()
707    status = dbmodels.CharField(maxlength=255)
708    meta_host = dbmodels.ForeignKey(Label, blank=True, null=True,
709                                    db_column='meta_host')
710    active = dbmodels.BooleanField(default=False)
711    complete = dbmodels.BooleanField(default=False)
712    deleted = dbmodels.BooleanField(default=False)
713    execution_subdir = dbmodels.CharField(maxlength=255, blank=True, default='')
714
715    objects = model_logic.ExtendedManager()
716
717
718    def __init__(self, *args, **kwargs):
719        super(HostQueueEntry, self).__init__(*args, **kwargs)
720        self._record_attributes(['status'])
721
722
723    def save(self):
724        self._set_active_and_complete()
725        super(HostQueueEntry, self).save()
726        self._check_for_updated_attributes()
727
728
729    def host_or_metahost_name(self):
730        if self.host:
731            return self.host.hostname
732        else:
733            assert self.meta_host
734            return self.meta_host.name
735
736
737    def _set_active_and_complete(self):
738        if self.status == self.Status.ABORT:
739            # must leave active flag unchanged so scheduler knows if entry was
740            # active before abort.
741            return
742        elif self.status in self.ACTIVE_STATUSES:
743            self.active, self.complete = True, False
744        elif self.status in self.COMPLETE_STATUSES:
745            self.active, self.complete = False, True
746        else:
747            self.active, self.complete = False, False
748
749
750    def on_attribute_changed(self, attribute, old_value):
751        assert attribute == 'status'
752        logger.info('%s/%d (%d) -> %s' % (self.host, self.job.id, self.id,
753                                           self.status))
754
755
756    def is_meta_host_entry(self):
757        'True if this is a entry has a meta_host instead of a host.'
758        return self.host is None and self.meta_host is not None
759
760
761    def log_abort(self, user):
762        if user is None:
763            # automatic system abort (i.e. job timeout)
764            return
765        abort_log = AbortedHostQueueEntry(queue_entry=self, aborted_by=user)
766        abort_log.save()
767
768
769    def abort(self, user):
770        # this isn't completely immune to race conditions since it's not atomic,
771        # but it should be safe given the scheduler's behavior.
772        if not self.complete and self.status not in self.ABORT_STATUSES:
773            self.status = HostQueueEntry.Status.ABORT
774            self.log_abort(user)
775            self.save()
776
777    class Meta:
778        db_table = 'host_queue_entries'
779
780    if settings.FULL_ADMIN:
781        class Admin:
782            list_display = ('id', 'job', 'host', 'status',
783                            'meta_host')
784
785
786class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions):
787    queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True)
788    aborted_by = dbmodels.ForeignKey(User)
789    aborted_on = dbmodels.DateTimeField()
790
791    objects = model_logic.ExtendedManager()
792
793
794    def save(self):
795        self.aborted_on = datetime.now()
796        super(AbortedHostQueueEntry, self).save()
797
798    class Meta:
799        db_table = 'aborted_host_queue_entries'
800