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,
        )
예제 #2
0
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)
예제 #3
0
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)
예제 #4
0
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)
예제 #5
0
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()
예제 #6
0
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()
예제 #7
0
파일: devices.py 프로젝트: dgarros/nautobot
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)
예제 #8
0
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,
        )