Пример #1
0
class RearPort(ComponentModel, CableTermination):
    """
    A pass-through port on the rear of a Device.
    """
    type = models.CharField(max_length=50, choices=PortTypeChoices)
    color = ColorField(blank=True)
    positions = models.PositiveSmallIntegerField(
        default=1,
        validators=[
            MinValueValidator(REARPORT_POSITIONS_MIN),
            MaxValueValidator(REARPORT_POSITIONS_MAX)
        ])
    clone_fields = ['device', 'type', 'positions']

    class Meta:
        ordering = ('device', '_name')
        unique_together = ('device', 'name')

    def get_absolute_url(self):
        return reverse('dcim:rearport', kwargs={'pk': self.pk})

    def clean(self):
        super().clean()

        # Check that positions count is greater than or equal to the number of associated FrontPorts
        frontport_count = self.frontports.count()
        if self.positions < frontport_count:
            raise ValidationError({
                "positions":
                f"The number of positions cannot be less than the number of mapped front ports "
                f"({frontport_count})"
            })
Пример #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,
    )

    objects = RestrictedQuerySet.as_manager()

    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,
        )
Пример #3
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 = models.SlugField(max_length=100, unique=True)
    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,
    )

    objects = RestrictedQuerySet.as_manager()

    class Meta:
        ordering = ['name']

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('dcim:devicerole', args=[self.pk])
Пример #4
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,
    )

    class Meta:
        ordering = ['name']

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('dcim:rackrole', args=[self.pk])
Пример #5
0
class Tag(ChangeLoggedModel, TagBase):
    color = ColorField(
        default=ColorChoices.COLOR_GREY
    )
    description = models.CharField(
        max_length=200,
        blank=True,
    )

    objects = RestrictedQuerySet.as_manager()

    csv_headers = ['name', 'slug', 'color', 'description']

    class Meta:
        ordering = ['name']

    def get_absolute_url(self):
        return reverse('extras:tag', args=[self.pk])

    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
        )
Пример #6
0
class FrontPortTemplate(ComponentTemplateModel):
    """
    Template for a pass-through port on the front of a new Device.
    """
    type = models.CharField(max_length=50, choices=PortTypeChoices)
    color = ColorField(blank=True)
    rear_port = models.ForeignKey(to='dcim.RearPortTemplate',
                                  on_delete=models.CASCADE,
                                  related_name='frontport_templates')
    rear_port_position = models.PositiveSmallIntegerField(
        default=1,
        validators=[
            MinValueValidator(REARPORT_POSITIONS_MIN),
            MaxValueValidator(REARPORT_POSITIONS_MAX)
        ])

    class Meta:
        ordering = ('device_type', '_name')
        unique_together = (
            ('device_type', 'name'),
            ('rear_port', 'rear_port_position'),
        )

    def clean(self):
        super().clean()

        try:

            # Validate rear port assignment
            if self.rear_port.device_type != self.device_type:
                raise ValidationError(
                    "Rear port ({}) must belong to the same device type".
                    format(self.rear_port))

            # Validate rear port position assignment
            if self.rear_port_position > self.rear_port.positions:
                raise ValidationError(
                    "Invalid rear port position ({}); rear port {} has only {} positions"
                    .format(self.rear_port_position, self.rear_port.name,
                            self.rear_port.positions))

        except RearPortTemplate.DoesNotExist:
            pass

    def instantiate(self, device):
        if self.rear_port:
            rear_port = RearPort.objects.get(device=device,
                                             name=self.rear_port.name)
        else:
            rear_port = None
        return FrontPort(device=device,
                         name=self.name,
                         label=self.label,
                         type=self.type,
                         color=self.color,
                         rear_port=rear_port,
                         rear_port_position=self.rear_port_position)
Пример #7
0
class Tag(TagBase, ChangeLoggedModel):
    color = ColorField(
        default='9e9e9e'
    )
    comments = models.TextField(
        blank=True,
        default=''
    )

    def get_absolute_url(self):
        return reverse('extras:tag', args=[self.slug])
Пример #8
0
class Tag(TagBase, ChangeLoggedModel):
    color = ColorField(default='9e9e9e')
    comments = models.TextField(blank=True, default='')

    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
