Add PortAssignmentTemplate for device types

This commit is contained in:
Jeremy Stretch
2025-11-20 15:32:11 -05:00
parent 4f54b29f48
commit f067122ccd
6 changed files with 235 additions and 96 deletions

View File

@@ -884,13 +884,14 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
choices=PortTypeChoices,
null_value=None
)
rear_port_id = django_filters.ModelMultipleChoiceFilter(
queryset=RearPort.objects.all()
)
# TODO
# rear_port_id = django_filters.ModelMultipleChoiceFilter(
# queryset=RearPortTemplate.objects.all()
# )
class Meta:
model = FrontPortTemplate
fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description')
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@@ -898,6 +899,10 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
choices=PortTypeChoices,
null_value=None
)
# TODO
# front_port_id = django_filters.ModelMultipleChoiceFilter(
# queryset=FrontPortTemplate.objects.all()
# )
class Meta:
model = RearPortTemplate

View File

@@ -1112,14 +1112,10 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
class FrontPortTemplateForm(ModularComponentTemplateForm):
rear_port = DynamicModelChoiceField(
label=_('Rear port'),
queryset=RearPortTemplate.objects.all(),
required=False,
query_params={
'device_type_id': '$device_type',
'module_type_id': '$module_type',
}
rear_ports = forms.MultipleChoiceField(
choices=[],
label=_('Rear ports'),
widget=forms.SelectMultiple(attrs={'size': 8})
)
fieldsets = (
@@ -1128,15 +1124,14 @@ class FrontPortTemplateForm(ModularComponentTemplateForm):
FieldSet('device_type', name=_('Device Type')),
FieldSet('module_type', name=_('Module Type')),
),
'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position', 'description',
'name', 'label', 'type', 'color', 'positions', 'rear_ports', 'description',
),
)
class Meta:
model = FrontPortTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', 'rear_port_position',
'description',
'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', 'description',
]
@@ -1581,7 +1576,7 @@ class FrontPortForm(ModularDeviceComponentForm):
rear_ports = forms.MultipleChoiceField(
choices=[],
label=_('Rear ports'),
widget=forms.SelectMultiple(attrs={'size': 6})
widget=forms.SelectMultiple(attrs={'size': 8})
)
fieldsets = (

View File

@@ -113,31 +113,11 @@ class FrontPortTemplateImportForm(forms.ModelForm):
label=_('Type'),
choices=PortTypeChoices.CHOICES
)
rear_port = forms.ModelChoiceField(
label=_('Rear port'),
queryset=RearPortTemplate.objects.all(),
to_field_name='name'
)
def clean_device_type(self):
if device_type := self.cleaned_data['device_type']:
rear_port = self.fields['rear_port']
rear_port.queryset = rear_port.queryset.filter(device_type=device_type)
return device_type
def clean_module_type(self):
if module_type := self.cleaned_data['module_type']:
rear_port = self.fields['rear_port']
rear_port.queryset = rear_port.queryset.filter(module_type=module_type)
return module_type
class Meta:
model = FrontPortTemplate
fields = [
'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label',
'description',
'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description',
]

View File

