mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-19 03:42:25 -06:00
Replicate front/rear port assignments from DeviceType
This commit is contained in:
@@ -243,13 +243,13 @@ class FrontPortTemplateSerializer(ComponentTemplateSerializer):
|
||||
default=None
|
||||
)
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
rear_port = RearPortTemplateSerializer(nested=True)
|
||||
rear_ports = RearPortTemplateSerializer(nested=True, many=True)
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
|
||||
'rear_port', 'rear_port_position', 'description', 'created', 'last_updated',
|
||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
|
||||
'rear_ports', 'description', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.constants import LOCATION_SCOPE_TYPES
|
||||
from dcim.models import Site
|
||||
from dcim.models import PortAssignmentTemplate, Site
|
||||
from utilities.forms import get_field_value
|
||||
from utilities.forms.fields import (
|
||||
ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField,
|
||||
@@ -13,6 +13,7 @@ from utilities.templatetags.builtins.filters import bettertitle
|
||||
from utilities.forms.widgets import HTMXSelect
|
||||
|
||||
__all__ = (
|
||||
'FrontPortFormMixin',
|
||||
'ScopedBulkEditForm',
|
||||
'ScopedForm',
|
||||
'ScopedImportForm',
|
||||
@@ -128,3 +129,51 @@ class ScopedImportForm(forms.Form):
|
||||
"Please select a {scope_type}."
|
||||
).format(scope_type=scope_type.model_class()._meta.model_name)
|
||||
})
|
||||
|
||||
|
||||
class FrontPortFormMixin(forms.Form):
|
||||
rear_ports = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label=_('Rear ports'),
|
||||
widget=forms.SelectMultiple(attrs={'size': 8})
|
||||
)
|
||||
|
||||
port_assignment_model = PortAssignmentTemplate
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# FrontPort with no positions cannot be mapped to more than one RearPort
|
||||
if not self.cleaned_data['positions'] and len(self.cleaned_data['rear_ports']) > 1:
|
||||
raise forms.ValidationError({
|
||||
'positions': _("A front port with no positions cannot be mapped to multiple rear ports.")
|
||||
})
|
||||
|
||||
# Count of selected rear port & position pairs much match the assigned number of positions
|
||||
if len(self.cleaned_data['rear_ports']) != self.cleaned_data['positions']:
|
||||
raise forms.ValidationError({
|
||||
'rear_ports': _(
|
||||
"The number of rear port/position pairs selected must match the number of positions assigned."
|
||||
)
|
||||
})
|
||||
|
||||
def _save_m2m(self):
|
||||
super()._save_m2m()
|
||||
|
||||
# TODO: Can this be made more efficient?
|
||||
# Delete existing rear port assignments
|
||||
self.port_assignment_model.objects.filter(front_port_id=self.instance.pk).delete()
|
||||
|
||||
# Create new rear port assignments
|
||||
assignments = []
|
||||
for i, rp_position in enumerate(self.cleaned_data['rear_ports'], start=1):
|
||||
rear_port_id, rear_port_position = rp_position.split(':')
|
||||
assignments.append(
|
||||
self.port_assignment_model(
|
||||
front_port_id=self.instance.pk,
|
||||
front_port_position=i,
|
||||
rear_port_id=rear_port_id,
|
||||
rear_port_position=rear_port_position,
|
||||
)
|
||||
)
|
||||
self.port_assignment_model.objects.bulk_create(assignments)
|
||||
|
||||
@@ -6,6 +6,7 @@ from timezone_field import TimeZoneFormField
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.forms.mixins import FrontPortFormMixin
|
||||
from dcim.models import *
|
||||
from extras.models import ConfigTemplate
|
||||
from ipam.choices import VLANQinQRoleChoices
|
||||
@@ -1111,29 +1112,67 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
|
||||
]
|
||||
|
||||
|
||||
class FrontPortTemplateForm(ModularComponentTemplateForm):
|
||||
rear_ports = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label=_('Rear ports'),
|
||||
widget=forms.SelectMultiple(attrs={'size': 8})
|
||||
)
|
||||
|
||||
class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('device_type', name=_('Device Type')),
|
||||
FieldSet('module_type', name=_('Module Type')),
|
||||
),
|
||||
'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description',
|
||||
'name', 'label', 'positions', 'rear_ports', 'description',
|
||||
),
|
||||
)
|
||||
|
||||
port_assignment_model = PortAssignmentTemplate
|
||||
parent_field = 'device_type'
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = [
|
||||
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if device_type_id := self.data.get('device_type') or self.initial.get('device_type'):
|
||||
device_type = DeviceType.objects.get(pk=device_type_id)
|
||||
else:
|
||||
return
|
||||
|
||||
# Populate rear port choices
|
||||
self.fields['rear_ports'].choices = self._get_rear_port_choices(device_type, self.instance)
|
||||
|
||||
# Set initial rear port assignments
|
||||
if self.instance.pk:
|
||||
self.initial['rear_ports'] = [
|
||||
f'{assignment.rear_port_id}:{assignment.rear_port_position}'
|
||||
for assignment in PortAssignmentTemplate.objects.filter(front_port_id=self.instance.pk)
|
||||
]
|
||||
|
||||
def _get_rear_port_choices(self, device_type, front_port):
|
||||
"""
|
||||
Return a list of choices representing each available rear port & position pair on the device type, excluding
|
||||
those assigned to the specified instance.
|
||||
"""
|
||||
occupied_rear_port_positions = [
|
||||
f'{assignment.rear_port_id}:{assignment.rear_port_position}'
|
||||
for assignment in PortAssignmentTemplate.objects.filter(
|
||||
front_port__device_type=device_type
|
||||
).exclude(front_port=front_port.pk)
|
||||
]
|
||||
|
||||
choices = []
|
||||
for rear_port in RearPortTemplate.objects.filter(device_type=device_type):
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
pair_id = f'{rear_port.pk}:{i}'
|
||||
if pair_id not in occupied_rear_port_positions:
|
||||
pair_label = f'{rear_port.name}:{i}'
|
||||
choices.append(
|
||||
(pair_id, pair_label)
|
||||
)
|
||||
return choices
|
||||
|
||||
|
||||
class RearPortTemplateForm(ModularComponentTemplateForm):
|
||||
fieldsets = (
|
||||
@@ -1572,13 +1611,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
||||
}
|
||||
|
||||
|
||||
class FrontPortForm(ModularDeviceComponentForm):
|
||||
rear_ports = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label=_('Rear ports'),
|
||||
widget=forms.SelectMultiple(attrs={'size': 8})
|
||||
)
|
||||
|
||||
class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
|
||||
@@ -1586,6 +1619,8 @@ class FrontPortForm(ModularDeviceComponentForm):
|
||||
),
|
||||
)
|
||||
|
||||
port_assignment_model = PortAssignment
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = [
|
||||
@@ -1611,44 +1646,6 @@ class FrontPortForm(ModularDeviceComponentForm):
|
||||
for assignment in PortAssignment.objects.filter(front_port_id=self.instance.pk)
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# FrontPort with no positions cannot be mapped to more than one RearPort
|
||||
if not self.cleaned_data['positions'] and len(self.cleaned_data['rear_ports']) > 1:
|
||||
raise forms.ValidationError({
|
||||
'positions': _("A front port with no positions cannot be mapped to multiple rear ports.")
|
||||
})
|
||||
|
||||
# Count of selected rear port & position pairs much match the assigned number of positions
|
||||
if len(self.cleaned_data['rear_ports']) != self.cleaned_data['positions']:
|
||||
raise forms.ValidationError({
|
||||
'rear_ports': _(
|
||||
"The number of rear port/position pairs selected must match the number of positions assigned."
|
||||
)
|
||||
})
|
||||
|
||||
def _save_m2m(self):
|
||||
super()._save_m2m()
|
||||
|
||||
# TODO: Can this be made more efficient?
|
||||
# Delete existing rear port assignments
|
||||
PortAssignment.objects.filter(front_port_id=self.instance.pk).delete()
|
||||
|
||||
# Create new rear port assignments
|
||||
assignments = []
|
||||
for i, rp_position in enumerate(self.cleaned_data['rear_ports'], start=1):
|
||||
rear_port_id, rear_port_position = rp_position.split(':')
|
||||
assignments.append(
|
||||
PortAssignment(
|
||||
front_port_id=self.instance.pk,
|
||||
front_port_position=i,
|
||||
rear_port_id=rear_port_id,
|
||||
rear_port_position=rear_port_position,
|
||||
)
|
||||
)
|
||||
PortAssignment.objects.bulk_create(assignments)
|
||||
|
||||
def _get_rear_port_choices(self, device, front_port):
|
||||
"""
|
||||
Return a list of choices representing each available rear port & position pair on the device, excluding those
|
||||
|
||||
@@ -109,69 +109,37 @@ class InterfaceTemplateCreateForm(ComponentCreateForm, model_forms.InterfaceTemp
|
||||
|
||||
|
||||
class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemplateForm):
|
||||
rear_port = forms.MultipleChoiceField(
|
||||
choices=[],
|
||||
label=_('Rear ports'),
|
||||
help_text=_('Select one rear port assignment for each front port being created.'),
|
||||
widget=forms.SelectMultiple(attrs={'size': 6})
|
||||
)
|
||||
|
||||
# Override fieldsets from FrontPortTemplateForm to omit rear_port_position
|
||||
# Override fieldsets from FrontPortTemplateForm
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
FieldSet('device_type', name=_('Device Type')),
|
||||
FieldSet('module_type', name=_('Module Type')),
|
||||
),
|
||||
'name', 'label', 'type', 'color', 'rear_port', 'description',
|
||||
'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description',
|
||||
),
|
||||
)
|
||||
|
||||
class Meta(model_forms.FrontPortTemplateForm.Meta):
|
||||
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# TODO: This needs better validation
|
||||
if 'device_type' in self.initial or self.data.get('device_type'):
|
||||
parent = DeviceType.objects.get(
|
||||
pk=self.initial.get('device_type') or self.data.get('device_type')
|
||||
)
|
||||
elif 'module_type' in self.initial or self.data.get('module_type'):
|
||||
parent = ModuleType.objects.get(
|
||||
pk=self.initial.get('module_type') or self.data.get('module_type')
|
||||
)
|
||||
else:
|
||||
return
|
||||
|
||||
# Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
|
||||
occupied_port_positions = [
|
||||
(front_port.rear_port_id, front_port.rear_port_position)
|
||||
for front_port in parent.frontporttemplates.all()
|
||||
]
|
||||
|
||||
# Populate rear port choices
|
||||
choices = []
|
||||
rear_ports = parent.rearporttemplates.all()
|
||||
for rear_port in rear_ports:
|
||||
for i in range(1, rear_port.positions + 1):
|
||||
if (rear_port.pk, i) not in occupied_port_positions:
|
||||
choices.append(
|
||||
('{}:{}'.format(rear_port.pk, i), '{}:{}'.format(rear_port.name, i))
|
||||
)
|
||||
self.fields['rear_port'].choices = choices
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = (
|
||||
'device_type', 'module_type', 'type', 'color', 'positions', 'description',
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
# TODO
|
||||
# super(ComponentCreateForm, self).clean()
|
||||
|
||||
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
|
||||
# positions
|
||||
print(f"name: {self.cleaned_data['name']}")
|
||||
print(f"rear_ports: {self.cleaned_data['rear_ports']}")
|
||||
frontport_count = len(self.cleaned_data['name'])
|
||||
rearport_count = len(self.cleaned_data['rear_port'])
|
||||
rearport_count = len(self.cleaned_data['rear_ports'])
|
||||
if frontport_count != rearport_count:
|
||||
raise forms.ValidationError({
|
||||
'rear_port': _(
|
||||
'rear_ports': _(
|
||||
"The number of front port templates to be created ({frontport_count}) must match the selected "
|
||||
"number of rear port positions ({rearport_count})."
|
||||
).format(
|
||||
@@ -181,13 +149,11 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
|
||||
})
|
||||
|
||||
def get_iterative_data(self, iteration):
|
||||
|
||||
# Assign rear port and position from selected set
|
||||
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
|
||||
positions = self.cleaned_data['positions']
|
||||
offset = positions * iteration
|
||||
|
||||
return {
|
||||
'rear_port': int(rear_port),
|
||||
'rear_port_position': int(position),
|
||||
'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ 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_assignments, update_interface_bridges
|
||||
from extras.models import ConfigContextModel, CustomField
|
||||
from extras.querysets import ConfigContextModelQuerySet
|
||||
from netbox.choices import ColorChoices
|
||||
@@ -30,6 +30,7 @@ from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||
from utilities.fields import ColorField, CounterCacheField
|
||||
from utilities.prefetch import get_prefetchable_fields
|
||||
from utilities.tracking import TrackingModelMixin
|
||||
from . import PortAssignmentTemplate
|
||||
from .device_components import *
|
||||
from .mixins import RenderConfigMixin
|
||||
from .modules import Module
|
||||
@@ -1008,6 +1009,10 @@ class Device(
|
||||
self._instantiate_components(self.device_type.inventoryitemtemplates.all(), bulk_create=False)
|
||||
# Interface bridges have to be set after interface instantiation
|
||||
update_interface_bridges(self, self.device_type.interfacetemplates.all())
|
||||
# Replicate any front/rear port assignments from the DeviceType
|
||||
create_port_assignments(self, PortAssignmentTemplate.objects.filter(
|
||||
front_port__device_type=self.device_type
|
||||
))
|
||||
|
||||
# Update Site and Rack assignment for any child Devices
|
||||
devices = Device.objects.filter(parent_bay__device=self)
|
||||
|
||||
@@ -83,3 +83,23 @@ def update_interface_bridges(device, interface_templates, module=None):
|
||||
)
|
||||
interface.full_clean()
|
||||
interface.save()
|
||||
|
||||
|
||||
def create_port_assignments(device, templates, module=None):
|
||||
"""
|
||||
Used for device and module instantiation. Replicate all front/rear port assignments from a DeviceType to the given
|
||||
device.
|
||||
"""
|
||||
from dcim.models.device_components import FrontPort, PortAssignment, RearPort
|
||||
|
||||
for template in templates:
|
||||
front_port = FrontPort.objects.get(device=device, name=template.front_port.resolve_name(module=module))
|
||||
rear_port = RearPort.objects.get(device=device, name=template.rear_port.resolve_name(module=module))
|
||||
|
||||
assignment = PortAssignment(
|
||||
front_port=front_port,
|
||||
front_port_position=template.front_port_position,
|
||||
rear_port=rear_port,
|
||||
rear_port_position=template.rear_port_position,
|
||||
)
|
||||
assignment.save()
|
||||
|
||||
Reference in New Issue
Block a user