コード例 #1
0
ファイル: device_components.py プロジェクト: ffddorf/netbox
class FrontPort(CableTermination, ComponentModel):
    """
    A pass-through port on the front of a Device.
    """
    device = models.ForeignKey(to='dcim.Device',
                               on_delete=models.CASCADE,
                               related_name='frontports')
    name = models.CharField(max_length=64)
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)
    type = models.CharField(max_length=50, choices=PortTypeChoices)
    rear_port = models.ForeignKey(to='dcim.RearPort',
                                  on_delete=models.CASCADE,
                                  related_name='frontports')
    rear_port_position = models.PositiveSmallIntegerField(
        default=1, validators=[MinValueValidator(1),
                               MaxValueValidator(64)])
    tags = TaggableManager(through=TaggedItem)

    csv_headers = [
        'device', 'name', 'type', 'rear_port', 'rear_port_position',
        'description'
    ]
    is_path_endpoint = False

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

    def __str__(self):
        return self.name

    def to_csv(self):
        return (
            self.device.identifier,
            self.name,
            self.get_type_display(),
            self.rear_port.name,
            self.rear_port_position,
            self.description,
        )

    def clean(self):

        # Validate rear port assignment
        if self.rear_port.device != self.device:
            raise ValidationError(
                "Rear port ({}) must belong to the same device".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))
コード例 #2
0
class RearPortTemplate(ComponentTemplateModel):
    """
    Template for a pass-through port on the rear of a new Device.
    """
    device_type = models.ForeignKey(to='dcim.DeviceType',
                                    on_delete=models.CASCADE,
                                    related_name='rearport_templates')
    name = models.CharField(max_length=64)
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)
    type = models.CharField(max_length=50, choices=PortTypeChoices)
    positions = models.PositiveSmallIntegerField(
        default=1, validators=[MinValueValidator(1),
                               MaxValueValidator(64)])

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

    def __str__(self):
        return self.name

    def instantiate(self, device):
        return RearPort(device=device,
                        name=self.name,
                        type=self.type,
                        positions=self.positions)
コード例 #3
0
class InterfaceTemplate(ComponentTemplateModel):
    """
    A template for a physical data interface on a new Device.
    """
    device_type = models.ForeignKey(to='dcim.DeviceType',
                                    on_delete=models.CASCADE,
                                    related_name='interface_templates')
    name = models.CharField(max_length=64)
    _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 __str__(self):
        return self.name

    def instantiate(self, device):
        return Interface(device=device,
                         name=self.name,
                         type=self.type,
                         mgmt_only=self.mgmt_only)
コード例 #4
0
ファイル: device_components.py プロジェクト: ffddorf/netbox
class RearPort(CableTermination, ComponentModel):
    """
    A pass-through port on the rear of a Device.
    """
    device = models.ForeignKey(to='dcim.Device',
                               on_delete=models.CASCADE,
                               related_name='rearports')
    name = models.CharField(max_length=64)
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)
    type = models.CharField(max_length=50, choices=PortTypeChoices)
    positions = models.PositiveSmallIntegerField(
        default=1, validators=[MinValueValidator(1),
                               MaxValueValidator(64)])
    tags = TaggableManager(through=TaggedItem)

    csv_headers = ['device', 'name', 'type', 'positions', 'description']
    is_path_endpoint = False

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

    def __str__(self):
        return self.name

    def to_csv(self):
        return (
            self.device.identifier,
            self.name,
            self.get_type_display(),
            self.positions,
            self.description,
        )
コード例 #5
0
ファイル: device_components.py プロジェクト: ypid/netbox
class ConsoleServerPort(CableTermination, ComponentModel):
    """
    A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
    """
    device = models.ForeignKey(to='dcim.Device',
                               on_delete=models.CASCADE,
                               related_name='consoleserverports')
    name = models.CharField(max_length=50)
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)
    type = models.CharField(max_length=50,
                            choices=ConsolePortTypeChoices,
                            blank=True,
                            help_text='Physical port type')
    connection_status = models.NullBooleanField(
        choices=CONNECTION_STATUS_CHOICES, blank=True)
    tags = TaggableManager(through=TaggedItem)

    csv_headers = ['device', 'name', 'type', 'description']

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

    def get_absolute_url(self):
        return self.device.get_absolute_url()

    def to_csv(self):
        return (
            self.device.identifier,
            self.name,
            self.type,
            self.description,
        )
コード例 #6
0
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
        )
コード例 #7
0
class ComponentTemplateModel(ChangeLoggedModel):
    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 super().to_objectchange(action, related_object=device_type)
コード例 #8
0
ファイル: device_components.py プロジェクト: stobib/netbox-1
class PowerOutlet(CableTermination, ComponentModel):
    """
    A physical power outlet (output) within a Device which provides power to a PowerPort.
    """
    device = models.ForeignKey(to='dcim.Device',
                               on_delete=models.CASCADE,
                               related_name='poweroutlets')
    name = models.CharField(max_length=50)
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)
    type = models.CharField(max_length=50,
                            choices=PowerOutletTypeChoices,
                            blank=True,
                            help_text='Physical port type')
    power_port = models.ForeignKey(to='dcim.PowerPort',
                                   on_delete=models.SET_NULL,
                                   blank=True,
                                   null=True,
                                   related_name='poweroutlets')
    feed_leg = models.CharField(max_length=50,
                                choices=PowerOutletFeedLegChoices,
                                blank=True,
                                help_text="Phase (for three-phase feeds)")
    connection_status = models.NullBooleanField(
        choices=CONNECTION_STATUS_CHOICES, blank=True)
    tags = TaggableManager(through=TaggedItem)

    csv_headers = [
        'device', 'name', 'type', 'power_port', 'feed_leg', 'description'
    ]

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

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.device.get_absolute_url()

    def to_csv(self):
        return (
            self.device.identifier,
            self.name,
            self.get_type_display(),
            self.power_port.name if self.power_port else None,
            self.get_feed_leg_display(),
            self.description,
        )

    def clean(self):

        # Validate power port assignment
        if self.power_port and self.power_port.device != self.device:
            raise ValidationError(
                "Parent power port ({}) must belong to the same device".format(
                    self.power_port))
