mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-19 03:42:25 -06:00
Add PortAssignmentTemplate for device types
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user