mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-21 00:11:39 +02:00
61
netbox/dcim/models/base.py
Normal file
61
netbox/dcim/models/base.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.constants import PORT_POSITION_MAX, PORT_POSITION_MIN
|
||||
|
||||
__all__ = (
|
||||
'PortMappingBase',
|
||||
)
|
||||
|
||||
|
||||
class PortMappingBase(models.Model):
|
||||
"""
|
||||
Base class for PortMapping and PortTemplateMapping
|
||||
"""
|
||||
front_port_position = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=(
|
||||
MinValueValidator(PORT_POSITION_MIN),
|
||||
MaxValueValidator(PORT_POSITION_MAX),
|
||||
),
|
||||
)
|
||||
rear_port_position = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=(
|
||||
MinValueValidator(PORT_POSITION_MIN),
|
||||
MaxValueValidator(PORT_POSITION_MAX),
|
||||
),
|
||||
)
|
||||
|
||||
_netbox_private = True
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('front_port', 'front_port_position'),
|
||||
name='%(app_label)s_%(class)s_unique_front_port_position'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('rear_port', 'rear_port_position'),
|
||||
name='%(app_label)s_%(class)s_unique_rear_port_position'
|
||||
),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate rear port position
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError({
|
||||
"rear_port_position": _(
|
||||
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
|
||||
"positions."
|
||||
).format(
|
||||
rear_port_position=self.rear_port_position,
|
||||
name=self.rear_port.name,
|
||||
positions=self.rear_port.positions
|
||||
)
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -22,7 +23,7 @@ from utilities.fields import ColorField, GenericArrayForeignKey
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.serialization import deserialize_object, serialize_object
|
||||
from wireless.models import WirelessLink
|
||||
from .device_components import FrontPort, PathEndpoint, RearPort
|
||||
from .device_components import FrontPort, PathEndpoint, PortMapping, RearPort
|
||||
|
||||
__all__ = (
|
||||
'Cable',
|
||||
@@ -30,6 +31,8 @@ __all__ = (
|
||||
'CableTermination',
|
||||
)
|
||||
|
||||
logger = logging.getLogger(f'netbox.{__name__}')
|
||||
|
||||
trace_paths = Signal()
|
||||
|
||||
|
||||
@@ -666,7 +669,13 @@ class CablePath(models.Model):
|
||||
is_active = True
|
||||
is_split = False
|
||||
|
||||
logger.debug(f'Tracing cable path from {terminations}...')
|
||||
|
||||
segment = 0
|
||||
while terminations:
|
||||
segment += 1
|
||||
logger.debug(f'[Path segment #{segment}] Position stack: {position_stack}')
|
||||
logger.debug(f'[Path segment #{segment}] Local terminations: {terminations}')
|
||||
|
||||
# Terminations must all be of the same type
|
||||
if not all(isinstance(t, type(terminations[0])) for t in terminations[1:]):
|
||||
@@ -697,7 +706,10 @@ class CablePath(models.Model):
|
||||
position_stack.append([terminations[0].cable_position])
|
||||
|
||||
# Step 2: Determine the attached links (Cable or WirelessLink), if any
|
||||
links = [termination.link for termination in terminations if termination.link is not None]
|
||||
links = list(dict.fromkeys(
|
||||
termination.link for termination in terminations if termination.link is not None
|
||||
))
|
||||
logger.debug(f'[Path segment #{segment}] Links: {links}')
|
||||
if len(links) == 0:
|
||||
if len(path) == 1:
|
||||
# If this is the start of the path and no link exists, return None
|
||||
@@ -760,10 +772,13 @@ class CablePath(models.Model):
|
||||
link.interface_b if link.interface_a is terminations[0] else link.interface_a for link in links
|
||||
]
|
||||
|
||||
logger.debug(f'[Path segment #{segment}] Remote terminations: {remote_terminations}')
|
||||
|
||||
# Remote Terminations must all be of the same type, otherwise return a split path
|
||||
if not all(isinstance(t, type(remote_terminations[0])) for t in remote_terminations[1:]):
|
||||
is_complete = False
|
||||
is_split = True
|
||||
logger.debug('Remote termination types differ; aborting trace.')
|
||||
break
|
||||
|
||||
# Step 7: Record the far-end termination object(s)
|
||||
@@ -777,58 +792,53 @@ class CablePath(models.Model):
|
||||
|
||||
if isinstance(remote_terminations[0], FrontPort):
|
||||
# Follow FrontPorts to their corresponding RearPorts
|
||||
rear_ports = RearPort.objects.filter(
|
||||
pk__in=[t.rear_port_id for t in remote_terminations]
|
||||
)
|
||||
if len(rear_ports) > 1 or rear_ports[0].positions > 1:
|
||||
position_stack.append([fp.rear_port_position for fp in remote_terminations])
|
||||
|
||||
terminations = rear_ports
|
||||
|
||||
elif isinstance(remote_terminations[0], RearPort):
|
||||
if len(remote_terminations) == 1 and remote_terminations[0].positions == 1:
|
||||
front_ports = FrontPort.objects.filter(
|
||||
rear_port_id__in=[rp.pk for rp in remote_terminations],
|
||||
rear_port_position=1
|
||||
)
|
||||
# Obtain the individual front ports based on the termination and all positions
|
||||
elif len(remote_terminations) > 1 and position_stack:
|
||||
if remote_terminations[0].positions > 1 and position_stack:
|
||||
positions = position_stack.pop()
|
||||
|
||||
# Ensure we have a number of positions equal to the amount of remote terminations
|
||||
if len(remote_terminations) != len(positions):
|
||||
raise UnsupportedCablePath(
|
||||
_("All positions counts within the path on opposite ends of links must match")
|
||||
)
|
||||
|
||||
# Get our front ports
|
||||
q_filter = Q()
|
||||
for rt in remote_terminations:
|
||||
position = positions.pop()
|
||||
q_filter |= Q(rear_port_id=rt.pk, rear_port_position=position)
|
||||
if q_filter is Q():
|
||||
raise UnsupportedCablePath(_("Remote termination position filter is missing"))
|
||||
front_ports = FrontPort.objects.filter(q_filter)
|
||||
# Obtain the individual front ports based on the termination and position
|
||||
elif position_stack:
|
||||
front_ports = FrontPort.objects.filter(
|
||||
rear_port_id=remote_terminations[0].pk,
|
||||
rear_port_position__in=position_stack.pop()
|
||||
)
|
||||
# If all rear ports have a single position, we can just get the front ports
|
||||
elif all([rp.positions == 1 for rp in remote_terminations]):
|
||||
front_ports = FrontPort.objects.filter(rear_port_id__in=[rp.pk for rp in remote_terminations])
|
||||
|
||||
if len(front_ports) != len(remote_terminations):
|
||||
# Some rear ports does not have a front port
|
||||
is_split = True
|
||||
break
|
||||
else:
|
||||
# No position indicated: path has split, so we stop at the RearPorts
|
||||
q_filter |= Q(front_port=rt, front_port_position__in=positions)
|
||||
port_mappings = PortMapping.objects.filter(q_filter)
|
||||
elif remote_terminations[0].positions > 1:
|
||||
is_split = True
|
||||
logger.debug(
|
||||
'Encountered front port mapped to multiple rear ports but position stack is empty; aborting '
|
||||
'trace.'
|
||||
)
|
||||
break
|
||||
else:
|
||||
port_mappings = PortMapping.objects.filter(front_port__in=remote_terminations)
|
||||
if not port_mappings:
|
||||
break
|
||||
|
||||
terminations = front_ports
|
||||
# Compile the list of RearPorts without duplication or altering their ordering
|
||||
terminations = list(dict.fromkeys(mapping.rear_port for mapping in port_mappings))
|
||||
if any(t.positions > 1 for t in terminations):
|
||||
position_stack.append([mapping.rear_port_position for mapping in port_mappings])
|
||||
|
||||
elif isinstance(remote_terminations[0], RearPort):
|
||||
# Follow RearPorts to their corresponding FrontPorts
|
||||
if remote_terminations[0].positions > 1 and position_stack:
|
||||
positions = position_stack.pop()
|
||||
q_filter = Q()
|
||||
for rt in remote_terminations:
|
||||
q_filter |= Q(rear_port=rt, rear_port_position__in=positions)
|
||||
port_mappings = PortMapping.objects.filter(q_filter)
|
||||
elif remote_terminations[0].positions > 1:
|
||||
is_split = True
|
||||
logger.debug(
|
||||
'Encountered rear port mapped to multiple front ports but position stack is empty; aborting '
|
||||
'trace.'
|
||||
)
|
||||
break
|
||||
else:
|
||||
port_mappings = PortMapping.objects.filter(rear_port__in=remote_terminations)
|
||||
if not port_mappings:
|
||||
break
|
||||
|
||||
# Compile the list of FrontPorts without duplication or altering their ordering
|
||||
terminations = list(dict.fromkeys(mapping.front_port for mapping in port_mappings))
|
||||
if any(t.positions > 1 for t in terminations):
|
||||
position_stack.append([mapping.front_port_position for mapping in port_mappings])
|
||||
|
||||
elif isinstance(remote_terminations[0], CircuitTermination):
|
||||
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
|
||||
@@ -876,6 +886,7 @@ class CablePath(models.Model):
|
||||
# Unsupported topology, mark as split and exit
|
||||
is_complete = False
|
||||
is_split = True
|
||||
logger.warning('Encountered an unsupported topology; aborting trace.')
|
||||
break
|
||||
|
||||
return cls(
|
||||
@@ -954,16 +965,23 @@ class CablePath(models.Model):
|
||||
|
||||
# RearPort splitting to multiple FrontPorts with no stack position
|
||||
if type(nodes[0]) is RearPort:
|
||||
return FrontPort.objects.filter(rear_port__in=nodes)
|
||||
return [
|
||||
mapping.front_port for mapping in
|
||||
PortMapping.objects.filter(rear_port__in=nodes).prefetch_related('front_port')
|
||||
]
|
||||
# Cable terminating to multiple FrontPorts mapped to different
|
||||
# RearPorts connected to different cables
|
||||
elif type(nodes[0]) is FrontPort:
|
||||
return RearPort.objects.filter(pk__in=[fp.rear_port_id for fp in nodes])
|
||||
if type(nodes[0]) is FrontPort:
|
||||
return [
|
||||
mapping.rear_port for mapping in
|
||||
PortMapping.objects.filter(front_port__in=nodes).prefetch_related('rear_port')
|
||||
]
|
||||
# Cable terminating to multiple CircuitTerminations
|
||||
elif type(nodes[0]) is CircuitTermination:
|
||||
if type(nodes[0]) is CircuitTermination:
|
||||
return [
|
||||
ct.get_peer_termination() for ct in nodes
|
||||
]
|
||||
return []
|
||||
|
||||
def get_asymmetric_nodes(self):
|
||||
"""
|
||||
|
||||
@@ -7,6 +7,7 @@ from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.models.base import PortMappingBase
|
||||
from dcim.models.mixins import InterfaceValidationMixin
|
||||
from netbox.models import ChangeLoggedModel
|
||||
from utilities.fields import ColorField, NaturalOrderingField
|
||||
@@ -28,6 +29,7 @@ __all__ = (
|
||||
'InterfaceTemplate',
|
||||
'InventoryItemTemplate',
|
||||
'ModuleBayTemplate',
|
||||
'PortTemplateMapping',
|
||||
'PowerOutletTemplate',
|
||||
'PowerPortTemplate',
|
||||
'RearPortTemplate',
|
||||
@@ -518,6 +520,53 @@ class InterfaceTemplate(InterfaceValidationMixin, ModularComponentTemplateModel)
|
||||
}
|
||||
|
||||
|
||||
class PortTemplateMapping(PortMappingBase):
|
||||
"""
|
||||
Maps a FrontPortTemplate & position to a RearPortTemplate & position.
|
||||
"""
|
||||
device_type = models.ForeignKey(
|
||||
to='dcim.DeviceType',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='port_mappings',
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
module_type = models.ForeignKey(
|
||||
to='dcim.ModuleType',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='port_mappings',
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
front_port = models.ForeignKey(
|
||||
to='dcim.FrontPortTemplate',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='mappings',
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
to='dcim.RearPortTemplate',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='mappings',
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate rear port assignment
|
||||
if self.front_port.device_type_id != self.rear_port.device_type_id:
|
||||
raise ValidationError({
|
||||
"rear_port": _("Rear port ({rear_port}) must belong to the same device type").format(
|
||||
rear_port=self.rear_port
|
||||
)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Associate the mapping with the parent DeviceType/ModuleType
|
||||
self.device_type = self.front_port.device_type
|
||||
self.module_type = self.front_port.module_type
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
"""
|
||||
Template for a pass-through port on the front of a new Device.
|
||||
@@ -531,18 +580,13 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
to='dcim.RearPortTemplate',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='frontport_templates'
|
||||
)
|
||||
rear_port_position = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('rear port position'),
|
||||
positions = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('positions'),
|
||||
default=1,
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
]
|
||||
MinValueValidator(PORT_POSITION_MIN),
|
||||
MaxValueValidator(PORT_POSITION_MAX)
|
||||
],
|
||||
)
|
||||
|
||||
component_model = FrontPort
|
||||
@@ -557,10 +601,6 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
fields=('module_type', 'name'),
|
||||
name='%(app_label)s_%(class)s_unique_module_type_name'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('rear_port', 'rear_port_position'),
|
||||
name='%(app_label)s_%(class)s_unique_rear_port_position'
|
||||
),
|
||||
)
|
||||
verbose_name = _('front port template')
|
||||
verbose_name_plural = _('front port templates')
|
||||
@@ -568,40 +608,23 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
try:
|
||||
|
||||
# Validate rear port assignment
|
||||
if self.rear_port.device_type != self.device_type:
|
||||
raise ValidationError(
|
||||
_("Rear port ({name}) must belong to the same device type").format(name=self.rear_port)
|
||||
)
|
||||
|
||||
# Validate rear port position assignment
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError(
|
||||
_("Invalid rear port position ({position}); rear port {name} has only {count} positions").format(
|
||||
position=self.rear_port_position,
|
||||
name=self.rear_port.name,
|
||||
count=self.rear_port.positions
|
||||
)
|
||||
)
|
||||
|
||||
except RearPortTemplate.DoesNotExist:
|
||||
pass
|
||||
# Check that positions is greater than or equal to the number of associated RearPortTemplates
|
||||
if not self._state.adding:
|
||||
mapping_count = self.mappings.count()
|
||||
if self.positions < mapping_count:
|
||||
raise ValidationError({
|
||||
"positions": _(
|
||||
"The number of positions cannot be less than the number of mapped rear port templates ({count})"
|
||||
).format(count=mapping_count)
|
||||
})
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
if self.rear_port:
|
||||
rear_port_name = self.rear_port.resolve_name(kwargs.get('module'))
|
||||
rear_port = RearPort.objects.get(name=rear_port_name, **kwargs)
|
||||
else:
|
||||
rear_port = None
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
label=self.resolve_label(kwargs.get('module')),
|
||||
type=self.type,
|
||||
color=self.color,
|
||||
rear_port=rear_port,
|
||||
rear_port_position=self.rear_port_position,
|
||||
positions=self.positions,
|
||||
**kwargs
|
||||
)
|
||||
instantiate.do_not_call_in_templates = True
|
||||
@@ -611,8 +634,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
|
||||
'name': self.name,
|
||||
'type': self.type,
|
||||
'color': self.color,
|
||||
'rear_port': self.rear_port.name,
|
||||
'rear_port_position': self.rear_port_position,
|
||||
'positions': self.positions,
|
||||
'label': self.label,
|
||||
'description': self.description,
|
||||
}
|
||||
@@ -635,9 +657,9 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
||||
verbose_name=_('positions'),
|
||||
default=1,
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
]
|
||||
MinValueValidator(PORT_POSITION_MIN),
|
||||
MaxValueValidator(PORT_POSITION_MAX)
|
||||
],
|
||||
)
|
||||
|
||||
component_model = RearPort
|
||||
@@ -646,6 +668,20 @@ class RearPortTemplate(ModularComponentTemplateModel):
|
||||
verbose_name = _('rear port template')
|
||||
verbose_name_plural = _('rear port templates')
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check that positions is greater than or equal to the number of associated FrontPortTemplates
|
||||
if not self._state.adding:
|
||||
mapping_count = self.mappings.count()
|
||||
if self.positions < mapping_count:
|
||||
raise ValidationError({
|
||||
"positions": _(
|
||||
"The number of positions cannot be less than the number of mapped front port templates "
|
||||
"({count})"
|
||||
).format(count=mapping_count)
|
||||
})
|
||||
|
||||
def instantiate(self, **kwargs):
|
||||
return self.component_model(
|
||||
name=self.resolve_name(kwargs.get('module')),
|
||||
|
||||
@@ -11,6 +11,7 @@ from mptt.models import MPTTModel, TreeForeignKey
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.fields import WWNField
|
||||
from dcim.models.base import PortMappingBase
|
||||
from dcim.models.mixins import InterfaceValidationMixin
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.models import OrganizationalModel, NetBoxModel
|
||||
@@ -35,6 +36,7 @@ __all__ = (
|
||||
'InventoryItemRole',
|
||||
'ModuleBay',
|
||||
'PathEndpoint',
|
||||
'PortMapping',
|
||||
'PowerOutlet',
|
||||
'PowerPort',
|
||||
'RearPort',
|
||||
@@ -208,10 +210,6 @@ class CabledObjectModel(models.Model):
|
||||
raise ValidationError({
|
||||
"cable_end": _("Must specify cable end (A or B) when attaching a cable.")
|
||||
})
|
||||
if not self.cable_position:
|
||||
raise ValidationError({
|
||||
"cable_position": _("Must specify cable termination position when attaching a cable.")
|
||||
})
|
||||
if self.cable_end and not self.cable:
|
||||
raise ValidationError({
|
||||
"cable_end": _("Cable end must not be set without a cable.")
|
||||
@@ -1069,6 +1067,43 @@ class Interface(
|
||||
# Pass-through ports
|
||||
#
|
||||
|
||||
class PortMapping(PortMappingBase):
|
||||
"""
|
||||
Maps a FrontPort & position to a RearPort & position.
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='port_mappings',
|
||||
)
|
||||
front_port = models.ForeignKey(
|
||||
to='dcim.FrontPort',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='mappings',
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
to='dcim.RearPort',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='mappings',
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Both ports must belong to the same device
|
||||
if self.front_port.device_id != self.rear_port.device_id:
|
||||
raise ValidationError({
|
||||
"rear_port": _("Rear port ({rear_port}) must belong to the same device").format(
|
||||
rear_port=self.rear_port
|
||||
)
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Associate the mapping with the parent Device
|
||||
self.device = self.front_port.device
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
"""
|
||||
A pass-through port on the front of a Device.
|
||||
@@ -1082,22 +1117,16 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
verbose_name=_('color'),
|
||||
blank=True
|
||||
)
|
||||
rear_port = models.ForeignKey(
|
||||
to='dcim.RearPort',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='frontports'
|
||||
)
|
||||
rear_port_position = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('rear port position'),
|
||||
positions = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('positions'),
|
||||
default=1,
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
MinValueValidator(PORT_POSITION_MIN),
|
||||
MaxValueValidator(PORT_POSITION_MAX)
|
||||
],
|
||||
help_text=_('Mapped position on corresponding rear port')
|
||||
)
|
||||
|
||||
clone_fields = ('device', 'type', 'color')
|
||||
clone_fields = ('device', 'type', 'color', 'positions')
|
||||
|
||||
class Meta(ModularComponentModel.Meta):
|
||||
constraints = (
|
||||
@@ -1105,10 +1134,6 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
fields=('device', 'name'),
|
||||
name='%(app_label)s_%(class)s_unique_device_name'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('rear_port', 'rear_port_position'),
|
||||
name='%(app_label)s_%(class)s_unique_rear_port_position'
|
||||
),
|
||||
)
|
||||
verbose_name = _('front port')
|
||||
verbose_name_plural = _('front ports')
|
||||
@@ -1116,27 +1141,14 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if hasattr(self, 'rear_port'):
|
||||
|
||||
# Validate rear port assignment
|
||||
if self.rear_port.device != self.device:
|
||||
# Check that positions is greater than or equal to the number of associated RearPorts
|
||||
if not self._state.adding:
|
||||
mapping_count = self.mappings.count()
|
||||
if self.positions < mapping_count:
|
||||
raise ValidationError({
|
||||
"rear_port": _(
|
||||
"Rear port ({rear_port}) must belong to the same device"
|
||||
).format(rear_port=self.rear_port)
|
||||
})
|
||||
|
||||
# Validate rear port position assignment
|
||||
if self.rear_port_position > self.rear_port.positions:
|
||||
raise ValidationError({
|
||||
"rear_port_position": _(
|
||||
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
|
||||
"positions."
|
||||
).format(
|
||||
rear_port_position=self.rear_port_position,
|
||||
name=self.rear_port.name,
|
||||
positions=self.rear_port.positions
|
||||
)
|
||||
"positions": _(
|
||||
"The number of positions cannot be less than the number of mapped rear ports ({count})"
|
||||
).format(count=mapping_count)
|
||||
})
|
||||
|
||||
|
||||
@@ -1157,11 +1169,11 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
verbose_name=_('positions'),
|
||||
default=1,
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
MinValueValidator(PORT_POSITION_MIN),
|
||||
MaxValueValidator(PORT_POSITION_MAX)
|
||||
],
|
||||
help_text=_('Number of front ports which may be mapped')
|
||||
)
|
||||
|
||||
clone_fields = ('device', 'type', 'color', 'positions')
|
||||
|
||||
class Meta(ModularComponentModel.Meta):
|
||||
@@ -1173,13 +1185,13 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
|
||||
|
||||
# Check that positions count is greater than or equal to the number of associated FrontPorts
|
||||
if not self._state.adding:
|
||||
frontport_count = self.frontports.count()
|
||||
if self.positions < frontport_count:
|
||||
mapping_count = self.mappings.count()
|
||||
if self.positions < mapping_count:
|
||||
raise ValidationError({
|
||||
"positions": _(
|
||||
"The number of positions cannot be less than the number of mapped front ports "
|
||||
"({frontport_count})"
|
||||
).format(frontport_count=frontport_count)
|
||||
"({count})"
|
||||
).format(count=mapping_count)
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import decimal
|
||||
import yaml
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
import yaml
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -19,14 +18,14 @@ from django.utils.translation import gettext_lazy as _
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.fields import MACAddressField
|
||||
from dcim.utils import update_interface_bridges
|
||||
from dcim.utils import create_port_mappings, update_interface_bridges
|
||||
from extras.models import ConfigContextModel, CustomField
|
||||
from extras.querysets import ConfigContextModelQuerySet
|
||||
from netbox.choices import ColorChoices
|
||||
from netbox.config import ConfigItem
|
||||
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models.mixins import WeightMixin
|
||||
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||
from netbox.models.mixins import WeightMixin
|
||||
from utilities.fields import ColorField, CounterCacheField
|
||||
from utilities.prefetch import get_prefetchable_fields
|
||||
from utilities.tracking import TrackingModelMixin
|
||||
@@ -34,7 +33,6 @@ from .device_components import *
|
||||
from .mixins import RenderConfigMixin
|
||||
from .modules import Module
|
||||
|
||||
|
||||
__all__ = (
|
||||
'Device',
|
||||
'DeviceRole',
|
||||
@@ -1009,6 +1007,8 @@ class Device(
|
||||
self._instantiate_components(self.device_type.interfacetemplates.all())
|
||||
self._instantiate_components(self.device_type.rearporttemplates.all())
|
||||
self._instantiate_components(self.device_type.frontporttemplates.all())
|
||||
# Replicate any front/rear port mappings from the DeviceType
|
||||
create_port_mappings(self, self.device_type)
|
||||
# Disable bulk_create to accommodate MPTT
|
||||
self._instantiate_components(self.device_type.modulebaytemplates.all(), bulk_create=False)
|
||||
self._instantiate_components(self.device_type.devicebaytemplates.all())
|
||||
|
||||
Reference in New Issue
Block a user