コード例 #9
0
ファイル: device_components.py プロジェクト: ypid/netbox
class DeviceBay(ComponentModel):
    """
    An empty space within a Device which can house a child device
    """
    device = models.ForeignKey(to='dcim.Device',
                               on_delete=models.CASCADE,
                               related_name='device_bays')
    name = models.CharField(max_length=50, verbose_name='Name')
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)
    installed_device = models.OneToOneField(to='dcim.Device',
                                            on_delete=models.SET_NULL,
                                            related_name='parent_bay',
                                            blank=True,
                                            null=True)
    tags = TaggableManager(through=TaggedItem)

    csv_headers = ['device', 'name', 'installed_device', 'description']

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

    def get_absolute_url(self):
        return self.device.get_absolute_url()

    def to_csv(self):
        return (
            self.device.identifier,
            self.name,
            self.installed_device.identifier
            if self.installed_device else None,
            self.description,
        )

    def clean(self):

        # Validate that the parent Device can have DeviceBays
        if not self.device.device_type.is_parent_device:
            raise ValidationError(
                "This type of device ({}) does not support device bays.".
                format(self.device.device_type))

        # Cannot install a device into itself, obviously
        if self.device == self.installed_device:
            raise ValidationError("Cannot install a device into itself.")

        # Check that the installed device is not already installed elsewhere
        if self.installed_device:
            current_bay = DeviceBay.objects.filter(
                installed_device=self.installed_device).first()
            if current_bay and current_bay != self:
                raise ValidationError({
                    'installed_device':
                    "Cannot install the specified device; device is already installed in {}"
                    .format(current_bay)
                })
コード例 #10
0
class FrontPortTemplate(ComponentTemplateModel):
    """
    Template for a pass-through port on the front of a new Device.
    """
    device_type = models.ForeignKey(to='dcim.DeviceType',
                                    on_delete=models.CASCADE,
                                    related_name='frontport_templates')
    name = models.CharField(max_length=64)
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)
    type = models.CharField(max_length=50, choices=PortTypeChoices)
    rear_port = models.ForeignKey(to='dcim.RearPortTemplate',
                                  on_delete=models.CASCADE,
                                  related_name='frontport_templates')
    rear_port_position = models.PositiveSmallIntegerField(
        default=1, validators=[MinValueValidator(1),
                               MaxValueValidator(64)])

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

    def __str__(self):
        return self.name

    def clean(self):

        # 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))

    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,
                         type=self.type,
                         rear_port=rear_port,
                         rear_port_position=self.rear_port_position)
