class Tag(TagBase, BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel): color = ColorField(default=ColorChoices.COLOR_GREY) description = models.CharField( max_length=200, blank=True, ) csv_headers = ["name", "slug", "color", "description"] class Meta: ordering = ["name"] def get_absolute_url(self): return reverse("extras:tag", args=[self.slug]) def slugify(self, tag, i=None): # Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names) slug = slugify(tag, allow_unicode=True) if i is not None: slug += "_%d" % i return slug def to_csv(self): return (self.name, self.slug, self.color, self.description)
class RackRole(OrganizationalModel): """ Racks can be organized by functional role, similar to Devices. """ name = models.CharField(max_length=100, unique=True) slug = models.SlugField(max_length=100, unique=True) color = ColorField(default=ColorChoices.COLOR_GREY) description = models.CharField( max_length=200, blank=True, ) csv_headers = ["name", "slug", "color", "description"] class Meta: ordering = ["name"] def __str__(self): return self.name def get_absolute_url(self): return reverse("dcim:rackrole", args=[self.pk]) def to_csv(self): return ( self.name, self.slug, self.color, self.description, )
class CommandLog(BaseModel): """Record of a single fully-executed Nautobot command. Incomplete commands (those requiring additional user input) should not be recorded, nor should any "help" commands or invalid command entries. """ start_time = models.DateTimeField(null=True) runtime = models.DurationField(null=True) user_name = models.CharField(max_length=255, help_text="Invoking username") user_id = models.CharField(max_length=255, help_text="Invoking user ID") platform = models.CharField(max_length=64, help_text="Chat platform") platform_color = ColorField() command = models.CharField(max_length=64, help_text="Command issued") subcommand = models.CharField(max_length=64, help_text="Sub-command issued") params = ArrayField(ArrayField(models.CharField(default="", max_length=255)), default=list, help_text="user_input_parameters") status = models.CharField( max_length=32, choices=CommandStatusChoices, default=CommandStatusChoices.STATUS_SUCCEEDED, ) details = models.CharField(max_length=255, default="") @property def status_label_class(self): """Bootstrap CSS label class for each status value.""" if self.status == CommandStatusChoices.STATUS_SUCCEEDED: return "success" elif self.status == CommandStatusChoices.STATUS_BLOCKED: return "default" elif self.status == CommandStatusChoices.STATUS_FAILED: return "warning" else: # STATUS_ERRORED, STATUS_UNKNOWN return "danger" def __str__(self): """String representation of a CommandLog entry.""" return f"{self.user_name} on {self.platform}: {self.command} {self.subcommand} {self.params} ({self.status})" class Meta: """Meta-attributes of a CommandLog.""" ordering = ["start_time"]
class Tag(TagBase, BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel): content_types = models.ManyToManyField( to=ContentType, related_name="tags", limit_choices_to=TaggableClassesQuery(), ) color = ColorField(default=ColorChoices.COLOR_GREY) description = models.CharField( max_length=200, blank=True, ) csv_headers = ["name", "slug", "color", "description"] objects = TagQuerySet.as_manager() class Meta: ordering = ["name"] def get_absolute_url(self): return reverse("extras:tag", args=[self.slug]) def slugify(self, tag, i=None): # Allow Unicode in Tag slugs (avoids empty slugs for Tags with all-Unicode names) slug = slugify(tag, allow_unicode=True) if i is not None: slug += "_%d" % i return slug def to_csv(self): return (self.name, self.slug, self.color, self.description) def validate_content_types_removal(self, content_types_id): """Validate content_types to be removed are not tagged to a model""" errors = {} removed_content_types = self.content_types.exclude(id__in=content_types_id) # check if tag is assigned to any of the removed content_types for content_type in removed_content_types: model = content_type.model_class() if model.objects.filter(tags=self).exists(): errors.setdefault("content_types", []).append( f"Unable to remove {model._meta.label_lower}. Dependent objects were found." ) return errors
class Status(BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel): """Model for database-backend enum choice objects.""" content_types = models.ManyToManyField( to=ContentType, related_name="statuses", verbose_name="Content type(s)", limit_choices_to=FeatureQuery("statuses"), help_text="The content type(s) to which this status applies.", ) name = models.CharField(max_length=50, unique=True) color = ColorField(default=ColorChoices.COLOR_GREY) slug = models.SlugField(max_length=50, unique=True) description = models.CharField( max_length=200, blank=True, ) objects = StatusQuerySet.as_manager() csv_headers = ["name", "slug", "color", "content_types", "description"] clone_fields = ["color", "content_types"] class Meta: ordering = ["name"] verbose_name_plural = "statuses" def __str__(self): return self.name def get_absolute_url(self): return reverse("extras:status", args=[self.slug]) def to_csv(self): labels = ",".join(f"{ct.app_label}.{ct.model}" for ct in self.content_types.all()) return ( self.name, self.slug, self.color, f'"{labels}"', # Wrap labels in double quotes for CSV self.description, )
class DeviceRole(OrganizationalModel): """ Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a color to be used when displaying rack elevations. The vm_role field determines whether the role is applicable to virtual machines as well. """ name = models.CharField(max_length=100, unique=True) slug = AutoSlugField(populate_from="name") color = ColorField(default=ColorChoices.COLOR_GREY) vm_role = models.BooleanField( default=True, verbose_name="VM Role", help_text="Virtual machines may be assigned to this role", ) description = models.CharField( max_length=200, blank=True, ) csv_headers = ["name", "slug", "color", "vm_role", "description"] class Meta: ordering = ["name"] def get_absolute_url(self): return reverse("dcim:devicerole", args=[self.slug]) def __str__(self): return self.name def to_csv(self): return ( self.name, self.slug, self.color, self.vm_role, self.description, )
class Cable(PrimaryModel, StatusModel): """ A physical connection between two endpoints. """ termination_a_type = models.ForeignKey( to=ContentType, limit_choices_to=CABLE_TERMINATION_MODELS, on_delete=models.PROTECT, related_name="+", ) termination_a_id = models.UUIDField() termination_a = GenericForeignKey(ct_field="termination_a_type", fk_field="termination_a_id") termination_b_type = models.ForeignKey( to=ContentType, limit_choices_to=CABLE_TERMINATION_MODELS, on_delete=models.PROTECT, related_name="+", ) termination_b_id = models.UUIDField() termination_b = GenericForeignKey(ct_field="termination_b_type", fk_field="termination_b_id") type = models.CharField(max_length=50, choices=CableTypeChoices, blank=True) label = models.CharField(max_length=100, blank=True) color = ColorField(blank=True) length = models.PositiveSmallIntegerField(blank=True, null=True) length_unit = models.CharField( max_length=50, choices=CableLengthUnitChoices, blank=True, ) # Stores the normalized length (in meters) for database ordering _abs_length = models.DecimalField(max_digits=10, decimal_places=4, blank=True, null=True) # Cache the associated device (where applicable) for the A and B terminations. This enables filtering of Cables by # their associated Devices. _termination_a_device = models.ForeignKey(to=Device, on_delete=models.CASCADE, related_name="+", blank=True, null=True) _termination_b_device = models.ForeignKey(to=Device, on_delete=models.CASCADE, related_name="+", blank=True, null=True) csv_headers = [ "termination_a_type", "termination_a_id", "termination_b_type", "termination_b_id", "type", "status", "label", "color", "length", "length_unit", ] class Meta: ordering = [ "termination_a_type", "termination_a_id", "termination_b_type", "termination_b_id", ] unique_together = ( ("termination_a_type", "termination_a_id"), ("termination_b_type", "termination_b_id"), ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # A copy of the PK to be used by __str__ in case the object is deleted self._pk = self.pk # Cache the original status so we can check later if it's been changed self._orig_status = self.status @classmethod def from_db(cls, db, field_names, values): """ Cache the original A and B terminations of existing Cable instances for later reference inside clean(). """ instance = super().from_db(db, field_names, values) instance._orig_termination_a_type_id = instance.termination_a_type_id instance._orig_termination_a_id = instance.termination_a_id instance._orig_termination_b_type_id = instance.termination_b_type_id instance._orig_termination_b_id = instance.termination_b_id return instance def __str__(self): pk = self.pk or self._pk return self.label or f"#{pk}" def get_absolute_url(self): return reverse("dcim:cable", args=[self.pk]) @classproperty def STATUS_CONNECTED(cls): """Return a cached "connected" `Status` object for later reference.""" if getattr(cls, "__status_connected", None) is None: cls.__status_connected = Status.objects.get_for_model(Cable).get( slug="connected") return cls.__status_connected def clean(self): super().clean() # Validate that termination A exists if not hasattr(self, "termination_a_type"): raise ValidationError("Termination A type has not been specified") try: self.termination_a_type.model_class().objects.get( pk=self.termination_a_id) except ObjectDoesNotExist: raise ValidationError({ "termination_a": "Invalid ID for type {}".format(self.termination_a_type) }) # Validate that termination B exists if not hasattr(self, "termination_b_type"): raise ValidationError("Termination B type has not been specified") try: self.termination_b_type.model_class().objects.get( pk=self.termination_b_id) except ObjectDoesNotExist: raise ValidationError({ "termination_b": "Invalid ID for type {}".format(self.termination_b_type) }) # If editing an existing Cable instance, check that neither termination has been modified. if not self._state.adding: err_msg = "Cable termination points may not be modified. Delete and recreate the cable instead." if (self.termination_a_type_id != self._orig_termination_a_type_id or self.termination_a_id != self._orig_termination_a_id): raise ValidationError({"termination_a": err_msg}) if (self.termination_b_type_id != self._orig_termination_b_type_id or self.termination_b_id != self._orig_termination_b_id): raise ValidationError({"termination_b": err_msg}) type_a = self.termination_a_type.model type_b = self.termination_b_type.model # Validate interface types if type_a == "interface" and self.termination_a.type in NONCONNECTABLE_IFACE_TYPES: raise ValidationError({ "termination_a_id": "Cables cannot be terminated to {} interfaces".format( self.termination_a.get_type_display()) }) if type_b == "interface" and self.termination_b.type in NONCONNECTABLE_IFACE_TYPES: raise ValidationError({ "termination_b_id": "Cables cannot be terminated to {} interfaces".format( self.termination_b.get_type_display()) }) # Check that termination types are compatible if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a): raise ValidationError( f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}" ) # Check that two connected RearPorts have the same number of positions (if both are >1) if isinstance(self.termination_a, RearPort) and isinstance( self.termination_b, RearPort): if self.termination_a.positions > 1 and self.termination_b.positions > 1: if self.termination_a.positions != self.termination_b.positions: raise ValidationError( f"{self.termination_a} has {self.termination_a.positions} position(s) but " f"{self.termination_b} has {self.termination_b.positions}. " f"Both terminations must have the same number of positions (if greater than one)." ) # A termination point cannot be connected to itself if self.termination_a == self.termination_b: raise ValidationError( f"Cannot connect {self.termination_a_type} to itself") # A front port cannot be connected to its corresponding rear port if (type_a in ["frontport", "rearport"] and type_b in ["frontport", "rearport"] and (getattr(self.termination_a, "rear_port", None) == self.termination_b or getattr(self.termination_b, "rear_port", None) == self.termination_a)): raise ValidationError( "A front port cannot be connected to it corresponding rear port" ) # Check for an existing Cable connected to either termination object if self.termination_a.cable not in (None, self): raise ValidationError( "{} already has a cable attached (#{})".format( self.termination_a, self.termination_a.cable_id)) if self.termination_b.cable not in (None, self): raise ValidationError( "{} already has a cable attached (#{})".format( self.termination_b, self.termination_b.cable_id)) # Validate length and length_unit if self.length is not None and not self.length_unit: raise ValidationError( "Must specify a unit when setting a cable length") elif self.length is None: self.length_unit = "" def save(self, *args, **kwargs): # Store the given length (if any) in meters for use in database ordering if self.length and self.length_unit: self._abs_length = to_meters(self.length, self.length_unit) else: self._abs_length = None # Store the parent Device for the A and B terminations (if applicable) to enable filtering if hasattr(self.termination_a, "device"): self._termination_a_device = self.termination_a.device if hasattr(self.termination_b, "device"): self._termination_b_device = self.termination_b.device super().save(*args, **kwargs) # Update the private pk used in __str__ in case this is a new object (i.e. just got its pk) self._pk = self.pk def to_csv(self): return ( "{}.{}".format(self.termination_a_type.app_label, self.termination_a_type.model), self.termination_a_id, "{}.{}".format(self.termination_b_type.app_label, self.termination_b_type.model), self.termination_b_id, self.get_type_display(), self.get_status_display(), self.label, self.color, self.length, self.length_unit, ) def get_compatible_types(self): """ Return all termination types compatible with termination A. """ if self.termination_a is None: return return COMPATIBLE_TERMINATION_TYPES[ self.termination_a._meta.model_name]