models.py revision 97db5ba6cdee21d706dff3d1a85c354c8bfae6f9
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=Job.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.debug(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=Job.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.hostname) 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 Status = enum.Enum('Created', 'Queued', 'Pending', 'Running', 593 'Completed', 'Abort', 'Aborting', 'Aborted', 594 'Failed', 'Starting', string_values=True) 595 596 owner = dbmodels.CharField(maxlength=255) 597 name = dbmodels.CharField(maxlength=255) 598 priority = dbmodels.SmallIntegerField(choices=Priority.choices(), 599 blank=True, # to allow 0 600 default=Priority.MEDIUM) 601 control_file = dbmodels.TextField() 602 control_type = dbmodels.SmallIntegerField(choices=ControlType.choices(), 603 blank=True, # to allow 0 604 default=ControlType.CLIENT) 605 created_on = dbmodels.DateTimeField() 606 synch_count = dbmodels.IntegerField(null=True, default=1) 607 timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT) 608 run_verify = dbmodels.BooleanField(default=True) 609 email_list = dbmodels.CharField(maxlength=250, blank=True) 610 dependency_labels = dbmodels.ManyToManyField( 611 Label, blank=True, filter_interface=dbmodels.HORIZONTAL) 612 reboot_before = dbmodels.SmallIntegerField(choices=RebootBefore.choices(), 613 blank=True, 614 default=DEFAULT_REBOOT_BEFORE) 615 reboot_after = dbmodels.SmallIntegerField(choices=RebootAfter.choices(), 616 blank=True, 617 default=DEFAULT_REBOOT_AFTER) 618 619 620 # custom manager 621 objects = JobManager() 622 623 624 def is_server_job(self): 625 return self.control_type == self.ControlType.SERVER 626 627 628 @classmethod 629 def create(cls, owner, name, priority, control_file, control_type, 630 hosts, synch_count, timeout, run_verify, email_list, 631 dependencies, reboot_before, reboot_after): 632 """\ 633 Creates a job by taking some information (the listed args) 634 and filling in the rest of the necessary information. 635 """ 636 AclGroup.check_for_acl_violation_hosts(hosts) 637 job = cls.add_object( 638 owner=owner, name=name, priority=priority, 639 control_file=control_file, control_type=control_type, 640 synch_count=synch_count, timeout=timeout, 641 run_verify=run_verify, email_list=email_list, 642 reboot_before=reboot_before, reboot_after=reboot_after, 643 created_on=datetime.now()) 644 645 if job.synch_count > len(hosts): 646 raise model_logic.ValidationError( 647 {'hosts': 'only %d hosts provided for job with synch_count = %d' 648 % (len(hosts), job.synch_count)}) 649 job.save() 650 job.dependency_labels = dependencies 651 return job 652 653 654 def queue(self, hosts): 655 'Enqueue a job on the given hosts.' 656 for host in hosts: 657 host.enqueue_job(self) 658 659 660 def user(self): 661 try: 662 return User.objects.get(login=self.owner) 663 except self.DoesNotExist: 664 return None 665 666 667 def abort(self, aborted_by): 668 for queue_entry in self.hostqueueentry_set.all(): 669 queue_entry.abort(aborted_by) 670 671 672 class Meta: 673 db_table = 'jobs' 674 675 if settings.FULL_ADMIN: 676 class Admin: 677 list_display = ('id', 'owner', 'name', 'control_type') 678 679 def __str__(self): 680 return '%s (%s-%s)' % (self.name, self.id, self.owner) 681 682 683class IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions): 684 job = dbmodels.ForeignKey(Job) 685 host = dbmodels.ForeignKey(Host) 686 687 objects = model_logic.ExtendedManager() 688 689 class Meta: 690 db_table = 'ineligible_host_queues' 691 692 if settings.FULL_ADMIN: 693 class Admin: 694 list_display = ('id', 'job', 'host') 695 696 697class HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions): 698 Status = enum.Enum('Queued', 'Starting', 'Verifying', 'Pending', 'Running', 699 'Parsing', 'Abort', 'Aborting', 'Aborted', 'Completed', 700 'Failed', 'Stopped', string_values=True) 701 ABORT_STATUSES = (Status.ABORT, Status.ABORTING, Status.ABORTED) 702 ACTIVE_STATUSES = (Status.STARTING, Status.VERIFYING, Status.PENDING, 703 Status.RUNNING, Status.ABORT, Status.ABORTING) 704 COMPLETE_STATUSES = (Status.PARSING, Status.ABORTED, Status.COMPLETED, 705 Status.FAILED, Status.STOPPED) 706 707 job = dbmodels.ForeignKey(Job) 708 host = dbmodels.ForeignKey(Host, blank=True, null=True) 709 priority = dbmodels.SmallIntegerField() 710 status = dbmodels.CharField(maxlength=255) 711 meta_host = dbmodels.ForeignKey(Label, blank=True, null=True, 712 db_column='meta_host') 713 active = dbmodels.BooleanField(default=False) 714 complete = dbmodels.BooleanField(default=False) 715 deleted = dbmodels.BooleanField(default=False) 716 execution_subdir = dbmodels.CharField(maxlength=255, blank=True, default='') 717 718 objects = model_logic.ExtendedManager() 719 720 721 def __init__(self, *args, **kwargs): 722 super(HostQueueEntry, self).__init__(*args, **kwargs) 723 self._record_attributes(['status']) 724 725 726 def save(self): 727 self._set_active_and_complete() 728 super(HostQueueEntry, self).save() 729 self._check_for_updated_attributes() 730 731 732 def _set_active_and_complete(self): 733 if self.status in self.ACTIVE_STATUSES: 734 self.active, self.complete = True, False 735 elif self.status in self.COMPLETE_STATUSES: 736 self.active, self.complete = False, True 737 else: 738 self.active, self.complete = False, False 739 740 741 def on_attribute_changed(self, attribute, old_value): 742 assert attribute == 'status' 743 logger.debug('%s/%d (%d) -> %s' % (self.host, self.job.id, self.id, 744 self.status)) 745 746 747 def is_meta_host_entry(self): 748 'True if this is a entry has a meta_host instead of a host.' 749 return self.host is None and self.meta_host is not None 750 751 752 def log_abort(self, user): 753 if user is None: 754 # automatic system abort (i.e. job timeout) 755 return 756 abort_log = AbortedHostQueueEntry(queue_entry=self, aborted_by=user) 757 abort_log.save() 758 759 760 def abort(self, user): 761 # this isn't completely immune to race conditions since it's not atomic, 762 # but it should be safe given the scheduler's behavior. 763 if not self.complete and self.status not in self.ABORT_STATUSES: 764 self.status = Job.Status.ABORT 765 self.log_abort(user) 766 self.save() 767 768 class Meta: 769 db_table = 'host_queue_entries' 770 771 if settings.FULL_ADMIN: 772 class Admin: 773 list_display = ('id', 'job', 'host', 'status', 774 'meta_host') 775 776 777class AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions): 778 queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True) 779 aborted_by = dbmodels.ForeignKey(User) 780 aborted_on = dbmodels.DateTimeField() 781 782 objects = model_logic.ExtendedManager() 783 784 785 def save(self): 786 self.aborted_on = datetime.now() 787 super(AbortedHostQueueEntry, self).save() 788 789 class Meta: 790 db_table = 'aborted_host_queue_entries' 791