コード例 #11
0
ファイル: device_components.py プロジェクト: zhbdesign/netbox
class ComponentModel(models.Model):
    """
    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
    )
    _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
    )

    objects = RestrictedQuerySet.as_manager()

    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,
            related_object=device,
            object_data=serialize_object(self)
        )

    @property
    def parent(self):
        return getattr(self, 'device', None)
コード例 #12
0
class PowerOutletTemplate(ComponentTemplateModel):
    """
    A template for a PowerOutlet to be created for a new Device.
    """
    device_type = models.ForeignKey(to='dcim.DeviceType',
                                    on_delete=models.CASCADE,
                                    related_name='poweroutlet_templates')
    name = models.CharField(max_length=50)
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)
    type = models.CharField(max_length=50,
                            choices=PowerOutletTypeChoices,
                            blank=True)
    power_port = models.ForeignKey(to='dcim.PowerPortTemplate',
                                   on_delete=models.SET_NULL,
                                   blank=True,
                                   null=True,
                                   related_name='poweroutlet_templates')
    feed_leg = models.CharField(max_length=50,
                                choices=PowerOutletFeedLegChoices,
                                blank=True,
                                help_text="Phase (for three-phase feeds)")

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

    def __str__(self):
        return self.name

    def clean(self):

        # Validate power port assignment
        if self.power_port and self.power_port.device_type != self.device_type:
            raise ValidationError(
                "Parent power port ({}) must belong to the same device type".
                format(self.power_port))

    def instantiate(self, device):
        if self.power_port:
            power_port = PowerPort.objects.get(device=device,
                                               name=self.power_port.name)
        else:
            power_port = None
        return PowerOutlet(device=device,
                           name=self.name,
                           type=self.type,
                           power_port=power_port,
                           feed_leg=self.feed_leg)
コード例 #13
0
class DeviceBayTemplate(ComponentTemplateModel):
    """
    A template for a DeviceBay to be created for a new parent Device.
    """
    device_type = models.ForeignKey(to='dcim.DeviceType',
                                    on_delete=models.CASCADE,
                                    related_name='device_bay_templates')
    name = models.CharField(max_length=50)
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)

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

    def __str__(self):
        return self.name

    def instantiate(self, device):
        return DeviceBay(device=device, name=self.name)
コード例 #14
0
class PowerPortTemplate(ComponentTemplateModel):
    """
    A template for a PowerPort to be created for a new Device.
    """
    device_type = models.ForeignKey(to='dcim.DeviceType',
                                    on_delete=models.CASCADE,
                                    related_name='powerport_templates')
    name = models.CharField(max_length=50)
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)
    type = models.CharField(max_length=50,
                            choices=PowerPortTypeChoices,
                            blank=True)
    maximum_draw = models.PositiveSmallIntegerField(
        blank=True,
        null=True,
        validators=[MinValueValidator(1)],
        help_text="Maximum power draw (watts)")
    allocated_draw = models.PositiveSmallIntegerField(
        blank=True,
        null=True,
        validators=[MinValueValidator(1)],
        help_text="Allocated power draw (watts)")

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

    def __str__(self):
        return self.name

    def instantiate(self, device):
        return PowerPort(device=device,
                         name=self.name,
                         type=self.type,
                         maximum_draw=self.maximum_draw,
                         allocated_draw=self.allocated_draw)
コード例 #15
0
class ConsoleServerPortTemplate(ComponentTemplateModel):
    """
    A template for a ConsoleServerPort to be created for a new Device.
    """
    device_type = models.ForeignKey(to='dcim.DeviceType',
                                    on_delete=models.CASCADE,
                                    related_name='consoleserverport_templates')
    name = models.CharField(max_length=50)
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)
    type = models.CharField(max_length=50,
                            choices=ConsolePortTypeChoices,
                            blank=True)

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

    def __str__(self):
        return self.name

    def instantiate(self, device):
        return ConsoleServerPort(device=device, name=self.name, type=self.type)
コード例 #16
0
ファイル: device_components.py プロジェクト: ffddorf/netbox
class Interface(CableTermination, ComponentModel):
    """
    A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other
    Interface.
    """
    device = models.ForeignKey(to='Device',
                               on_delete=models.CASCADE,
                               related_name='interfaces',
                               null=True,
                               blank=True)
    virtual_machine = models.ForeignKey(to='virtualization.VirtualMachine',
                                        on_delete=models.CASCADE,
                                        related_name='interfaces',
                                        null=True,
                                        blank=True)
    name = models.CharField(max_length=64)
    _name = NaturalOrderingField(target_field='name',
                                 naturalize_function=naturalize_interface,
                                 max_length=100,
                                 blank=True)
    _connected_interface = models.OneToOneField(to='self',
                                                on_delete=models.SET_NULL,
                                                related_name='+',
                                                blank=True,
                                                null=True)
    _connected_circuittermination = models.OneToOneField(
        to='circuits.CircuitTermination',
        on_delete=models.SET_NULL,
        related_name='+',
        blank=True,
        null=True)
    connection_status = models.NullBooleanField(
        choices=CONNECTION_STATUS_CHOICES, 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)
    enabled = models.BooleanField(default=True)
    mac_address = MACAddressField(null=True,
                                  blank=True,
                                  verbose_name='MAC Address')
    mtu = models.PositiveIntegerField(
        blank=True,
        null=True,
        validators=[MinValueValidator(1),
                    MaxValueValidator(65536)],
        verbose_name='MTU')
    mgmt_only = models.BooleanField(
        default=False,
        verbose_name='OOB Management',
        help_text='This interface is used only for out-of-band management')
    mode = models.CharField(
        max_length=50,
        choices=InterfaceModeChoices,
        blank=True,
    )
    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')
    tags = TaggableManager(through=TaggedItem)

    csv_headers = [
        'device',
        'virtual_machine',
        'name',
        'lag',
        'type',
        'enabled',
        'mac_address',
        'mtu',
        'mgmt_only',
        'description',
        'mode',
    ]

    class Meta:
        # TODO: ordering and unique_together should include virtual_machine
        ordering = ('device', '_name')
        unique_together = ('device', 'name')

    def __str__(self):
        return self.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.virtual_machine.name if self.virtual_machine else None,
            self.name,
            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):

        # An Interface must belong to a Device *or* to a VirtualMachine
        if self.device and self.virtual_machine:
            raise ValidationError(
                "An interface cannot belong to both a device and a virtual machine."
            )
        if not self.device and not self.virtual_machine:
            raise ValidationError(
                "An interface must belong to either a device or a virtual machine."
            )

        # VM interfaces must be virtual
        if self.virtual_machine and self.type not in VMInterfaceTypeChoices.values(
        ):
            raise ValidationError({
                'type':
                "Invalid interface type for a virtual machine: {}".format(
                    self.type)
            })

        # 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 VC master)
        if self.lag and self.lag.device not in [
                self.device, self.device.get_vc_master()
        ]:
            raise ValidationError({
                'lag':
                "The selected LAG interface ({}) belongs to a different device ({})."
                .format(self.lag.name, self.lag.device.name)
            })

        # A virtual interface cannot have a parent LAG
        if self.type in NONCONNECTABLE_IFACE_TYPES and self.lag is not None:
            raise ValidationError({
                'lag':
                "{} interfaces cannot have a parent LAG interface.".format(
                    self.get_type_display())
            })

        # Only a LAG can have LAG members
        if self.type != InterfaceTypeChoices.TYPE_LAG and self.member_interfaces.exists(
        ):
            raise ValidationError({
                'type':
                "Cannot change interface type; it has LAG members ({}).".
                format(", ".join(
                    [iface.name for iface in self.member_interfaces.all()]))
            })

        # 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/VM, or it must be global".format(self.untagged_vlan)
            })

    def save(self, *args, **kwargs):

        # Remove untagged VLAN assignment for non-802.1Q interfaces
        if self.mode is None:
            self.untagged_vlan = None

        # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.)
        if self.pk and self.mode != InterfaceModeChoices.MODE_TAGGED:
            self.tagged_vlans.clear()

        return super().save(*args, **kwargs)

    def to_objectchange(self, action):
        # Annotate the parent Device/VM
        try:
            parent_obj = self.device or self.virtual_machine
        except ObjectDoesNotExist:
            parent_obj = None

        return ObjectChange(changed_object=self,
                            object_repr=str(self),
                            action=action,
                            related_object=parent_obj,
                            object_data=serialize_object(self))

    @property
    def connected_endpoint(self):
        """
        Return the connected Interface, if it exists, or the connected CircuitTermination, if it exists. We have to
        check for ObjectDoesNotExist in case the referenced object has been deleted from the database.
        """
        try:
            if self._connected_interface:
                return self._connected_interface
        except ObjectDoesNotExist:
            pass
        try:
            if self._connected_circuittermination:
                return self._connected_circuittermination
        except ObjectDoesNotExist:
            pass
        return None

    @connected_endpoint.setter
    def connected_endpoint(self, value):
        from circuits.models import CircuitTermination

        if value is None:
            self._connected_interface = None
            self._connected_circuittermination = None
        elif isinstance(value, Interface):
            self._connected_interface = value
            self._connected_circuittermination = None
        elif isinstance(value, CircuitTermination):
            self._connected_interface = None
            self._connected_circuittermination = value
        else:
            raise ValueError(
                "Connected endpoint must be an Interface or CircuitTermination, not {}."
                .format(type(value)))

    @property
    def parent(self):
        return self.device or self.virtual_machine

    @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()
コード例 #17
0
ファイル: device_components.py プロジェクト: ffddorf/netbox
class PowerPort(CableTermination, ComponentModel):
    """
    A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
    """
    device = models.ForeignKey(to='dcim.Device',
                               on_delete=models.CASCADE,
                               related_name='powerports')
    name = models.CharField(max_length=50)
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)
    type = models.CharField(max_length=50,
                            choices=PowerPortTypeChoices,
                            blank=True)
    maximum_draw = models.PositiveSmallIntegerField(
        blank=True,
        null=True,
        validators=[MinValueValidator(1)],
        help_text="Maximum power draw (watts)")
    allocated_draw = models.PositiveSmallIntegerField(
        blank=True,
        null=True,
        validators=[MinValueValidator(1)],
        help_text="Allocated power draw (watts)")
    _connected_poweroutlet = models.OneToOneField(
        to='dcim.PowerOutlet',
        on_delete=models.SET_NULL,
        related_name='connected_endpoint',
        blank=True,
        null=True)
    _connected_powerfeed = models.OneToOneField(to='dcim.PowerFeed',
                                                on_delete=models.SET_NULL,
                                                related_name='+',
                                                blank=True,
                                                null=True)
    connection_status = models.NullBooleanField(
        choices=CONNECTION_STATUS_CHOICES, blank=True)
    tags = TaggableManager(through=TaggedItem)

    csv_headers = [
        'device', 'name', 'type', 'maximum_draw', 'allocated_draw',
        'description'
    ]

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

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return self.device.get_absolute_url()

    def to_csv(self):
        return (
            self.device.identifier,
            self.name,
            self.get_type_display(),
            self.maximum_draw,
            self.allocated_draw,
            self.description,
        )

    @property
    def connected_endpoint(self):
        """
        Return the connected PowerOutlet, if it exists, or the connected PowerFeed, if it exists. We have to check for
        ObjectDoesNotExist in case the referenced object has been deleted from the database.
        """
        try:
            if self._connected_poweroutlet:
                return self._connected_poweroutlet
        except ObjectDoesNotExist:
            pass
        try:
            if self._connected_powerfeed:
                return self._connected_powerfeed
        except ObjectDoesNotExist:
            pass
        return None

    @connected_endpoint.setter
    def connected_endpoint(self, value):
        # TODO: Fix circular import
        from . import PowerFeed

        if value is None:
            self._connected_poweroutlet = None
            self._connected_powerfeed = None
        elif isinstance(value, PowerOutlet):
            self._connected_poweroutlet = value
            self._connected_powerfeed = None
        elif isinstance(value, PowerFeed):
            self._connected_poweroutlet = None
            self._connected_powerfeed = value
        else:
            raise ValueError(
                "Connected endpoint must be a PowerOutlet or PowerFeed, not {}."
                .format(type(value)))

    def get_power_draw(self):
        """
        Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort.
        """
        # Calculate aggregate draw of all child power outlets if no numbers have been defined manually
        if self.allocated_draw is None and self.maximum_draw is None:
            outlet_ids = PowerOutlet.objects.filter(
                power_port=self).values_list('pk', flat=True)
            utilization = PowerPort.objects.filter(
                _connected_poweroutlet_id__in=outlet_ids).aggregate(
                    maximum_draw_total=Sum('maximum_draw'),
                    allocated_draw_total=Sum('allocated_draw'),
                )
            ret = {
                'allocated': utilization['allocated_draw_total'] or 0,
                'maximum': utilization['maximum_draw_total'] or 0,
                'outlet_count': len(outlet_ids),
                'legs': [],
            }

            # Calculate per-leg aggregates for three-phase feeds
            if self._connected_powerfeed and self._connected_powerfeed.phase == PowerFeedPhaseChoices.PHASE_3PHASE:
                for leg, leg_name in PowerOutletFeedLegChoices:
                    outlet_ids = PowerOutlet.objects.filter(
                        power_port=self, feed_leg=leg).values_list('pk',
                                                                   flat=True)
                    utilization = PowerPort.objects.filter(
                        _connected_poweroutlet_id__in=outlet_ids).aggregate(
                            maximum_draw_total=Sum('maximum_draw'),
                            allocated_draw_total=Sum('allocated_draw'),
                        )
                    ret['legs'].append({
                        'name':
                        leg_name,
                        'allocated':
                        utilization['allocated_draw_total'] or 0,
                        'maximum':
                        utilization['maximum_draw_total'] or 0,
                        'outlet_count':
                        len(outlet_ids),
                    })

            return ret

        # Default to administratively defined values
        return {
            'allocated': self.allocated_draw or 0,
            'maximum': self.maximum_draw or 0,
            'outlet_count':
            PowerOutlet.objects.filter(power_port=self).count(),
            'legs': [],
        }
コード例 #18
0
ファイル: racks.py プロジェクト: vinciconsulting/netbox
class Rack(ChangeLoggedModel, CustomFieldModel):
    """
    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
    )
    status = models.CharField(
        max_length=50,
        choices=RackStatusChoices,
        default=RackStatusChoices.STATUS_ACTIVE
    )
    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'
    )
    tags = TaggableManager(through=TaggedItem)

    objects = RestrictedQuerySet.as_manager()

    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', 'pk')  # (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 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.pk:
            # 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_status_class(self):
        return RackStatusChoices.CSS_CLASSES.get(self.status)

    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.pk:

            # 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):
        """
        Determine the utilization rate of the rack and return it as a percentage. Occupied and reserved units both count
        as utilized.
        """
        # 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)

        occupied_unit_count = self.u_height - len(available_units)
        percentage = int(float(occupied_unit_count) / self.u_height * 100)

        return percentage

    def get_power_utilization(self):
        """
        Determine the utilization rate of power in the rack and return it as a percentage.
        """
        powerfeeds = PowerFeed.objects.filter(rack=self)
        available_power_total = sum(pf.available_power for pf in powerfeeds)
        if not available_power_total:
            return 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 int(allocated_draw_total / available_power_total * 100)
