Fixes #20584: Ensure consistent validation between Interface & InterfaceTemplate (#20589)

This commit is contained in:
Jeremy Stretch 2025-10-15 14:04:39 -04:00 committed by GitHub
parent c902a1c510
commit addda0538f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 45 additions and 34 deletions

View File

@ -7,6 +7,7 @@ from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models.mixins import InterfaceValidationMixin
from netbox.models import ChangeLoggedModel from netbox.models import ChangeLoggedModel
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.mptt import TreeManager from utilities.mptt import TreeManager
@ -405,7 +406,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
} }
class InterfaceTemplate(ModularComponentTemplateModel): class InterfaceTemplate(InterfaceValidationMixin, ModularComponentTemplateModel):
""" """
A template for a physical data interface on a new Device. A template for a physical data interface on a new Device.
""" """
@ -469,8 +470,6 @@ class InterfaceTemplate(ModularComponentTemplateModel):
super().clean() super().clean()
if self.bridge: if self.bridge:
if self.pk and self.bridge_id == self.pk:
raise ValidationError({'bridge': _("An interface cannot be bridged to itself.")})
if self.device_type and self.device_type != self.bridge.device_type: if self.device_type and self.device_type != self.bridge.device_type:
raise ValidationError({ raise ValidationError({
'bridge': _( 'bridge': _(
@ -484,11 +483,6 @@ class InterfaceTemplate(ModularComponentTemplateModel):
).format(bridge=self.bridge) ).format(bridge=self.bridge)
}) })
if self.rf_role and self.type not in WIRELESS_IFACE_TYPES:
raise ValidationError({
'rf_role': "Wireless role may be set only on wireless interfaces."
})
def instantiate(self, **kwargs): def instantiate(self, **kwargs):
return self.component_model( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),

View File

@ -11,6 +11,7 @@ from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.fields import WWNField from dcim.fields import WWNField
from dcim.models.mixins import InterfaceValidationMixin
from netbox.choices import ColorChoices from netbox.choices import ColorChoices
from netbox.models import OrganizationalModel, NetBoxModel from netbox.models import OrganizationalModel, NetBoxModel
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
@ -676,7 +677,14 @@ class BaseInterface(models.Model):
return self.primary_mac_address.mac_address return self.primary_mac_address.mac_address
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint, TrackingModelMixin): class Interface(
InterfaceValidationMixin,
ModularComponentModel,
BaseInterface,
CabledObjectModel,
PathEndpoint,
TrackingModelMixin,
):
""" """
A network interface within a Device. A physical Interface can connect to exactly one other Interface. A network interface within a Device. A physical Interface can connect to exactly one other Interface.
""" """
@ -893,10 +901,6 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
# Bridge validation # 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 belongs to the same device or virtual chassis # A bridged interface belongs to the same device or virtual chassis
if self.bridge and self.bridge.device != self.device: if self.bridge and self.bridge.device != self.device:
if self.device.virtual_chassis is None: if self.device.virtual_chassis is None:
@ -942,29 +946,9 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
) )
}) })
# PoE validation
# Only physical interfaces may have a PoE mode/type assigned
if self.poe_mode and self.is_virtual:
raise ValidationError({
'poe_mode': _("Virtual interfaces cannot have a PoE mode.")
})
if self.poe_type and self.is_virtual:
raise ValidationError({
'poe_type': _("Virtual interfaces cannot have a PoE type.")
})
# An interface with a PoE type set must also specify a mode
if self.poe_type and not self.poe_mode:
raise ValidationError({
'poe_type': _("Must specify PoE mode when designating a PoE type.")
})
# Wireless validation # Wireless validation
# RF role & channel may only be set for wireless interfaces # RF 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: if self.rf_channel and not self.is_wireless:
raise ValidationError({'rf_channel': _("Channel may be set only on wireless interfaces.")}) raise ValidationError({'rf_channel': _("Channel may be set only on wireless interfaces.")})

View File

@ -4,8 +4,11 @@ from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from dcim.constants import VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES
__all__ = ( __all__ = (
'CachedScopeMixin', 'CachedScopeMixin',
'InterfaceValidationMixin',
'RenderConfigMixin', 'RenderConfigMixin',
) )
@ -116,3 +119,33 @@ class CachedScopeMixin(models.Model):
self._site = self.scope.site self._site = self.scope.site
self._location = self.scope self._location = self.scope
cache_related_objects.alters_data = True cache_related_objects.alters_data = True
class InterfaceValidationMixin:
def clean(self):
super().clean()
# 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.")})
# Only physical interfaces may have a PoE mode/type assigned
if self.poe_mode and self.type in VIRTUAL_IFACE_TYPES:
raise ValidationError({
'poe_mode': _("Virtual interfaces cannot have a PoE mode.")
})
if self.poe_type and self.type in VIRTUAL_IFACE_TYPES:
raise ValidationError({
'poe_type': _("Virtual interfaces cannot have a PoE type.")
})
# An interface with a PoE type set must also specify a mode
if self.poe_type and not self.poe_mode:
raise ValidationError({
'poe_type': _("Must specify PoE mode when designating a PoE type.")
})
# RF role may be set only for wireless interfaces
if self.rf_role and self.type not in WIRELESS_IFACE_TYPES:
raise ValidationError({'rf_role': _("Wireless role may be set only on wireless interfaces.")})