class InterfaceTemplate(ComponentTemplateModel): """ A template for a physical data interface on a new Device. """ # Override ComponentTemplateModel._name to specify naturalize_interface function _name = NaturalOrderingField( target_field="name", naturalize_function=naturalize_interface, max_length=100, blank=True, ) type = models.CharField(max_length=50, choices=InterfaceTypeChoices) mgmt_only = models.BooleanField(default=False, verbose_name="Management only") class Meta: ordering = ("device_type", "_name") unique_together = ("device_type", "name") def instantiate(self, device): return Interface( device=device, name=self.name, label=self.label, type=self.type, mgmt_only=self.mgmt_only, )
class ComponentTemplateModel(BaseModel, CustomFieldModel, RelationshipModel): device_type = models.ForeignKey(to="dcim.DeviceType", on_delete=models.CASCADE, related_name="%(class)ss") name = models.CharField(max_length=64) _name = NaturalOrderingField(target_field="name", max_length=100, blank=True) label = models.CharField(max_length=64, blank=True, help_text="Physical label") description = models.CharField(max_length=200, blank=True) class Meta: abstract = True def __str__(self): if self.label: return f"{self.name} ({self.label})" return self.name def instantiate(self, device): """ Instantiate a new component on the specified Device. """ raise NotImplementedError() def to_objectchange(self, action): # Annotate the parent DeviceType try: device_type = self.device_type except ObjectDoesNotExist: # The parent DeviceType has already been deleted device_type = None return ObjectChange( changed_object=self, object_repr=str(self), action=action, related_object=device_type, object_data=serialize_object(self), ) def instantiate_model(self, model, device, **kwargs): """ Helper method to self.instantiate(). """ return model(device=device, name=self.name, label=self.label, description=self.description, **kwargs)
class ComponentModel(BaseModel, CustomFieldModel, RelationshipModel): """ An abstract model inherited by any model which has a parent Device. """ device = models.ForeignKey(to="dcim.Device", on_delete=models.CASCADE, related_name="%(class)ss") name = models.CharField(max_length=64, db_index=True) _name = NaturalOrderingField(target_field="name", max_length=100, blank=True, db_index=True) label = models.CharField(max_length=64, blank=True, help_text="Physical label") description = models.CharField(max_length=200, blank=True) tags = TaggableManager(through=TaggedItem) class Meta: abstract = True def __str__(self): if self.label: return f"{self.name} ({self.label})" return self.name def to_objectchange(self, action): # Annotate the parent Device try: device = self.device except ObjectDoesNotExist: # The parent Device has already been deleted device = None return ObjectChange( changed_object=self, object_repr=str(self), action=action, object_data=serialize_object(self), object_data_v2=serialize_object_v2(self), related_object=device, ) @property def parent(self): return getattr(self, "device", None)
class Rack(PrimaryModel, StatusModel): """ Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face. Each Rack is assigned to a Site and (optionally) a RackGroup. """ name = models.CharField(max_length=100) _name = NaturalOrderingField(target_field="name", max_length=100, blank=True) facility_id = models.CharField( max_length=50, blank=True, null=True, verbose_name="Facility ID", help_text="Locally-assigned identifier", ) site = models.ForeignKey(to="dcim.Site", on_delete=models.PROTECT, related_name="racks") group = models.ForeignKey( to="dcim.RackGroup", on_delete=models.SET_NULL, related_name="racks", blank=True, null=True, help_text="Assigned group", ) tenant = models.ForeignKey( to="tenancy.Tenant", on_delete=models.PROTECT, related_name="racks", blank=True, null=True, ) role = models.ForeignKey( to="dcim.RackRole", on_delete=models.PROTECT, related_name="racks", blank=True, null=True, help_text="Functional role", ) serial = models.CharField(max_length=50, blank=True, verbose_name="Serial number") asset_tag = models.CharField( max_length=50, blank=True, null=True, unique=True, verbose_name="Asset tag", help_text="A unique tag used to identify this rack", ) type = models.CharField(choices=RackTypeChoices, max_length=50, blank=True, verbose_name="Type") width = models.PositiveSmallIntegerField( choices=RackWidthChoices, default=RackWidthChoices.WIDTH_19IN, verbose_name="Width", help_text="Rail-to-rail width", ) u_height = models.PositiveSmallIntegerField( default=RACK_U_HEIGHT_DEFAULT, verbose_name="Height (U)", validators=[MinValueValidator(1), MaxValueValidator(100)], help_text="Height in rack units", ) desc_units = models.BooleanField( default=False, verbose_name="Descending units", help_text="Units are numbered top-to-bottom", ) outer_width = models.PositiveSmallIntegerField(blank=True, null=True, help_text="Outer dimension of rack (width)") outer_depth = models.PositiveSmallIntegerField(blank=True, null=True, help_text="Outer dimension of rack (depth)") outer_unit = models.CharField( max_length=50, choices=RackDimensionUnitChoices, blank=True, ) comments = models.TextField(blank=True) images = GenericRelation(to="extras.ImageAttachment") csv_headers = [ "site", "group", "name", "facility_id", "tenant", "status", "role", "type", "serial", "asset_tag", "width", "u_height", "desc_units", "outer_width", "outer_depth", "outer_unit", "comments", ] clone_fields = [ "site", "group", "tenant", "status", "role", "type", "width", "u_height", "desc_units", "outer_width", "outer_depth", "outer_unit", ] class Meta: ordering = ("site", "group", "_name") # (site, group, name) may be non-unique unique_together = ( # Name and facility_id must be unique *only* within a RackGroup ("group", "name"), ("group", "facility_id"), ) def __str__(self): return self.display_name or super().__str__() def get_absolute_url(self): return reverse("dcim:rack", args=[self.pk]) def clean(self): super().clean() # Validate group/site assignment if self.site and self.group and self.group.site != self.site: raise ValidationError(f"Assigned rack group must belong to parent site ({self.site}).") # Validate outer dimensions and unit if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit: raise ValidationError("Must specify a unit when setting an outer width/depth") elif self.outer_width is None and self.outer_depth is None: self.outer_unit = "" if self.present_in_database: # Validate that Rack is tall enough to house the installed Devices top_device = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by("-position").first() if top_device: min_height = top_device.position + top_device.device_type.u_height - 1 if self.u_height < min_height: raise ValidationError( { "u_height": "Rack must be at least {}U tall to house currently installed devices.".format( min_height ) } ) # Validate that Rack was assigned a group of its same site, if applicable if self.group: if self.group.site != self.site: raise ValidationError({"group": "Rack group must be from the same site, {}.".format(self.site)}) def to_csv(self): return ( self.site.name, self.group.name if self.group else None, self.name, self.facility_id, self.tenant.name if self.tenant else None, self.get_status_display(), self.role.name if self.role else None, self.get_type_display() if self.type else None, self.serial, self.asset_tag, self.width, self.u_height, self.desc_units, self.outer_width, self.outer_depth, self.outer_unit, self.comments, ) @property def units(self): if self.desc_units: return range(1, self.u_height + 1) else: return reversed(range(1, self.u_height + 1)) @property def display_name(self): if self.facility_id: return f"{self.name} ({self.facility_id})" return self.name def get_rack_units( self, user=None, face=DeviceFaceChoices.FACE_FRONT, exclude=None, expand_devices=True, ): """ Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'} Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy. :param face: Rack face (front or rear) :param user: User instance to be used for evaluating device view permissions. If None, all devices will be included. :param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack :param expand_devices: When True, all units that a device occupies will be listed with each containing a reference to the device. When False, only the bottom most unit for a device is included and that unit contains a height attribute for the device """ elevation = OrderedDict() for u in self.units: elevation[u] = { "id": u, "name": f"U{u}", "face": face, "device": None, "occupied": False, } # Add devices to rack units list if self.present_in_database: # Retrieve all devices installed within the rack queryset = ( Device.objects.prefetch_related("device_type", "device_type__manufacturer", "device_role") .annotate(devicebay_count=Count("devicebays")) .exclude(pk=exclude) .filter(rack=self, position__gt=0, device_type__u_height__gt=0) .filter(Q(face=face) | Q(device_type__is_full_depth=True)) ) # Determine which devices the user has permission to view permitted_device_ids = [] if user is not None: permitted_device_ids = self.devices.restrict(user, "view").values_list("pk", flat=True) for device in queryset: if expand_devices: for u in range(device.position, device.position + device.device_type.u_height): if user is None or device.pk in permitted_device_ids: elevation[u]["device"] = device elevation[u]["occupied"] = True else: if user is None or device.pk in permitted_device_ids: elevation[device.position]["device"] = device elevation[device.position]["occupied"] = True elevation[device.position]["height"] = device.device_type.u_height for u in range( device.position + 1, device.position + device.device_type.u_height, ): elevation.pop(u, None) return [u for u in elevation.values()] def get_available_units(self, u_height=1, rack_face=None, exclude=None): """ Return a list of units within the rack available to accommodate a device of a given U height (default 1). Optionally exclude one or more devices when calculating empty units (needed when moving a device from one position to another within a rack). :param u_height: Minimum number of contiguous free units required :param rack_face: The face of the rack (front or rear) required; 'None' if device is full depth :param exclude: List of devices IDs to exclude (useful when moving a device within a rack) """ # Gather all devices which consume U space within the rack devices = self.devices.prefetch_related("device_type").filter(position__gte=1) if exclude is not None: devices = devices.exclude(pk__in=exclude) # Initialize the rack unit skeleton units = list(range(1, self.u_height + 1)) # Remove units consumed by installed devices for d in devices: if rack_face is None or d.face == rack_face or d.device_type.is_full_depth: for u in range(d.position, d.position + d.device_type.u_height): try: units.remove(u) except ValueError: # Found overlapping devices in the rack! pass # Remove units without enough space above them to accommodate a device of the specified height available_units = [] for u in units: if set(range(u, u + u_height)).issubset(units): available_units.append(u) return list(reversed(available_units)) def get_reserved_units(self): """ Return a dictionary mapping all reserved units within the rack to their reservation. """ reserved_units = {} for r in self.reservations.all(): for u in r.units: reserved_units[u] = r return reserved_units def get_elevation_svg( self, face=DeviceFaceChoices.FACE_FRONT, user=None, unit_width=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH, unit_height=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT, legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT, include_images=True, base_url=None, ): """ Return an SVG of the rack elevation :param face: Enum of [front, rear] representing the desired side of the rack elevation to render :param user: User instance to be used for evaluating device view permissions. If None, all devices will be included. :param unit_width: Width in pixels for the rendered drawing :param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total height of the elevation :param legend_width: Width of the unit legend, in pixels :param include_images: Embed front/rear device images where available :param base_url: Base URL for links and images. If none, URLs will be relative. """ elevation = RackElevationSVG(self, user=user, include_images=include_images, base_url=base_url) return elevation.render(face, unit_width, unit_height, legend_width) def get_0u_devices(self): return self.devices.filter(position=0) def get_utilization(self): """Gets utilization numerator and denominator for racks. Returns: UtilizationData: (numerator=Occupied Unit Count, denominator=U Height of the rack) """ # Determine unoccupied units available_units = self.get_available_units() # Remove reserved units for u in self.get_reserved_units(): if u in available_units: available_units.remove(u) # Return the numerator and denominator as percentage is to be calculated later where needed return UtilizationData(numerator=self.u_height - len(available_units), denominator=self.u_height) def get_power_utilization(self): """Determine the utilization numerator and denominator for power utilization on the rack. Returns: UtilizationData: (numerator, denominator) """ powerfeeds = PowerFeed.objects.filter(rack=self) available_power_total = sum(pf.available_power for pf in powerfeeds) if not available_power_total: return UtilizationData(numerator=0, denominator=0) pf_powerports = PowerPort.objects.filter( _cable_peer_type=ContentType.objects.get_for_model(PowerFeed), _cable_peer_id__in=powerfeeds.values_list("id", flat=True), ) poweroutlets = PowerOutlet.objects.filter(power_port_id__in=pf_powerports) allocated_draw_total = ( PowerPort.objects.filter( _cable_peer_type=ContentType.objects.get_for_model(PowerOutlet), _cable_peer_id__in=poweroutlets.values_list("id", flat=True), ).aggregate(Sum("allocated_draw"))["allocated_draw__sum"] or 0 ) return UtilizationData(numerator=allocated_draw_total, denominator=available_power_total)
class VMInterface(BaseModel, BaseInterface, CustomFieldModel): virtual_machine = models.ForeignKey( to="virtualization.VirtualMachine", on_delete=models.CASCADE, related_name="interfaces", ) name = models.CharField(max_length=64) _name = NaturalOrderingField( target_field="name", naturalize_function=naturalize_interface, max_length=100, blank=True, ) description = models.CharField(max_length=200, blank=True) untagged_vlan = models.ForeignKey( to="ipam.VLAN", on_delete=models.SET_NULL, related_name="vminterfaces_as_untagged", null=True, blank=True, verbose_name="Untagged VLAN", ) tagged_vlans = models.ManyToManyField( to="ipam.VLAN", related_name="vminterfaces_as_tagged", blank=True, verbose_name="Tagged VLANs", ) ip_addresses = GenericRelation( to="ipam.IPAddress", content_type_field="assigned_object_type", object_id_field="assigned_object_id", related_query_name="vminterface", ) tags = TaggableManager(through=TaggedItem, related_name="vminterface") csv_headers = [ "virtual_machine", "name", "enabled", "mac_address", "mtu", "description", "mode", ] class Meta: verbose_name = "VM interface" ordering = ("virtual_machine", CollateAsChar("_name")) unique_together = ("virtual_machine", "name") def __str__(self): return self.name def get_absolute_url(self): return reverse("virtualization:vminterface", kwargs={"pk": self.pk}) def to_csv(self): return ( self.virtual_machine.name, self.name, self.enabled, self.mac_address, self.mtu, self.description, self.get_mode_display(), ) def clean(self): super().clean() # Validate untagged VLAN if self.untagged_vlan and self.untagged_vlan.site not in [ self.virtual_machine.site, None, ]: raise ValidationError({ "untagged_vlan": f"The untagged VLAN ({self.untagged_vlan}) must belong to the same site as the " f"interface's parent virtual machine, or it must be global" }) def to_objectchange(self, action): # Annotate the parent VirtualMachine return ObjectChange( changed_object=self, object_repr=str(self), action=action, related_object=self.virtual_machine, object_data=serialize_object(self), ) @property def parent(self): return self.virtual_machine @property def count_ipaddresses(self): return self.ip_addresses.count()
class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): """ A network interface within a Device. A physical Interface can connect to exactly one other Interface. """ # Override ComponentModel._name to specify naturalize_interface function _name = NaturalOrderingField( target_field="name", naturalize_function=naturalize_interface, max_length=100, blank=True, ) lag = models.ForeignKey( to="self", on_delete=models.SET_NULL, related_name="member_interfaces", null=True, blank=True, verbose_name="Parent LAG", ) type = models.CharField(max_length=50, choices=InterfaceTypeChoices) mgmt_only = models.BooleanField( default=False, verbose_name="Management only", help_text="This interface is used only for out-of-band management", ) untagged_vlan = models.ForeignKey( to="ipam.VLAN", on_delete=models.SET_NULL, related_name="interfaces_as_untagged", null=True, blank=True, verbose_name="Untagged VLAN", ) tagged_vlans = models.ManyToManyField( to="ipam.VLAN", related_name="interfaces_as_tagged", blank=True, verbose_name="Tagged VLANs", ) ip_addresses = GenericRelation( to="ipam.IPAddress", content_type_field="assigned_object_type", object_id_field="assigned_object_id", related_query_name="interface", ) csv_headers = [ "device", "name", "label", "lag", "type", "enabled", "mac_address", "mtu", "mgmt_only", "description", "mode", ] class Meta: ordering = ("device", CollateAsChar("_name")) unique_together = ("device", "name") def get_absolute_url(self): return reverse("dcim:interface", kwargs={"pk": self.pk}) def to_csv(self): return ( self.device.identifier if self.device else None, self.name, self.label, self.lag.name if self.lag else None, self.get_type_display(), self.enabled, self.mac_address, self.mtu, self.mgmt_only, self.description, self.get_mode_display(), ) def clean(self): super().clean() # Virtual interfaces cannot be connected if self.type in NONCONNECTABLE_IFACE_TYPES and (self.cable or getattr( self, "circuit_termination", False)): raise ValidationError({ "type": "Virtual and wireless interfaces cannot be connected to another interface or circuit. " "Disconnect the interface or choose a suitable type." }) # An interface's LAG must belong to the same device or virtual chassis if self.lag and self.lag.device != self.device: if self.device.virtual_chassis is None: raise ValidationError({ "lag": f"The selected LAG interface ({self.lag}) belongs to a different device ({self.lag.device})." }) elif self.lag.device.virtual_chassis != self.device.virtual_chassis: raise ValidationError({ "lag": f"The selected LAG interface ({self.lag}) belongs to {self.lag.device}, which is not part " f"of virtual chassis {self.device.virtual_chassis}." }) # A virtual interface cannot have a parent LAG if self.type == InterfaceTypeChoices.TYPE_VIRTUAL and self.lag is not None: raise ValidationError({ "lag": "Virtual interfaces cannot have a parent LAG interface." }) # A LAG interface cannot be its own parent if self.present_in_database and self.lag_id == self.pk: raise ValidationError( {"lag": "A LAG interface cannot be its own parent."}) # Validate untagged VLAN if self.untagged_vlan and self.untagged_vlan.site not in [ self.parent.site, None, ]: raise ValidationError({ "untagged_vlan": "The untagged VLAN ({}) must belong to the same site as the interface's parent " "device, or it must be global".format(self.untagged_vlan) }) @property def parent(self): return self.device @property def is_connectable(self): return self.type not in NONCONNECTABLE_IFACE_TYPES @property def is_virtual(self): return self.type in VIRTUAL_IFACE_TYPES @property def is_wireless(self): return self.type in WIRELESS_IFACE_TYPES @property def is_lag(self): return self.type == InterfaceTypeChoices.TYPE_LAG @property def count_ipaddresses(self): return self.ip_addresses.count()
class Device(PrimaryModel, ConfigContextModel, StatusModel): """ A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique. Each Device must be assigned to a site, and optionally to a rack within that site. Associating a device with a particular rack face or unit is optional (for example, vertically mounted PDUs do not consume rack units). When a new Device is created, console/power/interface/device bay components are created along with it as dictated by the component templates assigned to its DeviceType. Components can also be added, modified, or deleted after the creation of a Device. """ device_type = models.ForeignKey(to="dcim.DeviceType", on_delete=models.PROTECT, related_name="instances") device_role = models.ForeignKey(to="dcim.DeviceRole", on_delete=models.PROTECT, related_name="devices") tenant = models.ForeignKey( to="tenancy.Tenant", on_delete=models.PROTECT, related_name="devices", blank=True, null=True, ) platform = models.ForeignKey( to="dcim.Platform", on_delete=models.SET_NULL, related_name="devices", blank=True, null=True, ) name = models.CharField(max_length=64, blank=True, null=True) _name = NaturalOrderingField(target_field="name", max_length=100, blank=True, null=True) serial = models.CharField(max_length=50, blank=True, verbose_name="Serial number") asset_tag = models.CharField( max_length=50, blank=True, null=True, unique=True, verbose_name="Asset tag", help_text="A unique tag used to identify this device", ) site = models.ForeignKey(to="dcim.Site", on_delete=models.PROTECT, related_name="devices") rack = models.ForeignKey( to="dcim.Rack", on_delete=models.PROTECT, related_name="devices", blank=True, null=True, ) position = models.PositiveSmallIntegerField( blank=True, null=True, validators=[MinValueValidator(1)], verbose_name="Position (U)", help_text="The lowest-numbered unit occupied by the device", ) face = models.CharField(max_length=50, blank=True, choices=DeviceFaceChoices, verbose_name="Rack face") primary_ip4 = models.OneToOneField( to="ipam.IPAddress", on_delete=models.SET_NULL, related_name="primary_ip4_for", blank=True, null=True, verbose_name="Primary IPv4", ) primary_ip6 = models.OneToOneField( to="ipam.IPAddress", on_delete=models.SET_NULL, related_name="primary_ip6_for", blank=True, null=True, verbose_name="Primary IPv6", ) cluster = models.ForeignKey( to="virtualization.Cluster", on_delete=models.SET_NULL, related_name="devices", blank=True, null=True, ) virtual_chassis = models.ForeignKey( to="VirtualChassis", on_delete=models.SET_NULL, related_name="members", blank=True, null=True, ) vc_position = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MaxValueValidator(255)]) vc_priority = models.PositiveSmallIntegerField(blank=True, null=True, validators=[MaxValueValidator(255)]) comments = models.TextField(blank=True) images = GenericRelation(to="extras.ImageAttachment") secrets_group = models.ForeignKey( to="extras.SecretsGroup", on_delete=models.SET_NULL, default=None, blank=True, null=True, ) objects = ConfigContextModelQuerySet.as_manager() csv_headers = [ "name", "device_role", "tenant", "manufacturer", "device_type", "platform", "serial", "asset_tag", "status", "site", "rack_group", "rack_name", "position", "face", "secrets_group", "comments", ] clone_fields = [ "device_type", "device_role", "tenant", "platform", "site", "rack", "status", "cluster", "secrets_group", ] class Meta: ordering = ("_name",) # Name may be null unique_together = ( ("site", "tenant", "name"), # See validate_unique below ("rack", "position", "face"), ("virtual_chassis", "vc_position"), ) def __str__(self): return self.display or super().__str__() def get_absolute_url(self): return reverse("dcim:device", args=[self.pk]) def validate_unique(self, exclude=None): # Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation # of the uniqueness constraint without manual intervention. if self.name and hasattr(self, "site") and self.tenant is None: if Device.objects.exclude(pk=self.pk).filter(name=self.name, site=self.site, tenant__isnull=True): raise ValidationError({"name": "A device with this name already exists."}) super().validate_unique(exclude) def clean(self): super().clean() # Validate site/rack combination if self.rack and self.site != self.rack.site: raise ValidationError( { "rack": f"Rack {self.rack} does not belong to site {self.site}.", } ) if self.rack is None: if self.face: raise ValidationError( { "face": "Cannot select a rack face without assigning a rack.", } ) if self.position: raise ValidationError( { "position": "Cannot select a rack position without assigning a rack.", } ) # Validate position/face combination if self.position and not self.face: raise ValidationError( { "face": "Must specify rack face when defining rack position.", } ) # Prevent 0U devices from being assigned to a specific position if self.position and self.device_type.u_height == 0: raise ValidationError( {"position": f"A U0 device type ({self.device_type}) cannot be assigned to a rack position."} ) if self.rack: try: # Child devices cannot be assigned to a rack face/unit if self.device_type.is_child_device and self.face: raise ValidationError( { "face": "Child device types cannot be assigned to a rack face. This is an attribute of the " "parent device." } ) if self.device_type.is_child_device and self.position: raise ValidationError( { "position": "Child device types cannot be assigned to a rack position. This is an attribute of " "the parent device." } ) # Validate rack space rack_face = self.face if not self.device_type.is_full_depth else None exclude_list = [self.pk] if self.present_in_database else [] available_units = self.rack.get_available_units( u_height=self.device_type.u_height, rack_face=rack_face, exclude=exclude_list, ) if self.position and self.position not in available_units: raise ValidationError( { "position": f"U{self.position} is already occupied or does not have sufficient space to " f"accommodate this device type: {self.device_type} ({self.device_type.u_height}U)" } ) except DeviceType.DoesNotExist: pass # Validate primary IP addresses vc_interfaces = self.vc_interfaces.all() if self.primary_ip4: if self.primary_ip4.family != 4: raise ValidationError({"primary_ip4": f"{self.primary_ip4} is not an IPv4 address."}) if self.primary_ip4.assigned_object in vc_interfaces: pass elif ( self.primary_ip4.nat_inside is not None and self.primary_ip4.nat_inside.assigned_object in vc_interfaces ): pass else: raise ValidationError( {"primary_ip4": f"The specified IP address ({self.primary_ip4}) is not assigned to this device."} ) if self.primary_ip6: if self.primary_ip6.family != 6: raise ValidationError({"primary_ip6": f"{self.primary_ip6} is not an IPv6 address."}) if self.primary_ip6.assigned_object in vc_interfaces: pass elif ( self.primary_ip6.nat_inside is not None and self.primary_ip6.nat_inside.assigned_object in vc_interfaces ): pass else: raise ValidationError( {"primary_ip6": f"The specified IP address ({self.primary_ip6}) is not assigned to this device."} ) # Validate manufacturer/platform if hasattr(self, "device_type") and self.platform: if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer: raise ValidationError( { "platform": "The assigned platform is limited to {} device types, but this device's type belongs " "to {}.".format(self.platform.manufacturer, self.device_type.manufacturer) } ) # A Device can only be assigned to a Cluster in the same Site (or no Site) if self.cluster and self.cluster.site is not None and self.cluster.site != self.site: raise ValidationError( {"cluster": "The assigned cluster belongs to a different site ({})".format(self.cluster.site)} ) # Validate virtual chassis assignment if self.virtual_chassis and self.vc_position is None: raise ValidationError( {"vc_position": "A device assigned to a virtual chassis must have its position defined."} ) def save(self, *args, **kwargs): is_new = not self.present_in_database super().save(*args, **kwargs) # If this is a new Device, instantiate all of the related components per the DeviceType definition if is_new: ConsolePort.objects.bulk_create([x.instantiate(self) for x in self.device_type.consoleporttemplates.all()]) ConsoleServerPort.objects.bulk_create( [x.instantiate(self) for x in self.device_type.consoleserverporttemplates.all()] ) PowerPort.objects.bulk_create([x.instantiate(self) for x in self.device_type.powerporttemplates.all()]) PowerOutlet.objects.bulk_create([x.instantiate(self) for x in self.device_type.poweroutlettemplates.all()]) Interface.objects.bulk_create([x.instantiate(self) for x in self.device_type.interfacetemplates.all()]) RearPort.objects.bulk_create([x.instantiate(self) for x in self.device_type.rearporttemplates.all()]) FrontPort.objects.bulk_create([x.instantiate(self) for x in self.device_type.frontporttemplates.all()]) DeviceBay.objects.bulk_create([x.instantiate(self) for x in self.device_type.devicebaytemplates.all()]) # Update Site and Rack assignment for any child Devices devices = Device.objects.filter(parent_bay__device=self) for device in devices: device.site = self.site device.rack = self.rack device.save() def to_csv(self): return ( self.name or "", self.device_role.name, self.tenant.name if self.tenant else None, self.device_type.manufacturer.name, self.device_type.model, self.platform.name if self.platform else None, self.serial, self.asset_tag, self.get_status_display(), self.site.name, self.rack.group.name if self.rack and self.rack.group else None, self.rack.name if self.rack else None, self.position, self.get_face_display(), self.comments, ) @property def display(self): if self.name: return self.name elif self.virtual_chassis: return f"{self.virtual_chassis.name}:{self.vc_position} ({self.pk})" elif self.device_type: return f"{self.device_type.manufacturer} {self.device_type.model} ({self.pk})" else: return "" # Device has not yet been created @property def identifier(self): """ Return the device name if set; otherwise return the Device's primary key as {pk} """ if self.name is not None: return self.name return "{{{}}}".format(self.pk) @property def primary_ip(self): if get_settings_or_config("PREFER_IPV4") and self.primary_ip4: return self.primary_ip4 elif self.primary_ip6: return self.primary_ip6 elif self.primary_ip4: return self.primary_ip4 else: return None def get_vc_master(self): """ If this Device is a VirtualChassis member, return the VC master. Otherwise, return None. """ return self.virtual_chassis.master if self.virtual_chassis else None @property def vc_interfaces(self): """ Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another Device belonging to the same VirtualChassis. """ filter = Q(device=self) if self.virtual_chassis and self.virtual_chassis.master == self: filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False) return Interface.objects.filter(filter) def get_cables(self, pk_list=False): """ Return a QuerySet or PK list matching all Cables connected to a component of this Device. """ from .cables import Cable cable_pks = [] for component_model in [ ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, FrontPort, RearPort, ]: cable_pks += component_model.objects.filter(device=self, cable__isnull=False).values_list( "cable", flat=True ) if pk_list: return cable_pks return Cable.objects.filter(pk__in=cable_pks) def get_children(self): """ Return the set of child Devices installed in DeviceBays within this Device. """ return Device.objects.filter(parent_bay__device=self.pk)
class Site(PrimaryModel, StatusModel): """ A Site represents a geographic location within a network; typically a building or campus. The optional facility field can be used to include an external designation, such as a data center name (e.g. Equinix SV6). """ name = models.CharField(max_length=100, unique=True) _name = NaturalOrderingField(target_field="name", max_length=100, blank=True, db_index=True) slug = AutoSlugField(populate_from="name") region = models.ForeignKey( to="dcim.Region", on_delete=models.SET_NULL, related_name="sites", blank=True, null=True, ) tenant = models.ForeignKey( to="tenancy.Tenant", on_delete=models.PROTECT, related_name="sites", blank=True, null=True, ) facility = models.CharField(max_length=50, blank=True, help_text="Local facility ID or description") asn = ASNField( blank=True, null=True, verbose_name="ASN", help_text="32-bit autonomous system number", ) time_zone = TimeZoneField(blank=True) description = models.CharField(max_length=200, blank=True) physical_address = models.CharField(max_length=200, blank=True) shipping_address = models.CharField(max_length=200, blank=True) latitude = models.DecimalField( max_digits=8, decimal_places=6, blank=True, null=True, help_text="GPS coordinate (latitude)", ) longitude = models.DecimalField( max_digits=9, decimal_places=6, blank=True, null=True, help_text="GPS coordinate (longitude)", ) contact_name = models.CharField(max_length=50, blank=True) contact_phone = models.CharField(max_length=20, blank=True) contact_email = models.EmailField(blank=True, verbose_name="Contact E-mail") comments = models.TextField(blank=True) images = GenericRelation(to="extras.ImageAttachment") csv_headers = [ "name", "slug", "status", "region", "tenant", "facility", "asn", "time_zone", "description", "physical_address", "shipping_address", "latitude", "longitude", "contact_name", "contact_phone", "contact_email", "comments", ] clone_fields = [ "status", "region", "tenant", "facility", "asn", "time_zone", "description", "physical_address", "shipping_address", "latitude", "longitude", "contact_name", "contact_phone", "contact_email", ] class Meta: ordering = ("_name", ) def __str__(self): return self.name def get_absolute_url(self): return reverse("dcim:site", args=[self.slug]) def to_csv(self): return ( self.name, self.slug, self.get_status_display(), self.region.name if self.region else None, self.tenant.name if self.tenant else None, self.facility, self.asn, self.time_zone, self.description, self.physical_address, self.shipping_address, self.latitude, self.longitude, self.contact_name, self.contact_phone, self.contact_email, self.comments, )