コード例 #19
0
ファイル: models.py プロジェクト: vsrn09/netbox
class VMInterface(BaseInterface):
    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')

    objects = RestrictedQuerySet.as_manager()

    csv_headers = [
        'virtual_machine',
        'name',
        'enabled',
        'mac_address',
        'mtu',
        'description',
        'mode',
    ]

    class Meta:
        verbose_name = '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):

        # Validate untagged VLAN
        if self.untagged_vlan and self.untagged_vlan.site not in [
                self.virtual_machine.site, None
        ]:
            raise ValidationError({
                'untagged_vlan':
                "The untagged VLAN ({}) must belong to the same site as the interface's parent "
                "virtual machine, or it must be global".format(
                    self.untagged_vlan)
            })

    def save(self, *args, **kwargs):

        # Remove untagged VLAN assignment for non-802.1Q interfaces
        if self.mode is None:
            self.untagged_vlan = None

        # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.)
        if self.pk and self.mode != InterfaceModeChoices.MODE_TAGGED:
            self.tagged_vlans.clear()

        return super().save(*args, **kwargs)

    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()
コード例 #20
0
class VMInterface(PrimaryModel, BaseInterface):
    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
    )
    parent = models.ForeignKey(
        to='self',
        on_delete=models.SET_NULL,
        related_name='child_interfaces',
        null=True,
        blank=True,
        verbose_name='Parent interface'
    )
    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'
    )

    objects = RestrictedQuerySet.as_manager()

    csv_headers = [
        'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
    ]

    class Meta:
        verbose_name = '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.parent.name if self.parent else None,
            self.mac_address,
            self.mtu,
            self.description,
            self.get_mode_display(),
        )

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

        # An interface's parent must belong to the same virtual machine
        if self.parent and self.parent.virtual_machine != self.virtual_machine:
            raise ValidationError({
                'parent': f"The selected parent interface ({self.parent}) belongs to a different virtual machine "
                          f"({self.parent.virtual_machine})."
            })

        # An interface cannot be its own parent
        if self.pk and self.parent_id == self.pk:
            raise ValidationError({'parent': "An interface cannot be its own parent."})

        # 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 super().to_objectchange(action, related_object=self.virtual_machine)

    @property
    def parent_object(self):
        return self.virtual_machine
