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
|
default=None
|
||||||
)
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
rear_port = RearPortTemplateSerializer(nested=True)
|
rear_ports = RearPortTemplateSerializer(nested=True, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color',
|
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
|
||||||
'rear_port', 'rear_port_position', 'description', 'created', 'last_updated',
|
'rear_ports', 'description', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description')
|
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 django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.constants import LOCATION_SCOPE_TYPES
|
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 import get_field_value
|
||||||
from utilities.forms.fields import (
|
from utilities.forms.fields import (
|
||||||
ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField,
|
ContentTypeChoiceField, CSVContentTypeField, DynamicModelChoiceField,
|
||||||
@@ -13,6 +13,7 @@ from utilities.templatetags.builtins.filters import bettertitle
|
|||||||
from utilities.forms.widgets import HTMXSelect
|
from utilities.forms.widgets import HTMXSelect
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'FrontPortFormMixin',
|
||||||
'ScopedBulkEditForm',
|
'ScopedBulkEditForm',
|
||||||
'ScopedForm',
|
'ScopedForm',
|
||||||
'ScopedImportForm',
|
'ScopedImportForm',
|
||||||
@@ -128,3 +129,51 @@ class ScopedImportForm(forms.Form):
|
|||||||
"Please select a {scope_type}."
|
"Please select a {scope_type}."
|
||||||
).format(scope_type=scope_type.model_class()._meta.model_name)
|
).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.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
|
from dcim.forms.mixins import FrontPortFormMixin
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
from ipam.choices import VLANQinQRoleChoices
|
from ipam.choices import VLANQinQRoleChoices
|
||||||
@@ -1111,29 +1112,67 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateForm(ModularComponentTemplateForm):
|
class FrontPortTemplateForm(FrontPortFormMixin, ModularComponentTemplateForm):
|
||||||
rear_ports = forms.MultipleChoiceField(
|
|
||||||
choices=[],
|
|
||||||
label=_('Rear ports'),
|
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8})
|
|
||||||
)
|
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
TabbedGroups(
|
TabbedGroups(
|
||||||
FieldSet('device_type', name=_('Device Type')),
|
FieldSet('device_type', name=_('Device Type')),
|
||||||
FieldSet('module_type', name=_('Module 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:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
|
'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):
|
class RearPortTemplateForm(ModularComponentTemplateForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -1572,13 +1611,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class FrontPortForm(ModularDeviceComponentForm):
|
class FrontPortForm(FrontPortFormMixin, ModularDeviceComponentForm):
|
||||||
rear_ports = forms.MultipleChoiceField(
|
|
||||||
choices=[],
|
|
||||||
label=_('Rear ports'),
|
|
||||||
widget=forms.SelectMultiple(attrs={'size': 8})
|
|
||||||
)
|
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
|
'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'mark_connected',
|
||||||
@@ -1586,6 +1619,8 @@ class FrontPortForm(ModularDeviceComponentForm):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
port_assignment_model = PortAssignment
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = [
|
fields = [
|
||||||
@@ -1611,44 +1646,6 @@ class FrontPortForm(ModularDeviceComponentForm):
|
|||||||
for assignment in PortAssignment.objects.filter(front_port_id=self.instance.pk)
|
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):
|
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
|
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):
|
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 = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
TabbedGroups(
|
TabbedGroups(
|
||||||
FieldSet('device_type', name=_('Device Type')),
|
FieldSet('device_type', name=_('Device Type')),
|
||||||
FieldSet('module_type', name=_('Module 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):
|
class Meta:
|
||||||
exclude = ('name', 'label', 'rear_port', 'rear_port_position')
|
model = FrontPortTemplate
|
||||||
|
fields = (
|
||||||
def __init__(self, *args, **kwargs):
|
'device_type', 'module_type', 'type', 'color', 'positions', 'description',
|
||||||
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
|
|
||||||
|
|
||||||
def clean(self):
|
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
|
# Check that the number of FrontPortTemplates to be created matches the selected number of RearPortTemplate
|
||||||
# positions
|
# positions
|
||||||
|
print(f"name: {self.cleaned_data['name']}")
|
||||||
|
print(f"rear_ports: {self.cleaned_data['rear_ports']}")
|
||||||
frontport_count = len(self.cleaned_data['name'])
|
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:
|
if frontport_count != rearport_count:
|
||||||
raise forms.ValidationError({
|
raise forms.ValidationError({
|
||||||
'rear_port': _(
|
'rear_ports': _(
|
||||||
"The number of front port templates to be created ({frontport_count}) must match the selected "
|
"The number of front port templates to be created ({frontport_count}) must match the selected "
|
||||||
"number of rear port positions ({rearport_count})."
|
"number of rear port positions ({rearport_count})."
|
||||||
).format(
|
).format(
|
||||||
@@ -181,13 +149,11 @@ class FrontPortTemplateCreateForm(ComponentCreateForm, model_forms.FrontPortTemp
|
|||||||
})
|
})
|
||||||
|
|
||||||
def get_iterative_data(self, iteration):
|
def get_iterative_data(self, iteration):
|
||||||
|
positions = self.cleaned_data['positions']
|
||||||
# Assign rear port and position from selected set
|
offset = positions * iteration
|
||||||
rear_port, position = self.cleaned_data['rear_port'][iteration].split(':')
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'rear_port': int(rear_port),
|
'rear_ports': self.cleaned_data['rear_ports'][offset:offset + positions]
|
||||||
'rear_port_position': int(position),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import MACAddressField
|
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.models import ConfigContextModel, CustomField
|
||||||
from extras.querysets import ConfigContextModelQuerySet
|
from extras.querysets import ConfigContextModelQuerySet
|
||||||
from netbox.choices import ColorChoices
|
from netbox.choices import ColorChoices
|
||||||
@@ -30,6 +30,7 @@ from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
|||||||
from utilities.fields import ColorField, CounterCacheField
|
from utilities.fields import ColorField, CounterCacheField
|
||||||
from utilities.prefetch import get_prefetchable_fields
|
from utilities.prefetch import get_prefetchable_fields
|
||||||
from utilities.tracking import TrackingModelMixin
|
from utilities.tracking import TrackingModelMixin
|
||||||
|
from . import PortAssignmentTemplate
|
||||||
from .device_components import *
|
from .device_components import *
|
||||||
from .mixins import RenderConfigMixin
|
from .mixins import RenderConfigMixin
|
||||||
from .modules import Module
|
from .modules import Module
|
||||||
@@ -1008,6 +1009,10 @@ class Device(
|
|||||||
self._instantiate_components(self.device_type.inventoryitemtemplates.all(), bulk_create=False)
|
self._instantiate_components(self.device_type.inventoryitemtemplates.all(), bulk_create=False)
|
||||||
# Interface bridges have to be set after interface instantiation
|
# Interface bridges have to be set after interface instantiation
|
||||||
update_interface_bridges(self, self.device_type.interfacetemplates.all())
|
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
|
# Update Site and Rack assignment for any child Devices
|
||||||
devices = Device.objects.filter(parent_bay__device=self)
|
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.full_clean()
|
||||||
interface.save()
|
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