@@ -6,12 +6,34 @@ from itertools import islice
def chunked(iterable, size):
"""Yield successive chunks of a given size from an iterator."""
"""
Yield successive chunks of a given size from an iterator.
"""
iterator = iter(iterable)
while chunk := list(islice(iterator, size)):
yield chunk
def populate_port_template_assignments(apps, schema_editor):
FrontPortTemplate = apps.get_model('dcim', 'FrontPortTemplate')
PortAssignmentTemplate = apps.get_model('dcim', 'PortAssignmentTemplate')
front_ports = FrontPortTemplate.objects.iterator(chunk_size=1000)
def generate_copies():
for front_port in front_ports:
yield PortAssignmentTemplate(
front_port_id=front_port.pk,
front_port_position=None,
rear_port_id=front_port.rear_port_id,
rear_port_position=front_port.rear_port_position,
)
# Bulk insert in streaming batches
for chunk in chunked(generate_copies(), 1000):
PortAssignmentTemplate.objects.bulk_create(chunk, batch_size=1000)
def populate_port_assignments(apps, schema_editor):
FrontPort = apps.get_model('dcim', 'FrontPort')
PortAssignment = apps.get_model('dcim', 'PortAssignment')
@@ -38,6 +60,68 @@ class Migration(migrations.Migration):
]
operations = [
# Create PortAssignmentTemplate model (for DeviceTypes)
migrations.CreateModel(
name='PortAssignmentTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
(
'front_port_position',
models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024)
]
)
),
(
'rear_port_position',
models.PositiveSmallIntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024)
]
)
),
(
'front_port',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.frontporttemplate')
),
(
'rear_port',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.rearporttemplate')
),
],
),
migrations.AddConstraint(
model_name='portassignmenttemplate',
constraint=models.UniqueConstraint(
fields=('front_port', 'front_port_position'),
name='dcim_portassignmenttemplate_unique_front_port_position'
),
),
migrations.AddConstraint(
model_name='portassignmenttemplate',
constraint=models.UniqueConstraint(
fields=('rear_port', 'rear_port_position'),
name='dcim_portassignmenttemplate_unique_rear_port_position'
),
),
# Add rear_ports ManyToManyField on FrontPortTemplate
migrations.AddField(
model_name='frontporttemplate',
name='rear_ports',
field=models.ManyToManyField(
related_name='front_ports',
through='dcim.PortAssignmentTemplate',
to='dcim.rearporttemplate'
),
),
# Create PortAssignment model (for Devices)
migrations.CreateModel(
name='PortAssignment',
fields=[
@@ -66,22 +150,39 @@ class Migration(migrations.Migration):
('rear_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.rearport')),
],
),
migrations.AddConstraint(
model_name='portassignment',
constraint=models.UniqueConstraint(
fields=('front_port', 'front_port_position'),
name='dcim_portassignment_unique_front_port_position'
),
),
migrations.AddConstraint(
model_name='portassignment',
constraint=models.UniqueConstraint(
fields=('rear_port', 'rear_port_position'),
name='dcim_portassignment_unique_rear_port_position'
),
),
# Add rear_ports ManyToManyField on FrontPort
migrations.AddField(
model_name='frontport',
name='rear_ports',
field=models.ManyToManyField(related_name='front_ports', through='dcim.PortAssignment', to='dcim.rearport'),
),
migrations.AddConstraint(
model_name='portassignment',
constraint=models.UniqueConstraint(
fields=('front_port', 'front_port_position'), name='dcim_portassignment_unique_front_port_position'
field=models.ManyToManyField(
related_name='front_ports',
through='dcim.PortAssignment',
to='dcim.rearport'
),
),
migrations.AddConstraint(
model_name='portassignment',
constraint=models.UniqueConstraint(
fields=('rear_port', 'rear_port_position'), name='dcim_portassignment_unique_rear_port_position'
),
# Data migration
migrations.RunPython(
code=populate_port_template_assignments,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
code=populate_port_assignments,
reverse_code=migrations.RunPython.noop
),
migrations.RunPython(code=populate_port_assignments, reverse_code=migrations.RunPython.noop),
]

View File

@@ -9,6 +9,34 @@ class Migration(migrations.Migration):
]
operations = [
# Remove rear_port & rear_port_position from FrontPortTemplate
migrations.RemoveConstraint(
model_name='frontporttemplate',
name='dcim_frontporttemplate_unique_rear_port_position',
),
migrations.RemoveField(
model_name='frontporttemplate',
name='rear_port',
),
migrations.RemoveField(
model_name='frontporttemplate',
name='rear_port_position',
),
# Add positions on FrontPortTemplate
migrations.AddField(
model_name='frontporttemplate',
name='positions',
field=models.PositiveSmallIntegerField(
default=1,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(1024)
]
),
),
# Remove rear_port & rear_port_position from FrontPort
migrations.RemoveConstraint(
model_name='frontport',
name='dcim_frontport_unique_rear_port_position',
@@ -21,6 +49,8 @@ class Migration(migrations.Migration):
model_name='frontport',
name='rear_port_position',
),
# Add positions on FrontPort
migrations.AddField(
model_name='frontport',
name='positions',

View File

@@ -518,6 +518,69 @@ class InterfaceTemplate(InterfaceValidationMixin, ModularComponentTemplateModel)
}
class PortAssignmentTemplate(models.Model):
"""
Maps a FrontPortTemplate & position to a RearPortTemplate & position.
"""
front_port = models.ForeignKey(
to='dcim.FrontPortTemplate',
on_delete=models.CASCADE,
)
front_port_position = models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=(
MinValueValidator(PORT_POSITION_MIN),
MaxValueValidator(PORT_POSITION_MAX),
),
)
rear_port = models.ForeignKey(
to='dcim.RearPortTemplate',
on_delete=models.CASCADE,
)
rear_port_position = models.PositiveSmallIntegerField(
validators=(
MinValueValidator(PORT_POSITION_MIN),
MaxValueValidator(PORT_POSITION_MAX),
),
)
class Meta:
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):
# 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
)
})
# 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
)
})
class FrontPortTemplate(ModularComponentTemplateModel):
"""
Template for a pass-through port on the front of a new Device.
@@ -531,18 +594,18 @@ 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(PORT_POSITION_MIN),
MaxValueValidator(PORT_POSITION_MAX)
]
],
)
rear_ports = models.ManyToManyField(
to='dcim.RearPortTemplate',
through='dcim.PortAssignmentTemplate',
related_name='front_ports',
)
component_model = FrontPort
@@ -557,51 +620,17 @@ 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')
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
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 +640,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,
}
@@ -637,7 +665,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
validators=[
MinValueValidator(PORT_POSITION_MIN),
MaxValueValidator(PORT_POSITION_MAX)
]
],
)
component_model = RearPort