コード例 #21
0
ファイル: device_components.py プロジェクト: xcdr/netbox
class Interface(ComponentModel, BaseInterface, CableTermination, PathEndpoint):
    """
    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)
    parent = models.ForeignKey(to='self',
                               on_delete=models.SET_NULL,
                               related_name='child_interfaces',
                               null=True,
                               blank=True,
                               verbose_name='Parent interface')
    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')

    clone_fields = ['device', 'parent', 'lag', 'type', 'mgmt_only']

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

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

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

        # Virtual interfaces cannot be connected
        if not self.is_connectable and self.cable:
            raise ValidationError({
                'type':
                f"{self.get_type_display()} interfaces cannot have a cable attached."
            })

        # Non-connectable interfaces cannot be marked as connected
        if not self.is_connectable and self.mark_connected:
            raise ValidationError({
                'mark_connected':
                f"{self.get_type_display()} interfaces cannot be marked as connected."
            })

        # An interface's parent must belong to the same device or virtual chassis
        if self.parent and self.parent.device != self.device:
            if self.device.virtual_chassis is None:
                raise ValidationError({
                    'parent':
                    f"The selected parent interface ({self.parent}) belongs to a different device "
                    f"({self.parent.device})."
                })
            elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
                raise ValidationError({
                    'parent':
                    f"The selected parent interface ({self.parent}) belongs to {self.parent.device}, which "
                    f"is not part of virtual chassis {self.device.virtual_chassis}."
                })

        # An interface cannot be its own parent
        if self.pk and self.parent_id == self.pk:
            raise ValidationError(
                {'parent': "An interface cannot be its own parent."})

        # A physical interface cannot have a parent interface
        if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None:
            raise ValidationError({
                'parent':
                "Only virtual interfaces may be assigned to a parent interface."
            })

        # 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.pk 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.device.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 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
コード例 #22
0
ファイル: models.py プロジェクト: JonathonReinhart/netbox
class VMInterface(PrimaryModel, BaseInterface):
    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'
    )
    fhrp_group_assignments = GenericRelation(
        to='ipam.FHRPGroupAssignment',
        content_type_field='interface_type',
        object_id_field='interface_id',
        related_query_name='+'
    )

    class Meta:
        verbose_name = '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 clean(self):
        super().clean()

        # Parent validation

        # An interface cannot be its own parent
        if self.pk and self.parent_id == self.pk:
            raise ValidationError({'parent': "An interface cannot be its own parent."})

        # An interface's parent must belong to the same virtual machine
        if self.parent and self.parent.virtual_machine != self.virtual_machine:
            raise ValidationError({
                'parent': f"The selected parent interface ({self.parent}) belongs to a different virtual machine "
                          f"({self.parent.virtual_machine})."
            })

        # Bridge validation

        # An interface cannot be bridged to itself
        if self.pk and self.bridge_id == self.pk:
            raise ValidationError({'bridge': "An interface cannot be bridged to itself."})

        # A bridged interface belong to the same virtual machine
        if self.bridge and self.bridge.virtual_machine != self.virtual_machine:
            raise ValidationError({
                'bridge': f"The selected bridge interface ({self.bridge}) belongs to a different virtual machine "
                          f"({self.bridge.virtual_machine})."
            })

        # VLAN validation

        # 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 super().to_objectchange(action, related_object=self.virtual_machine)

    @property
    def parent_object(self):
        return self.virtual_machine
コード例 #23
0
ファイル: sites.py プロジェクト: Vimpel/netbox-docker-russian
class Site(ChangeLoggedModel, CustomFieldModel):
    """
    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=50,
        unique=True,
        verbose_name='наименование'
    )
    _name = NaturalOrderingField(
        target_field='name',
        max_length=100,
        blank=True
    )
    slug = models.SlugField(
        unique=True
    )
    status = models.CharField(
        max_length=50,
        choices=SiteStatusChoices,
        default=SiteStatusChoices.STATUS_ACTIVE,
        verbose_name='статус'
    )
    region = models.ForeignKey(
        to='dcim.Region',
        on_delete=models.SET_NULL,
        related_name='sites',
        blank=True,
        null=True,
        verbose_name='регион'
    )
    tenant = models.ForeignKey(
        to='tenancy.Tenant',
        on_delete=models.PROTECT,
        related_name='sites',
        blank=True,
        null=True,
        verbose_name='учреждение'
    )
    facility = models.CharField(
        max_length=50,
        blank=True,
        help_text='код региона'
    )
    asn = ASNField(
        blank=True,
        null=True,
        verbose_name='ASN',
        help_text='32-bit автономный номер'
    )
    time_zone = TimeZoneField(
        blank=True,
        verbose_name='часовой пояс'
    )
    description = models.CharField(
        max_length=200,
        blank=True,
        verbose_name='описание'
    )
    physical_address = models.CharField(
        max_length=200,
        blank=True,
        verbose_name='фактический адрес'
    )
    shipping_address = models.CharField(
        max_length=200,
        blank=True,
        verbose_name='адрес доставки'
    )
    latitude = models.DecimalField(
        max_digits=8,
        decimal_places=6,
        blank=True,
        null=True,
        verbose_name='широта',
        help_text='GPS координаты (широта)'
    )
    longitude = models.DecimalField(
        max_digits=9,
        decimal_places=6,
        blank=True,
        null=True,
        verbose_name='долгота',
        help_text='GPS координаты (долгота)'
    )
    contact_name = models.CharField(
        max_length=50,
        blank=True,
        verbose_name='ФИО ответственного'
    )
    contact_phone = models.CharField(
        max_length=20,
        blank=True,
        verbose_name='контактный телефон'
    )
    contact_email = models.EmailField(
        blank=True,
        verbose_name='электронная почта'
    )
    comments = models.TextField(
        blank=True,
        verbose_name='коментарии'
    )
    custom_field_values = GenericRelation(
        to='extras.CustomFieldValue',
        content_type_field='obj_type',
        object_id_field='obj_id'
    )
    images = GenericRelation(
        to='extras.ImageAttachment',
        verbose_name='изображения'
    )
    tags = TaggableManager(through=TaggedItem)

    objects = RestrictedQuerySet.as_manager()

    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',
    ]

    STATUS_CLASS_MAP = {
        SiteStatusChoices.STATUS_PLANNED: 'info',
        SiteStatusChoices.STATUS_STAGING: 'primary',
        SiteStatusChoices.STATUS_ACTIVE: 'success',
        SiteStatusChoices.STATUS_DECOMMISSIONING: 'warning',
        SiteStatusChoices.STATUS_RETIRED: 'danger',
    }

    class Meta:
        ordering = ('_name',)
        verbose_name = 'адрес'
        verbose_name_plural = 'адреса'

    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,
        )

    def get_status_class(self):
        return self.STATUS_CLASS_MAP.get(self.status)
