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