class AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model): """\ An atomic group defines a collection of hosts which must only be scheduled all at once. Any host with a label having an atomic group will only be scheduled for a job at the same time as other hosts sharing that label. Required: name: A name for this atomic group. ex: 'rack23' or 'funky_net' max_number_of_machines: The maximum number of machines that will be scheduled at once when scheduling jobs to this atomic group. The job.synch_count is considered the minimum. Optional: description: Arbitrary text description of this group's purpose. """ name = dbmodels.CharField(max_length=255, unique=True) description = dbmodels.TextField(blank=True) # This magic value is the default to simplify the scheduler logic. # It must be "large". The common use of atomic groups is to want all # machines in the group to be used, limits on which subset used are # often chosen via dependency labels. INFINITE_MACHINES = 333333333 max_number_of_machines = dbmodels.IntegerField(default=INFINITE_MACHINES) invalid = dbmodels.BooleanField(default=False, editable=settings.FULL_ADMIN) name_field = 'name' objects = model_logic.ModelWithInvalidManager() valid_objects = model_logic.ValidObjectsManager() def enqueue_job(self, job, is_template=False): """Enqueue a job on an associated atomic group of hosts.""" queue_entry = HostQueueEntry.create(atomic_group=self, job=job, is_template=is_template) queue_entry.save() def clean_object(self): self.label_set.clear() class Meta: db_table = 'afe_atomic_groups' def __unicode__(self): return unicode(self.name)
class Label(model_logic.ModelWithInvalid, dbmodels.Model): """\ Required: name: label name Optional: kernel_config: URL/path to kernel config for jobs run on this label. platform: If True, this is a platform label (defaults to False). only_if_needed: If True, a Host with this label can only be used if that label is requested by the job/test (either as the meta_host or in the job_dependencies). atomic_group: The atomic group associated with this label. """ name = dbmodels.CharField(max_length=255, unique=True) kernel_config = dbmodels.CharField(max_length=255, blank=True) platform = dbmodels.BooleanField(default=False) invalid = dbmodels.BooleanField(default=False, editable=settings.FULL_ADMIN) only_if_needed = dbmodels.BooleanField(default=False) name_field = 'name' objects = model_logic.ModelWithInvalidManager() valid_objects = model_logic.ValidObjectsManager() atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True) def clean_object(self): self.host_set.clear() self.test_set.clear() def enqueue_job(self, job, atomic_group=None, is_template=False): """Enqueue a job on any host of this label.""" queue_entry = HostQueueEntry.create(meta_host=self, job=job, is_template=is_template, atomic_group=atomic_group) queue_entry.save() class Meta: db_table = 'afe_labels' def __unicode__(self): return unicode(self.name)
class Host(model_logic.ModelWithInvalid, dbmodels.Model, model_logic.ModelWithAttributes): """\ Required: hostname optional: locked: if true, host is locked and will not be queued Internal: synch_id: currently unused status: string describing status of host invalid: true if the host has been deleted protection: indicates what can be done to this host during repair locked_by: user that locked the host, or null if the host is unlocked lock_time: DateTime at which the host was locked dirty: true if the host has been used without being rebooted """ Status = enum.Enum('Verifying', 'Running', 'Ready', 'Repairing', 'Repair Failed', 'Cleaning', 'Pending', string_values=True) Protection = host_protections.Protection hostname = dbmodels.CharField(max_length=255, unique=True) labels = dbmodels.ManyToManyField(Label, blank=True, db_table='afe_hosts_labels') locked = dbmodels.BooleanField(default=False) synch_id = dbmodels.IntegerField(blank=True, null=True, editable=settings.FULL_ADMIN) status = dbmodels.CharField(max_length=255, default=Status.READY, choices=Status.choices(), editable=settings.FULL_ADMIN) invalid = dbmodels.BooleanField(default=False, editable=settings.FULL_ADMIN) protection = dbmodels.SmallIntegerField(null=False, blank=True, choices=host_protections.choices, default=host_protections.default) locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False) lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False) dirty = dbmodels.BooleanField(default=True, editable=settings.FULL_ADMIN) name_field = 'hostname' objects = model_logic.ModelWithInvalidManager() valid_objects = model_logic.ValidObjectsManager() def __init__(self, *args, **kwargs): super(Host, self).__init__(*args, **kwargs) self._record_attributes(['status']) @staticmethod def create_one_time_host(hostname): query = Host.objects.filter(hostname=hostname) if query.count() == 0: host = Host(hostname=hostname, invalid=True) host.do_validate() else: host = query[0] if not host.invalid: raise model_logic.ValidationError({ 'hostname' : '%s already exists in the autotest DB. ' 'Select it rather than entering it as a one time ' 'host.' % hostname }) host.protection = host_protections.Protection.DO_NOT_REPAIR host.locked = False host.save() host.clean_object() return host def resurrect_object(self, old_object): super(Host, self).resurrect_object(old_object) # invalid hosts can be in use by the scheduler (as one-time hosts), so # don't change the status self.status = old_object.status def clean_object(self): self.aclgroup_set.clear() self.labels.clear() def save(self, *args, **kwargs): # extra spaces in the hostname can be a sneaky source of errors self.hostname = self.hostname.strip() # is this a new object being saved for the first time? first_time = (self.id is None) if not first_time: AclGroup.check_for_acl_violation_hosts([self]) if self.locked and not self.locked_by: self.locked_by = User.current_user() self.lock_time = datetime.now() self.dirty = True elif not self.locked and self.locked_by: self.locked_by = None self.lock_time = None super(Host, self).save(*args, **kwargs) if first_time: everyone = AclGroup.objects.get(name='Everyone') everyone.hosts.add(self) self._check_for_updated_attributes() def delete(self): AclGroup.check_for_acl_violation_hosts([self]) for queue_entry in self.hostqueueentry_set.all(): queue_entry.deleted = True queue_entry.abort() super(Host, self).delete() def on_attribute_changed(self, attribute, old_value): assert attribute == 'status' logging.info(self.hostname + ' -> ' + self.status) def enqueue_job(self, job, atomic_group=None, is_template=False): """Enqueue a job on this host.""" queue_entry = HostQueueEntry.create(host=self, job=job, is_template=is_template, atomic_group=atomic_group) # allow recovery of dead hosts from the frontend if not self.active_queue_entry() and self.is_dead(): self.status = Host.Status.READY self.save() queue_entry.save() block = IneligibleHostQueue(job=job, host=self) block.save() def platform(self): # TODO(showard): slighly hacky? platforms = self.labels.filter(platform=True) if len(platforms) == 0: return None return platforms[0] platform.short_description = 'Platform' @classmethod def check_no_platform(cls, hosts): Host.objects.populate_relationships(hosts, Label, 'label_list') errors = [] for host in hosts: platforms = [label.name for label in host.label_list if label.platform] if platforms: # do a join, just in case this host has multiple platforms, # we'll be able to see it errors.append('Host %s already has a platform: %s' % ( host.hostname, ', '.join(platforms))) if errors: raise model_logic.ValidationError({'labels': '; '.join(errors)}) def is_dead(self): return self.status == Host.Status.REPAIR_FAILED def active_queue_entry(self): active = list(self.hostqueueentry_set.filter(active=True)) if not active: return None assert len(active) == 1, ('More than one active entry for ' 'host ' + self.hostname) return active[0] def _get_attribute_model_and_args(self, attribute): return HostAttribute, dict(host=self, attribute=attribute) class Meta: db_table = 'afe_hosts' def __unicode__(self): return unicode(self.hostname)