コード例 #24
0
ファイル: device_components.py プロジェクト: vista-/netbox
class Interface(CableTermination, 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)
    _connected_interface = models.OneToOneField(to='self',
                                                on_delete=models.SET_NULL,
                                                related_name='+',
                                                blank=True,
                                                null=True)
    _connected_circuittermination = models.OneToOneField(
        to='circuits.CircuitTermination',
        on_delete=models.SET_NULL,
        related_name='+',
        blank=True,
        null=True)
    connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES,
                                            blank=True,
                                            null=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='OOB Management',
        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')
    tags = TaggableManager(through=TaggedItem)

    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):

        # 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.pk 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)
            })

    def save(self, *args, **kwargs):

        # Remove untagged VLAN assignment for non-802.1Q interfaces
        if self.mode is None:
            self.untagged_vlan = None

        # Only "tagged" interfaces may have tagged VLANs assigned. ("tagged all" implies all VLANs are assigned.)
        if self.pk and self.mode != InterfaceModeChoices.MODE_TAGGED:
            self.tagged_vlans.clear()

        return super().save(*args, **kwargs)

    @property
    def connected_endpoint(self):
        """
        Return the connected Interface, if it exists, or the connected CircuitTermination, if it exists. We have to
        check for ObjectDoesNotExist in case the referenced object has been deleted from the database.
        """
        try:
            if self._connected_interface:
                return self._connected_interface
        except ObjectDoesNotExist:
            pass
        try:
            if self._connected_circuittermination:
                return self._connected_circuittermination
        except ObjectDoesNotExist:
            pass
        return None

    @connected_endpoint.setter
    def connected_endpoint(self, value):
        from circuits.models import CircuitTermination

        if value is None:
            self._connected_interface = None
            self._connected_circuittermination = None
        elif isinstance(value, Interface):
            self._connected_interface = value
            self._connected_circuittermination = None
        elif isinstance(value, CircuitTermination):
            self._connected_interface = None
            self._connected_circuittermination = value
        else:
            raise ValueError(
                "Connected endpoint must be an Interface or CircuitTermination, not {}."
                .format(type(value)))

    @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()
コード例 #25
0
ファイル: device_components.py プロジェクト: ffddorf/netbox
class InventoryItem(ComponentModel):
    """
    An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
    InventoryItems are used only for inventory purposes.
    """
    device = models.ForeignKey(to='dcim.Device',
                               on_delete=models.CASCADE,
                               related_name='inventory_items')
    parent = models.ForeignKey(to='self',
                               on_delete=models.CASCADE,
                               related_name='child_items',
                               blank=True,
                               null=True)
    name = models.CharField(max_length=50, verbose_name='Name')
    _name = NaturalOrderingField(target_field='name',
                                 max_length=100,
                                 blank=True)
    manufacturer = models.ForeignKey(to='dcim.Manufacturer',
                                     on_delete=models.PROTECT,
                                     related_name='inventory_items',
                                     blank=True,
                                     null=True)
    part_id = models.CharField(max_length=50,
                               verbose_name='Part ID',
                               blank=True)
    serial = models.CharField(max_length=50,
                              verbose_name='Serial number',
                              blank=True)
    asset_tag = models.CharField(
        max_length=50,
        unique=True,
        blank=True,
        null=True,
        verbose_name='Asset tag',
        help_text='A unique tag used to identify this item')
    discovered = models.BooleanField(default=False, verbose_name='Discovered')

    tags = TaggableManager(through=TaggedItem)

    csv_headers = [
        'device',
        'name',
        'manufacturer',
        'part_id',
        'serial',
        'asset_tag',
        'discovered',
        'description',
    ]

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

    def __str__(self):
        return self.name

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

    def to_csv(self):
        return (
            self.device.name or '{{{}}}'.format(self.device.pk),
            self.name,
            self.manufacturer.name if self.manufacturer else None,
            self.part_id,
            self.serial,
            self.asset_tag,
            self.discovered,
            self.description,
        )
