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})" })
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, )
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])
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])
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 )
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)
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])
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
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" })
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, )
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
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)
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]