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, choices=PortTypeChoices,
null_value=None null_value=None
) )
rear_port_id = django_filters.ModelMultipleChoiceFilter( # TODO
queryset=RearPort.objects.all() # rear_port_id = django_filters.ModelMultipleChoiceFilter(
) # queryset=RearPortTemplate.objects.all()
# )
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description') fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@@ -898,6 +899,10 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
choices=PortTypeChoices, choices=PortTypeChoices,
null_value=None null_value=None
) )
# TODO
# front_port_id = django_filters.ModelMultipleChoiceFilter(
# queryset=FrontPortTemplate.objects.all()
# )
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate

View File

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

View File

@@ -113,31 +113,11 @@ class FrontPortTemplateImportForm(forms.ModelForm):
label=_('Type'), label=_('Type'),
choices=PortTypeChoices.CHOICES 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: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = [ fields = [
'device_type', 'module_type', 'name', 'type', 'color', 'rear_port', 'rear_port_position', 'label', 'device_type', 'module_type', 'name', 'type', 'color', 'positions', 'label', 'description',
'description',
] ]

View File

@@ -6,12 +6,34 @@ from itertools import islice
def chunked(iterable, size): 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) iterator = iter(iterable)
while chunk := list(islice(iterator, size)): while chunk := list(islice(iterator, size)):
yield chunk 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): def populate_port_assignments(apps, schema_editor):
FrontPort = apps.get_model('dcim', 'FrontPort') FrontPort = apps.get_model('dcim', 'FrontPort')
PortAssignment = apps.get_model('dcim', 'PortAssignment') PortAssignment = apps.get_model('dcim', 'PortAssignment')
@@ -38,6 +60,68 @@ class Migration(migrations.Migration):
] ]
operations = [ 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( migrations.CreateModel(
name='PortAssignment', name='PortAssignment',
fields=[ fields=[
@@ -66,22 +150,39 @@ class Migration(migrations.Migration):
('rear_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.rearport')), ('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( migrations.AddField(
model_name='frontport', model_name='frontport',
name='rear_ports', name='rear_ports',
field=models.ManyToManyField(related_name='front_ports', through='dcim.PortAssignment', to='dcim.rearport'), field=models.ManyToManyField(
), related_name='front_ports',
migrations.AddConstraint( through='dcim.PortAssignment',
model_name='portassignment', to='dcim.rearport'
constraint=models.UniqueConstraint(
fields=('front_port', 'front_port_position'), name='dcim_portassignment_unique_front_port_position'
), ),
), ),
migrations.AddConstraint(
model_name='portassignment', # Data migration
constraint=models.UniqueConstraint( migrations.RunPython(
fields=('rear_port', 'rear_port_position'), name='dcim_portassignment_unique_rear_port_position' 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 = [ 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( migrations.RemoveConstraint(
model_name='frontport', model_name='frontport',
name='dcim_frontport_unique_rear_port_position', name='dcim_frontport_unique_rear_port_position',
@@ -21,6 +49,8 @@ class Migration(migrations.Migration):
model_name='frontport', model_name='frontport',
name='rear_port_position', name='rear_port_position',
), ),
# Add positions on FrontPort
migrations.AddField( migrations.AddField(
model_name='frontport', model_name='frontport',
name='positions', 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): class FrontPortTemplate(ModularComponentTemplateModel):
""" """
Template for a pass-through port on the front of a new Device. Template for a pass-through port on the front of a new Device.
@@ -531,18 +594,18 @@ class FrontPortTemplate(ModularComponentTemplateModel):
verbose_name=_('color'), verbose_name=_('color'),
blank=True blank=True
) )
rear_port = models.ForeignKey( positions = models.PositiveSmallIntegerField(
to='dcim.RearPortTemplate', verbose_name=_('positions'),
on_delete=models.CASCADE,
related_name='frontport_templates'
)
rear_port_position = models.PositiveSmallIntegerField(
verbose_name=_('rear port position'),
default=1, default=1,
validators=[ validators=[
MinValueValidator(PORT_POSITION_MIN), MinValueValidator(PORT_POSITION_MIN),
MaxValueValidator(PORT_POSITION_MAX) MaxValueValidator(PORT_POSITION_MAX)
] ],
)
rear_ports = models.ManyToManyField(
to='dcim.RearPortTemplate',
through='dcim.PortAssignmentTemplate',
related_name='front_ports',
) )
component_model = FrontPort component_model = FrontPort
@@ -557,51 +620,17 @@ class FrontPortTemplate(ModularComponentTemplateModel):
fields=('module_type', 'name'), fields=('module_type', 'name'),
name='%(app_label)s_%(class)s_unique_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 = _('front port template')
verbose_name_plural = _('front port templates') 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): 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( return self.component_model(
name=self.resolve_name(kwargs.get('module')), name=self.resolve_name(kwargs.get('module')),
label=self.resolve_label(kwargs.get('module')), label=self.resolve_label(kwargs.get('module')),
type=self.type, type=self.type,
color=self.color, color=self.color,
rear_port=rear_port, positions=self.positions,
rear_port_position=self.rear_port_position,
**kwargs **kwargs
) )
instantiate.do_not_call_in_templates = True instantiate.do_not_call_in_templates = True
@@ -611,8 +640,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
'name': self.name, 'name': self.name,
'type': self.type, 'type': self.type,
'color': self.color, 'color': self.color,
'rear_port': self.rear_port.name, 'positions': self.positions,
'rear_port_position': self.rear_port_position,
'label': self.label, 'label': self.label,
'description': self.description, 'description': self.description,
} }
@@ -637,7 +665,7 @@ class RearPortTemplate(ModularComponentTemplateModel):
validators=[ validators=[
MinValueValidator(PORT_POSITION_MIN), MinValueValidator(PORT_POSITION_MIN),
MaxValueValidator(PORT_POSITION_MAX) MaxValueValidator(PORT_POSITION_MAX)
] ],
) )
component_model = RearPort component_model = RearPort