コード例 #26
0
class Site(PrimaryModel):
    """
    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
    )
    slug = models.SlugField(
        max_length=100,
        unique=True
    )
    status = models.CharField(
        max_length=50,
        choices=SiteStatusChoices,
        default=SiteStatusChoices.STATUS_ACTIVE
    )
    region = models.ForeignKey(
        to='dcim.Region',
        on_delete=models.SET_NULL,
        related_name='sites',
        blank=True,
        null=True
    )
    group = models.ForeignKey(
        to='dcim.SiteGroup',
        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
    )
    vlan_groups = GenericRelation(
        to='ipam.VLANGroup',
        content_type_field='scope_type',
        object_id_field='scope_id',
        related_query_name='site'
    )
    images = GenericRelation(
        to='extras.ImageAttachment'
    )

    objects = RestrictedQuerySet.as_manager()

    clone_fields = [
        'status', 'region', 'group', '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.pk])

    def get_status_class(self):
        return SiteStatusChoices.CSS_CLASSES.get(self.status)
コード例 #27
0
class VirtualMachine(PrimaryModel, ConfigContextModel):
    """
    A virtual machine which runs inside a Cluster.
    """
    cluster = models.ForeignKey(
        to='virtualization.Cluster',
        on_delete=models.PROTECT,
        related_name='virtual_machines'
    )
    tenant = models.ForeignKey(
        to='tenancy.Tenant',
        on_delete=models.PROTECT,
        related_name='virtual_machines',
        blank=True,
        null=True
    )
    platform = models.ForeignKey(
        to='dcim.Platform',
        on_delete=models.SET_NULL,
        related_name='virtual_machines',
        blank=True,
        null=True
    )
    name = models.CharField(
        max_length=64
    )
    _name = NaturalOrderingField(
        target_field='name',
        max_length=100,
        blank=True
    )
    status = models.CharField(
        max_length=50,
        choices=VirtualMachineStatusChoices,
        default=VirtualMachineStatusChoices.STATUS_ACTIVE,
        verbose_name='Status'
    )
    role = models.ForeignKey(
        to='dcim.DeviceRole',
        on_delete=models.PROTECT,
        related_name='virtual_machines',
        limit_choices_to={'vm_role': True},
        blank=True,
        null=True
    )
    primary_ip4 = models.OneToOneField(
        to='ipam.IPAddress',
        on_delete=models.SET_NULL,
        related_name='+',
        blank=True,
        null=True,
        verbose_name='Primary IPv4'
    )
    primary_ip6 = models.OneToOneField(
        to='ipam.IPAddress',
        on_delete=models.SET_NULL,
        related_name='+',
        blank=True,
        null=True,
        verbose_name='Primary IPv6'
    )
    vcpus = models.DecimalField(
        max_digits=6,
        decimal_places=2,
        blank=True,
        null=True,
        verbose_name='vCPUs',
        validators=(
            MinValueValidator(0.01),
        )
    )
    memory = models.PositiveIntegerField(
        blank=True,
        null=True,
        verbose_name='Memory (MB)'
    )
    disk = models.PositiveIntegerField(
        blank=True,
        null=True,
        verbose_name='Disk (GB)'
    )
    comments = models.TextField(
        blank=True
    )
    secrets = GenericRelation(
        to='secrets.Secret',
        content_type_field='assigned_object_type',
        object_id_field='assigned_object_id',
        related_query_name='virtual_machine'
    )

    objects = ConfigContextModelQuerySet.as_manager()

    csv_headers = [
        'name', 'status', 'role', 'cluster', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
    ]
    clone_fields = [
        'cluster', 'tenant', 'platform', 'status', 'role', 'vcpus', 'memory', 'disk',
    ]

    class Meta:
        ordering = ('_name', 'pk')  # Name may be non-unique
        unique_together = [
            ['cluster', 'tenant', 'name']
        ]

    def __str__(self):
        return self.name

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

    def validate_unique(self, exclude=None):

        # Check for a duplicate name on a VM assigned to the same Cluster 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.tenant is None and VirtualMachine.objects.exclude(pk=self.pk).filter(
                name=self.name, cluster=self.cluster, tenant__isnull=True
        ):
            raise ValidationError({
                'name': 'A virtual machine with this name already exists in the assigned cluster.'
            })

        super().validate_unique(exclude)

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

        # Validate primary IP addresses
        interfaces = self.interfaces.all()
        for field in ['primary_ip4', 'primary_ip6']:
            ip = getattr(self, field)
            if ip is not None:
                if ip.assigned_object in interfaces:
                    pass
                elif ip.nat_inside is not None and ip.nat_inside.assigned_object in interfaces:
                    pass
                else:
                    raise ValidationError({
                        field: f"The specified IP address ({ip}) is not assigned to this VM.",
                    })

    def to_csv(self):
        return (
            self.name,
            self.get_status_display(),
            self.role.name if self.role else None,
            self.cluster.name,
            self.tenant.name if self.tenant else None,
            self.platform.name if self.platform else None,
            self.vcpus,
            self.memory,
            self.disk,
            self.comments,
        )

    def get_status_class(self):
        return VirtualMachineStatusChoices.CSS_CLASSES.get(self.status)

    @property
    def primary_ip(self):
        if settings.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

    @property
    def site(self):
        return self.cluster.site
コード例 #28
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='OOB Management',
        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')
    tags = TaggableManager(through=TaggedItem)

    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):

        # 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.pk 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()
コード例 #29
0
class Device(PrimaryModel, ConfigContextModel):
    """
    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'
    )
    location = models.ForeignKey(
        to='dcim.Location',
        on_delete=models.PROTECT,
        related_name='devices',
        blank=True,
        null=True
    )
    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'
    )
    status = models.CharField(
        max_length=50,
        choices=DeviceStatusChoices,
        default=DeviceStatusChoices.STATUS_ACTIVE
    )
    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 = GenericRelation(
        to='secrets.Secret',
        content_type_field='assigned_object_type',
        object_id_field='assigned_object_id',
        related_query_name='device'
    )
    fabric = models.ForeignKey(
        to='netbox_ix_plugin.Fabric',
        on_delete=models.PROTECT,
        related_name='devices'
    )

    objects = ConfigContextModelQuerySet.as_manager()

    csv_headers = [
        'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
        'site', 'location', 'rack_name', 'position', 'face', 'comments',
    ]
    clone_fields = [
        'device_type', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'status', 'cluster',
    ]

    class Meta:
        ordering = ('_name', 'pk')  # 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_name 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/location/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.location and self.site != self.location.site:
            raise ValidationError({
                'location': f"Location {self.location} does not belong to site {self.site}.",
            })
        if self.rack and self.location and self.rack.location != self.location:
            raise ValidationError({
                'rack': f"Rack {self.rack} does not belong to location {self.location}.",
            })
        elif self.rack:
            self.location = self.rack.location

        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.pk 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(if_master=False)
        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 bool(self.pk)

        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.location.name if self.rack and self.rack.location else None,
            self.rack.name if self.rack else None,
            self.position,
            self.get_face_display(),
            self.comments,
        )

    @property
    def display_name(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 settings.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

    @property
    def interfaces_count(self):
        return self.vc_interfaces().count()

    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

    def vc_interfaces(self, if_master=True):
        """
        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.

        :param if_master: If True, return VC member interfaces only if this Device is the VC master.
        """
        filter = Q(device=self)
        if self.virtual_chassis and (self.virtual_chassis.master == self or not if_master):
            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)

    def get_status_class(self):
        return DeviceStatusChoices.CSS_CLASSES.get(self.status)
