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