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