コード例 #30
0
class Interface(ComponentModel, BaseInterface, LinkTermination, PathEndpoint):
    """
    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'
    )
    wwn = WWNField(
        null=True,
        blank=True,
        verbose_name='WWN',
        help_text='64-bit World Wide Name'
    )
    rf_role = models.CharField(
        max_length=30,
        choices=WirelessRoleChoices,
        blank=True,
        verbose_name='Wireless role'
    )
    rf_channel = models.CharField(
        max_length=50,
        choices=WirelessChannelChoices,
        blank=True,
        verbose_name='Wireless channel'
    )
    rf_channel_frequency = models.DecimalField(
        max_digits=7,
        decimal_places=2,
        blank=True,
        null=True,
        verbose_name='Channel frequency (MHz)'
    )
    rf_channel_width = models.DecimalField(
        max_digits=7,
        decimal_places=3,
        blank=True,
        null=True,
        verbose_name='Channel width (MHz)'
    )
    tx_power = models.PositiveSmallIntegerField(
        blank=True,
        null=True,
        validators=(MaxValueValidator(127),),
        verbose_name='Transmit power (dBm)'
    )
    wireless_link = models.ForeignKey(
        to='wireless.WirelessLink',
        on_delete=models.SET_NULL,
        related_name='+',
        blank=True,
        null=True
    )
    wireless_lans = models.ManyToManyField(
        to='wireless.WirelessLAN',
        related_name='interfaces',
        blank=True,
        verbose_name='Wireless LANs'
    )
    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'
    )
    fhrp_group_assignments = GenericRelation(
        to='ipam.FHRPGroupAssignment',
        content_type_field='interface_type',
        object_id_field='interface_id',
        related_query_name='+'
    )

    clone_fields = ['device', 'parent', 'bridge', 'lag', 'type', 'mgmt_only']

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

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

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

        # Virtual Interfaces cannot have a Cable attached
        if self.is_virtual and self.cable:
            raise ValidationError({
                'type': f"{self.get_type_display()} interfaces cannot have a cable attached."
            })

        # Virtual Interfaces cannot be marked as connected
        if self.is_virtual and self.mark_connected:
            raise ValidationError({
                'mark_connected': f"{self.get_type_display()} interfaces cannot be marked as connected."
            })

        # Parent validation

        # An interface cannot be its own parent
        if self.pk and self.parent_id == self.pk:
            raise ValidationError({'parent': "An interface cannot be its own parent."})

        # A physical interface cannot have a parent interface
        if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None:
            raise ValidationError({'parent': "Only virtual interfaces may be assigned to a parent interface."})

        # An interface's parent must belong to the same device or virtual chassis
        if self.parent and self.parent.device != self.device:
            if self.device.virtual_chassis is None:
                raise ValidationError({
                    'parent': f"The selected parent interface ({self.parent}) belongs to a different device "
                              f"({self.parent.device})."
                })
            elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
                raise ValidationError({
                    'parent': f"The selected parent interface ({self.parent}) belongs to {self.parent.device}, which "
                              f"is not part of virtual chassis {self.device.virtual_chassis}."
                })

        # Bridge validation

        # An interface cannot be bridged to itself
        if self.pk and self.bridge_id == self.pk:
            raise ValidationError({'bridge': "An interface cannot be bridged to itself."})

        # A bridged interface belong to the same device or virtual chassis
        if self.bridge and self.bridge.device != self.device:
            if self.device.virtual_chassis is None:
                raise ValidationError({
                    'bridge': f"The selected bridge interface ({self.bridge}) belongs to a different device "
                              f"({self.bridge.device})."
                })
            elif self.bridge.device.virtual_chassis != self.device.virtual_chassis:
                raise ValidationError({
                    'bridge': f"The selected bridge interface ({self.bridge}) belongs to {self.bridge.device}, which "
                              f"is not part of virtual chassis {self.device.virtual_chassis}."
                })

        # LAG validation

        # 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.pk and self.lag_id == self.pk:
            raise ValidationError({'lag': "A LAG interface cannot be its own parent."})

        # 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}."
                })

        # Wireless validation

        # RF role & channel may only be set for wireless interfaces
        if self.rf_role and not self.is_wireless:
            raise ValidationError({'rf_role': "Wireless role may be set only on wireless interfaces."})
        if self.rf_channel and not self.is_wireless:
            raise ValidationError({'rf_channel': "Channel may be set only on wireless interfaces."})

        # Validate channel frequency against interface type and selected channel (if any)
        if self.rf_channel_frequency:
            if not self.is_wireless:
                raise ValidationError({
                    'rf_channel_frequency': "Channel frequency may be set only on wireless interfaces.",
                })
            if self.rf_channel and self.rf_channel_frequency != get_channel_attr(self.rf_channel, 'frequency'):
                raise ValidationError({
                    'rf_channel_frequency': "Cannot specify custom frequency with channel selected.",
                })
        elif self.rf_channel:
            self.rf_channel_frequency = get_channel_attr(self.rf_channel, 'frequency')

        # Validate channel width against interface type and selected channel (if any)
        if self.rf_channel_width:
            if not self.is_wireless:
                raise ValidationError({'rf_channel_width': "Channel width may be set only on wireless interfaces."})
            if self.rf_channel and self.rf_channel_width != get_channel_attr(self.rf_channel, 'width'):
                raise ValidationError({'rf_channel_width': "Cannot specify custom width with channel selected."})
        elif self.rf_channel:
            self.rf_channel_width = get_channel_attr(self.rf_channel, 'width')

        # VLAN validation

        # Validate untagged VLAN
        if self.untagged_vlan and self.untagged_vlan.site not in [self.device.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 device, or it must be global."
            })

    @property
    def _occupied(self):
        return super()._occupied or bool(self.wireless_link_id)

    @property
    def is_wired(self):
        return not self.is_virtual and not self.is_wireless

    @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 link(self):
        return self.cable or self.wireless_link