Example #1
0
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)
Example #2
0
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"]
Example #4
0
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
Example #5
0
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,
        )
Example #6
0
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,
        )
Example #7
0
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]