diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 5e4311c13..16926081f 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -32,8 +32,8 @@ CABLE_POSITION_MAX = 1024 # RearPorts # -REARPORT_POSITIONS_MIN = 1 -REARPORT_POSITIONS_MAX = 1024 +PORT_POSITION_MIN = 1 +PORT_POSITION_MAX = 1024 # diff --git a/netbox/dcim/migrations/0221_m2m_port_assignments.py b/netbox/dcim/migrations/0221_m2m_port_assignments.py new file mode 100644 index 000000000..d6c3710d4 --- /dev/null +++ b/netbox/dcim/migrations/0221_m2m_port_assignments.py @@ -0,0 +1,85 @@ +import django.core.validators +import django.db.models.deletion +from django.db import migrations +from django.db import models +from itertools import islice + + +def chunked(iterable, size): + """Yield successive chunks of a given size from an iterator.""" + iterator = iter(iterable) + while chunk := list(islice(iterator, size)): + yield chunk + + +def populate_port_assignments(apps, schema_editor): + FrontPort = apps.get_model('dcim', 'FrontPort') + PortAssignment = apps.get_model('dcim', 'PortAssignment') + + front_ports = FrontPort.objects.iterator(chunk_size=1000) + + def generate_copies(): + for front_port in front_ports: + yield PortAssignment( + front_port_id=front_port.pk, + front_port_position=1, + 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): + PortAssignment.objects.bulk_create(chunk, batch_size=1000) + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0220_cable_position'), + ] + + operations = [ + migrations.CreateModel( + name='PortAssignment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ( + 'front_port_position', + models.PositiveSmallIntegerField( + 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.frontport')), + ('rear_port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.rearport')), + ], + ), + 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' + ), + ), + migrations.AddConstraint( + model_name='portassignment', + constraint=models.UniqueConstraint( + fields=('rear_port', 'rear_port_position'), name='dcim_portassignment_unique_rear_port_position' + ), + ), + migrations.RunPython(code=populate_port_assignments, reverse_code=migrations.RunPython.noop), + ] diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 74e624d6c..177da1765 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -540,8 +540,8 @@ class FrontPortTemplate(ModularComponentTemplateModel): verbose_name=_('rear port position'), default=1, validators=[ - MinValueValidator(REARPORT_POSITIONS_MIN), - MaxValueValidator(REARPORT_POSITIONS_MAX) + MinValueValidator(PORT_POSITION_MIN), + MaxValueValidator(PORT_POSITION_MAX) ] ) @@ -635,8 +635,8 @@ class RearPortTemplate(ModularComponentTemplateModel): verbose_name=_('positions'), default=1, validators=[ - MinValueValidator(REARPORT_POSITIONS_MIN), - MaxValueValidator(REARPORT_POSITIONS_MAX) + MinValueValidator(PORT_POSITION_MIN), + MaxValueValidator(PORT_POSITION_MAX) ] ) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 8c9acc48f..3f90963a7 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1069,6 +1069,44 @@ class Interface( # Pass-through ports # +class PortAssignment(models.Model): + """ + Maps a FrontPort & position to a RearPort & position. + """ + front_port = models.ForeignKey( + to='dcim.FrontPort', + on_delete=models.CASCADE, + ) + front_port_position = models.PositiveSmallIntegerField( + validators=( + MinValueValidator(PORT_POSITION_MIN), + MaxValueValidator(PORT_POSITION_MAX), + ) + ) + rear_port = models.ForeignKey( + to='dcim.RearPort', + 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' + ), + ) + + class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): """ A pass-through port on the front of a Device. @@ -1082,6 +1120,13 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): verbose_name=_('color'), blank=True ) + rear_ports = models.ManyToManyField( + to='dcim.RearPort', + through='dcim.PortAssignment', + related_name='front_ports', + ) + + # Legacy fields rear_port = models.ForeignKey( to='dcim.RearPort', on_delete=models.CASCADE, @@ -1091,8 +1136,8 @@ class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): verbose_name=_('rear port position'), default=1, validators=[ - MinValueValidator(REARPORT_POSITIONS_MIN), - MaxValueValidator(REARPORT_POSITIONS_MAX) + MinValueValidator(PORT_POSITION_MIN), + MaxValueValidator(PORT_POSITION_MAX) ], help_text=_('Mapped position on corresponding rear port') ) @@ -1157,8 +1202,8 @@ class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin): verbose_name=_('positions'), default=1, validators=[ - MinValueValidator(REARPORT_POSITIONS_MIN), - MaxValueValidator(REARPORT_POSITIONS_MAX) + MinValueValidator(PORT_POSITION_MIN), + MaxValueValidator(PORT_POSITION_MAX) ], help_text=_('Number of front ports which may be mapped') )