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