Пример #9
0
class FrontPort(ComponentModel, LinkTermination):
    """
    A pass-through port on the front of a Device.
    """
    type = models.CharField(
        max_length=50,
        choices=PortTypeChoices
    )
    color = ColorField(
        blank=True
    )
    rear_port = models.ForeignKey(
        to='dcim.RearPort',
        on_delete=models.CASCADE,
        related_name='frontports'
    )
    rear_port_position = models.PositiveSmallIntegerField(
        default=1,
        validators=[
            MinValueValidator(REARPORT_POSITIONS_MIN),
            MaxValueValidator(REARPORT_POSITIONS_MAX)
        ]
    )

    clone_fields = ['device', 'type']

    class Meta:
        ordering = ('device', '_name')
        unique_together = (
            ('device', 'name'),
            ('rear_port', 'rear_port_position'),
        )

    def get_absolute_url(self):
        return reverse('dcim:frontport', kwargs={'pk': self.pk})

    def clean(self):
        super().clean()

        # Validate rear port assignment
        if self.rear_port.device != self.device:
            raise ValidationError({
                "rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
            })

        # Validate rear port position assignment
        if self.rear_port_position > self.rear_port.positions:
            raise ValidationError({
                "rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port "
                                      f"{self.rear_port.name} has only {self.rear_port.positions} positions"
            })
Пример #10
0
class DeviceRole(ChangeLoggedModel):
    """
    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 = models.SlugField(
        max_length=100,
        unique=True
    )
    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,
    )

    objects = RestrictedQuerySet.as_manager()

    csv_headers = ['name', 'slug', 'color', 'vm_role', 'description']

    class Meta:
        ordering = ['name']

    def __str__(self):
        return self.name

    def to_csv(self):
        return (
            self.name,
            self.slug,
            self.color,
            self.vm_role,
            self.description,
        )
Пример #11
0
class Tag(TagBase, ChangeLoggedModel):
    color = ColorField(
        default=ColorChoices.COLOR_GREY
    )
    description = models.CharField(
        max_length=200,
        blank=True,
    )

    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
Пример #12
0
class RearPortTemplate(ComponentTemplateModel):
    """
    Template for a pass-through port on the rear of a new Device.
    """
    type = models.CharField(max_length=50, choices=PortTypeChoices)
    color = ColorField(blank=True)
    positions = models.PositiveSmallIntegerField(
        default=1,
        validators=[
            MinValueValidator(REARPORT_POSITIONS_MIN),
            MaxValueValidator(REARPORT_POSITIONS_MAX)
        ])

    class Meta:
        ordering = ('device_type', '_name')
        unique_together = ('device_type', 'name')

    def instantiate(self, device):
        return RearPort(device=device,
                        name=self.name,
                        label=self.label,
                        type=self.type,
                        color=self.color,
                        positions=self.positions)
Пример #13
0
class Cable(PrimaryModel):
    """
    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.PositiveIntegerField()
    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.PositiveIntegerField()
    termination_b = GenericForeignKey(ct_field='termination_b_type',
                                      fk_field='termination_b_id')
    type = models.CharField(max_length=50,
                            choices=CableTypeChoices,
                            blank=True)
    status = models.CharField(max_length=50,
                              choices=CableStatusChoices,
                              default=CableStatusChoices.STATUS_CONNECTED)
    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)

    objects = RestrictedQuerySet.as_manager()

    csv_headers = [
        'termination_a_type',
        'termination_a_id',
        'termination_b_type',
        'termination_b_id',
        'type',
        'status',
        'label',
        'color',
        'length',
        'length_unit',
    ]

    class Meta:
        ordering = ['pk']
        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])

    def clean(self):
        from circuits.models import CircuitTermination

        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 self.pk:
            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"
            )

        # A CircuitTermination attached to a ProviderNetwork cannot have a Cable
        if isinstance(self.termination_a, CircuitTermination
                      ) and self.termination_a.provider_network is not None:
            raise ValidationError({
                'termination_a_id':
                "Circuit terminations attached to a provider network may not be cabled."
            })
        if isinstance(self.termination_b, CircuitTermination
                      ) and self.termination_b.provider_network is not None:
            raise ValidationError({
                'termination_b_id':
                "Circuit terminations attached to a provider network may not be cabled."
            })

        # 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_status_class(self):
        return CableStatusChoices.CSS_CLASSES.get(self.status)

    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]