From b19f81cedebaa61197c1c5be62d6d7ae794a24e6 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Wed, 9 Jul 2025 10:36:41 -0500 Subject: [PATCH] More work on IP Address/Range and Prefix relationship --- netbox/ipam/api/serializers_/ip.py | 3 + netbox/ipam/filtersets.py | 14 ++ netbox/ipam/forms/bulk_edit.py | 3 + netbox/ipam/forms/bulk_import.py | 4 + netbox/ipam/forms/filtersets.py | 10 +- netbox/ipam/forms/model_forms.py | 13 +- .../0082_ipaddress_iprange_prefix_parent.py | 58 ++++++ ...3_ipaddress_iprange_prefix_parent_data.py} | 97 +++++---- netbox/ipam/models/ip.py | 75 ++++++- netbox/ipam/search.py | 7 +- netbox/ipam/signals.py | 128 +++++++++++- netbox/ipam/tables/ip.py | 3 + netbox/ipam/tests/test_api.py | 3 + netbox/ipam/tests/test_filtersets.py | 6 + netbox/ipam/tests/test_models.py | 196 +++++++++++++++++- netbox/ipam/tests/test_views.py | 12 ++ 16 files changed, 574 insertions(+), 58 deletions(-) create mode 100644 netbox/ipam/migrations/0082_ipaddress_iprange_prefix_parent.py rename netbox/ipam/migrations/{0082_ipaddress_prefix.py => 0083_ipaddress_iprange_prefix_parent_data.py} (68%) diff --git a/netbox/ipam/api/serializers_/ip.py b/netbox/ipam/api/serializers_/ip.py index 6f815b5ce..a07eadce6 100644 --- a/netbox/ipam/api/serializers_/ip.py +++ b/netbox/ipam/api/serializers_/ip.py @@ -44,6 +44,7 @@ class AggregateSerializer(NetBoxModelSerializer): class PrefixSerializer(NetBoxModelSerializer): + # TODO: Alter for parent prefix family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) vrf = VRFSerializer(nested=True, required=False, allow_null=True) scope_type = ContentTypeField( @@ -134,6 +135,7 @@ class AvailablePrefixSerializer(serializers.Serializer): # class IPRangeSerializer(NetBoxModelSerializer): + # TODO: Alter for prefix family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) start_address = IPAddressField() end_address = IPAddressField() @@ -158,6 +160,7 @@ class IPRangeSerializer(NetBoxModelSerializer): # class IPAddressSerializer(NetBoxModelSerializer): + # TODO: Alter for prefix family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) address = IPAddressField() vrf = VRFSerializer(nested=True, required=False, allow_null=True) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 2434e6c1c..720c5d0bf 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -291,6 +291,8 @@ class RoleFilterSet(OrganizationalModelFilterSet): class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet, ContactModelFilterSet): + # TODO: Alter for aggregate + # TODO: Alter for parent prefix family = django_filters.NumberFilter( field_name='prefix', lookup_expr='family' @@ -457,6 +459,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet, C class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet, ContactModelFilterSet): + # TODO: Alter for prefix family = django_filters.NumberFilter( field_name='start_address', lookup_expr='family' @@ -473,6 +476,16 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet, ContactModelFilte method='search_contains', label=_('Ranges which contain this prefix or IP'), ) + prefix_id = django_filters.ModelMultipleChoiceFilter( + queryset=Prefix.objects.all(), + label=_('Prefix (ID)'), + ) + prefix = django_filters.ModelMultipleChoiceFilter( + field_name='prefix__prefix', + queryset=Prefix.objects.all(), + to_field_name='prefix', + label=_('Prefix'), + ) vrf_id = django_filters.ModelMultipleChoiceFilter( queryset=VRF.objects.all(), label=_('VRF'), @@ -549,6 +562,7 @@ class IPRangeFilterSet(TenancyFilterSet, NetBoxModelFilterSet, ContactModelFilte class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): + # TODO: Alter for prefix family = django_filters.NumberFilter( field_name='address', lookup_expr='family' diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index 411208eeb..cefd59cbc 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -207,6 +207,7 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm): class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): + # TODO: Alter for parent prefix vlan_group = DynamicModelChoiceField( queryset=VLANGroup.objects.all(), required=False, @@ -276,6 +277,7 @@ class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): class IPRangeBulkEditForm(NetBoxModelBulkEditForm): + # TODO: Alter for prefix vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -323,6 +325,7 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm): class IPAddressBulkEditForm(NetBoxModelBulkEditForm): + # TODO: Alter for prefix prefix = DynamicModelChoiceField( queryset=Prefix.objects.all(), required=False, diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index d4945a50d..f01f5bdf7 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -156,6 +156,8 @@ class RoleImportForm(NetBoxModelImportForm): class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm): + # TODO: Alter for aggregate + # TODO: Alter for parent prefix vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -245,6 +247,7 @@ class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm): class IPRangeImportForm(NetBoxModelImportForm): + # TODO: Alter for prefix vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -281,6 +284,7 @@ class IPRangeImportForm(NetBoxModelImportForm): class IPAddressImportForm(NetBoxModelImportForm): + # TODO: Alter for prefix prefix = CSVModelChoiceField( label=_('Prefix'), queryset=Prefix.objects.all(), diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index 669b94ec9..780d43bb5 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -278,10 +278,18 @@ class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFi model = IPRange fieldsets = ( FieldSet('q', 'filter_id', 'tag'), - FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')), + FieldSet( + 'prefix', 'family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes') + ), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), ) + prefix = DynamicModelMultipleChoiceField( + queryset=Prefix.objects.all(), + required=False, + label=_('Prefix'), + null_option='None' + ) family = forms.ChoiceField( required=False, choices=add_blank_choice(IPAddressFamilyChoices), diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index a8fd43421..1df915ca9 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -250,6 +250,11 @@ class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm): class IPRangeForm(TenancyForm, NetBoxModelForm): + prefix = DynamicModelChoiceField( + queryset=Prefix.objects.all(), + required=False, + label=_('Prefix') + ) vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -265,8 +270,8 @@ class IPRangeForm(TenancyForm, NetBoxModelForm): fieldsets = ( FieldSet( - 'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized', 'description', - 'tags', name=_('IP Range') + 'prefix', 'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized', + 'description', 'tags', name=_('IP Range') ), FieldSet('tenant_group', 'tenant', name=_('Tenancy')), ) @@ -274,8 +279,8 @@ class IPRangeForm(TenancyForm, NetBoxModelForm): class Meta: model = IPRange fields = [ - 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_populated', - 'mark_utilized', 'description', 'comments', 'tags', + 'prefix', 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', + 'mark_populated', 'mark_utilized', 'description', 'comments', 'tags', ] diff --git a/netbox/ipam/migrations/0082_ipaddress_iprange_prefix_parent.py b/netbox/ipam/migrations/0082_ipaddress_iprange_prefix_parent.py new file mode 100644 index 000000000..d86c18670 --- /dev/null +++ b/netbox/ipam/migrations/0082_ipaddress_iprange_prefix_parent.py @@ -0,0 +1,58 @@ +# Generated by Django 5.0.9 on 2025-02-20 16:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0081_remove_service_device_virtual_machine_add_parent_gfk_index'), + ] + + operations = [ + migrations.AddField( + model_name='prefix', + name='parent', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='children', + to='ipam.prefix', + ), + ), + migrations.AddField( + model_name='ipaddress', + name='prefix', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='ip_addresses', + to='ipam.prefix', + ), + ), + migrations.AddField( + model_name='iprange', + name='prefix', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='ip_ranges', + to='ipam.prefix', + ), + ), + migrations.AddField( + model_name='prefix', + name='aggregate', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='prefixes', + to='ipam.aggregate', + ), + ), + ] diff --git a/netbox/ipam/migrations/0082_ipaddress_prefix.py b/netbox/ipam/migrations/0083_ipaddress_iprange_prefix_parent_data.py similarity index 68% rename from netbox/ipam/migrations/0082_ipaddress_prefix.py rename to netbox/ipam/migrations/0083_ipaddress_iprange_prefix_parent_data.py index 7f9e78e24..321c51f38 100644 --- a/netbox/ipam/migrations/0082_ipaddress_prefix.py +++ b/netbox/ipam/migrations/0083_ipaddress_iprange_prefix_parent_data.py @@ -3,11 +3,14 @@ import sys import time -import django.db.models.deletion from django.db import migrations, models +from ipam.choices import PrefixStatusChoices + def draw_progress(count, total, length=20): + if total == 0: + return progress = count / total percent = int(progress * 100) bar = int(progress * length) @@ -24,8 +27,9 @@ def set_ipaddress_prefix(apps, schema_editor): addresses = IPAddress.objects.all() i = 0 total = addresses.count() - if total > 0: - print('\r\n') + if total == 0: + return + print('\r\n') draw_progress(i, total, 50) for address in addresses: i += 1 @@ -55,8 +59,10 @@ def set_iprange_prefix(apps, schema_editor): addresses = IPRange.objects.all() i = 0 total = addresses.count() - if total > 0: - print('\r\n') + if total == 0: + return + + print('\r\n') draw_progress(i, total, 50) for address in addresses: i += 1 @@ -86,8 +92,10 @@ def set_prefix_aggregate(apps, schema_editor): addresses = Prefix.objects.all() i = 0 total = addresses.count() - if total > 0: - print('\r\n') + if total == 0: + return + + print('\r\n') draw_progress(i, total, 50) for address in addresses: i += 1 @@ -108,47 +116,54 @@ def unset_prefix_aggregate(apps, schema_editor): Prefix.objects.update(aggregate=None) +def set_prefix_parent(apps, schema_editor): + Prefix = apps.get_model('ipam', 'Prefix') + start = time.time() + addresses = Prefix.objects.all() + i = 0 + total = addresses.count() + if total == 0: + return + + print('\r\n') + draw_progress(i, total, 50) + for address in addresses: + i += 1 + prefixes = Prefix.objects.exclude(pk=address.pk).filter( + models.Q( + vrf=address.vrf, + prefix__net_contains=str(address.prefix.ip) + ) | models.Q( + vrf=None, + status=PrefixStatusChoices.STATUS_CONTAINER, + prefix__net_contains=str(address.prefix.ip), + ) + ) + if not prefixes.exists(): + draw_progress(i, total, 50) + continue + + address.parent = prefixes.last() + address.save() + draw_progress(i, total, 50) + end = time.time() + print(f"\r\nElapsed Time: {end - start:.2f}s") + + +def unset_prefix_parent(apps, schema_editor): + Prefix = apps.get_model('ipam', 'Prefix') + Prefix.objects.update(parent=None) + + class Migration(migrations.Migration): dependencies = [ - ('ipam', '0081_remove_service_device_virtual_machine_add_parent_gfk_index'), + ('ipam', '0082_ipaddress_iprange_prefix_parent'), ] operations = [ - migrations.AddField( - model_name='ipaddress', - name='prefix', - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name='ip_addresses', - to='ipam.prefix', - ), - ), - migrations.AddField( - model_name='iprange', - name='prefix', - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name='ip_ranges', - to='ipam.prefix', - ), - ), - migrations.AddField( - model_name='prefix', - name='aggregate', - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name='prefixes', - to='ipam.aggregate', - ), - ), migrations.RunPython(set_ipaddress_prefix, unset_ipaddress_prefix), migrations.RunPython(set_iprange_prefix, unset_iprange_prefix), migrations.RunPython(set_prefix_aggregate, unset_prefix_aggregate), + migrations.RunPython(set_prefix_parent, unset_prefix_parent), ] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index a3f5451b2..45bce76c9 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -6,6 +6,7 @@ from django.db.models import F from django.db.models.functions import Cast from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ +from netaddr.ip import IPNetwork from core.models import ObjectType from dcim.models.mixins import CachedScopeMixin @@ -206,12 +207,20 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary """ aggregate = models.ForeignKey( to='ipam.Aggregate', - on_delete=models.PROTECT, + on_delete=models.SET_NULL, related_name='prefixes', blank=True, null=True, verbose_name=_('aggregate') ) + parent = models.ForeignKey( + to='ipam.Prefix', + on_delete=models.SET_NULL, + related_name='children', + blank=True, + null=True, + verbose_name=_('Prefix') + ) prefix = IPNetworkField( verbose_name=_('prefix'), help_text=_('IPv4 or IPv6 network with mask') @@ -299,6 +308,8 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary super().clean() if self.prefix: + if not isinstance(self.prefix, IPNetwork): + self.prefix = IPNetwork(self.prefix) # /0 masks are not acceptable if self.prefix.prefixlen == 0: @@ -306,6 +317,17 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary 'prefix': _("Cannot create prefix with /0 mask.") }) + if self.parent: + if self.prefix not in self.parent.prefix: + raise ValidationError({ + 'parent': _("Prefix must be part of parent prefix.") + }) + + if self.parent.status != PrefixStatusChoices.STATUS_CONTAINER and self.vrf != self.parent.vrf: + raise ValidationError({ + 'vrf': _("VRF must match the parent VRF.") + }) + # Enforce unique IP space (if applicable) if (self.vrf is None and get_config().ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique): duplicate_prefixes = self.get_duplicates() @@ -319,6 +341,14 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary }) def save(self, *args, **kwargs): + vrf_id = self.vrf.pk if self.vrf else None + + if not self.pk and not self.parent: + parent = self.find_parent_prefix(self) + self.parent = parent + elif self.parent and (self.prefix != self._prefix or vrf_id != self._vrf_id): + parent = self.find_parent_prefix(self) + self.parent = parent if isinstance(self.prefix, netaddr.IPNetwork): @@ -483,6 +513,20 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary return min(utilization, 100) + @classmethod + def find_parent_prefix(cls, network): + prefixes = Prefix.objects.filter( + models.Q( + vrf=network.vrf, + prefix__net_contains=str(network) + ) | models.Q( + vrf=None, + status=PrefixStatusChoices.STATUS_CONTAINER, + prefix__net_contains=str(network), + ) + ) + return prefixes.last() + class IPRange(ContactsMixin, PrimaryModel): """ @@ -490,7 +534,7 @@ class IPRange(ContactsMixin, PrimaryModel): """ prefix = models.ForeignKey( to='ipam.Prefix', - on_delete=models.PROTECT, + on_delete=models.SET_NULL, related_name='ip_ranges', null=True, blank=True, @@ -565,6 +609,27 @@ class IPRange(ContactsMixin, PrimaryModel): super().clean() if self.start_address and self.end_address: + # If prefix is set, validate suitability + if self.prefix: + # Check that start address and end address are within the prefix range + if self.start_address not in self.prefix.prefix and self.end_address not in self.prefix.prefix: + raise ValidationError({ + 'start_address': _("Start address must be part of the selected prefix"), + 'end_address': _("End address must be part of the selected prefix.") + }) + elif self.start_address not in self.prefix.prefix: + raise ValidationError({ + 'start_address': _("Start address must be part of the selected prefix") + }) + elif self.end_address not in self.prefix.prefix: + raise ValidationError({ + 'end_address': _("End address must be part of the selected prefix.") + }) + # Check that VRF matches prefix VRF + if self.vrf != self.prefix.vrf: + raise ValidationError({ + 'vrf': _("VRF must match the prefix VRF.") + }) # Check that start & end IP versions match if self.start_address.version != self.end_address.version: @@ -828,6 +893,7 @@ class IPAddress(ContactsMixin, PrimaryModel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self._address = self.address # Denote the original assigned object (if any) for validation in clean() self._original_assigned_object_id = self.__dict__.get('assigned_object_id') self._original_assigned_object_type_id = self.__dict__.get('assigned_object_type_id') @@ -869,11 +935,16 @@ class IPAddress(ContactsMixin, PrimaryModel): super().clean() if self.address: + # If prefix is set, validate suitability if self.prefix: if self.address not in self.prefix.prefix: raise ValidationError({ 'prefix': _("IP address must be part of the selected prefix.") }) + if self.vrf != self.prefix.vrf: + raise ValidationError({ + 'vrf': _("IP address VRF must match the prefix VRF.") + }) # /0 masks are not acceptable if self.address.prefixlen == 0: diff --git a/netbox/ipam/search.py b/netbox/ipam/search.py index 953cb5fb9..664165d73 100644 --- a/netbox/ipam/search.py +++ b/netbox/ipam/search.py @@ -66,10 +66,11 @@ class IPRangeIndex(SearchIndex): fields = ( ('start_address', 100), ('end_address', 300), + ('prefix', 400), ('description', 500), ('comments', 5000), ) - display_attrs = ('vrf', 'tenant', 'status', 'role', 'description') + display_attrs = ('prefix', 'vrf', 'tenant', 'status', 'role', 'description') @register_search @@ -77,10 +78,12 @@ class PrefixIndex(SearchIndex): model = models.Prefix fields = ( ('prefix', 110), + ('parent', 200), + ('aggregate', 300), ('description', 500), ('comments', 5000), ) - display_attrs = ('scope', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description') + display_attrs = ('scope', 'aggregate', 'parent', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description') @register_search diff --git a/netbox/ipam/signals.py b/netbox/ipam/signals.py index 25fde9ff2..450e89758 100644 --- a/netbox/ipam/signals.py +++ b/netbox/ipam/signals.py @@ -1,10 +1,12 @@ from django.db.models import Q from django.db.models.signals import post_delete, post_save, pre_delete from django.dispatch import receiver +from netaddr.ip import IPNetwork from dcim.models import Device from virtualization.models import VirtualMachine -from .models import IPAddress, Prefix +from .choices import PrefixStatusChoices +from .models import IPAddress, Prefix, IPRange def update_parents_children(prefix): @@ -46,6 +48,7 @@ def update_ipaddress_prefix(prefix, delete=False): Q(address__net_contained_or_equal=prefix.prefix, vrf=prefix.vrf) | Q(prefix=prefix) ) + for address in addresses: if not address.prefix or (prefix.prefix in address.prefix.prefix and address.address in prefix.prefix): # Set to new Prefix as the prefix is a child of the old prefix and the address is contained in the @@ -65,6 +68,126 @@ def update_ipaddress_prefix(prefix, delete=False): IPAddress.objects.bulk_update(addresses, ['prefix'], batch_size=100) +def update_iprange_prefix(prefix, delete=False): + if delete: + # Get all possible addresses + addresses = IPRange.objects.filter(prefix=prefix) + # Find a new containing prefix + prefix = Prefix.objects.filter( + prefix__net_contains_or_equals=prefix.prefix, + vrf=prefix.vrf + ).exclude(pk=prefix.pk).last() + + for address in addresses: + # Set contained addresses to the containing prefix if it exists + address.prefix = prefix + else: + # Get all possible modified addresses + addresses = IPRange.objects.filter( + Q(start_address__net_contained_or_equal=prefix.prefix, vrf=prefix.vrf) | + Q(prefix=prefix) + ) + + for address in addresses: + if not address.prefix or ( + prefix.prefix in address.prefix.prefix and address.start_address in prefix.prefix and + address.end_address in prefix.prefix + ): + # Set to new Prefix as the prefix is a child of the old prefix and the address is contained in the + # prefix + address.prefix = prefix + elif address.prefix and address.address not in prefix.prefix: + # Find a new prefix as the prefix no longer contains the address + address.prefix = Prefix.objects.filter(Q(prefix__net_contains_or_equals=address.start_address) & + Q(prefix__net_contains_or_equals=address.end_address), + vrf=prefix.vrf + ).last() + else: + # No-OP as the prefix does not require modification + pass + + # Update the addresses + IPAddress.objects.bulk_update(addresses, ['prefix'], batch_size=100) + + +def update_prefix_parents(prefix, delete=False): + if delete: + # Get all possible addresses + prefixes = prefix.children.all() + + for pfx in prefixes: + parent = Prefix.objects.exclude(pk=pfx.pk).exclude(pk=prefix.pk).filter( + Q( + vrf=pfx.vrf, + prefix__net_contains=str(pfx.prefix) + ) | Q( + vrf=None, + status=PrefixStatusChoices.STATUS_CONTAINER, + prefix__net_contains=str(pfx.prefix), + ) + ).last() + # Set contained addresses to the containing prefix if it exists + pfx.parent = parent + else: + # Get all possible addresses + prefixes = prefix.children.all() | Prefix.objects.filter( + Q( + parent=prefix.parent, + vrf=prefix.vrf, + prefix__net_contained=str(prefix.prefix) + ) | Q( + parent=prefix.parent, + vrf=None, + status=PrefixStatusChoices.STATUS_CONTAINER, + prefix__net_contained=str(prefix.prefix), + ) + ) + + if isinstance(prefix.prefix, str): + prefix.prefix = IPNetwork(prefix.prefix) + for pfx in prefixes: + if isinstance(pfx.prefix, str): + pfx.prefix = IPNetwork(pfx.prefix) + + if pfx.parent == prefix and pfx.prefix.ip not in prefix.prefix: + # Find new parents for orphaned prefixes + parent = Prefix.objects.exclude(pk=pfx.pk).filter( + Q( + vrf=pfx.vrf, + prefix__net_contains=str(pfx.prefix) + ) | Q( + vrf=None, + status=PrefixStatusChoices.STATUS_CONTAINER, + prefix__net_contains=str(pfx.prefix), + ) + ).last() + # Set contained addresses to the containing prefix if it exists + pfx.parent = parent + elif pfx.parent == prefix and pfx.vrf != prefix.vrf: + # Find new parents for orphaned prefixes + parent = Prefix.objects.exclude(pk=pfx.pk).filter( + Q( + vrf=pfx.vrf, + prefix__net_contains=str(pfx.prefix) + ) | Q( + vrf=None, + status=PrefixStatusChoices.STATUS_CONTAINER, + prefix__net_contains=str(pfx.prefix), + ) + ).last() + # Set contained addresses to the containing prefix if it exists + pfx.parent = parent + elif pfx.parent != prefix and pfx.vrf == prefix.vrf and pfx.prefix in prefix.prefix: + # Set the parent to the prefix + pfx.parent = prefix + else: + # No-OP as the prefix does not require modification + pass + + # Update the prefixes + Prefix.objects.bulk_update(prefixes, ['parent'], batch_size=100) + + @receiver(post_save, sender=Prefix) def handle_prefix_saved(instance, created, **kwargs): @@ -72,6 +195,7 @@ def handle_prefix_saved(instance, created, **kwargs): if created or instance.vrf_id != instance._vrf_id or instance.prefix != instance._prefix: update_ipaddress_prefix(instance) + update_prefix_parents(instance) update_parents_children(instance) update_children_depth(instance) @@ -85,6 +209,7 @@ def handle_prefix_saved(instance, created, **kwargs): @receiver(pre_delete, sender=Prefix) def pre_handle_prefix_deleted(instance, **kwargs): update_ipaddress_prefix(instance, True) + update_prefix_parents(instance, delete=True) @receiver(post_delete, sender=Prefix) @@ -93,6 +218,7 @@ def handle_prefix_deleted(instance, **kwargs): update_parents_children(instance) update_children_depth(instance) update_ipaddress_prefix(instance, delete=True) + update_prefix_parents(instance, delete=True) @receiver(pre_delete, sender=IPAddress) diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index c0d7a1c2f..967fa009d 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -155,6 +155,7 @@ class PrefixUtilizationColumn(columns.UtilizationColumn): class PrefixTable(TenancyColumnsMixin, NetBoxTable): + # TODO: Alter for parent prefix prefix = columns.TemplateColumn( verbose_name=_('Prefix'), template_code=PREFIX_LINK_WITH_DEPTH, @@ -253,6 +254,7 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable): # IP ranges # class IPRangeTable(TenancyColumnsMixin, NetBoxTable): + # TODO: Alter for prefix start_address = tables.Column( verbose_name=_('Start address'), linkify=True @@ -309,6 +311,7 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable): # class IPAddressTable(TenancyColumnsMixin, NetBoxTable): + # TODO: Alter for prefix address = tables.TemplateColumn( template_code=IPADDRESS_LINK, verbose_name=_('IP Address') diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 4b9b340c4..907924f25 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -356,6 +356,7 @@ class RoleTest(APIViewTestCases.APIViewTestCase): class PrefixTest(APIViewTestCases.APIViewTestCase): model = Prefix + # TODO: Alter for parent prefix brief_fields = ['_depth', 'description', 'display', 'family', 'id', 'prefix', 'url'] create_data = [ { @@ -535,6 +536,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase): class IPRangeTest(APIViewTestCases.APIViewTestCase): model = IPRange + # TODO: Alter for parent prefix brief_fields = ['description', 'display', 'end_address', 'family', 'id', 'start_address', 'url'] create_data = [ { @@ -634,6 +636,7 @@ class IPRangeTest(APIViewTestCases.APIViewTestCase): class IPAddressTest(APIViewTestCases.APIViewTestCase): model = IPAddress + # TODO: Alter for parent prefix brief_fields = ['address', 'description', 'display', 'family', 'id', 'url'] create_data = [ { diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index c1b4acf60..94cb39a51 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -901,6 +901,10 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + # TODO: Test for parent prefix + # TODO: Test for children? + # TODO: Test for aggregate + class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests): queryset = IPRange.objects.all() @@ -1079,6 +1083,7 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_parent(self): + # TODO: Alter for prefix params = {'parent': ['10.0.1.0/24', '10.0.2.0/24']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) params = {'parent': ['10.0.1.0/25']} # Range 10.0.1.100-199 is not fully contained by 10.0.1.0/25 @@ -1315,6 +1320,7 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_parent(self): + # TODO: Alter for prefix params = {'parent': ['10.0.0.0/30', '2001:db8::/126']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index ec191edc2..c5efb8efa 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -39,6 +39,26 @@ class TestAggregate(TestCase): class TestIPRange(TestCase): + @classmethod + def setUpTestData(cls): + cls.vrf = VRF.objects.create(name='VRF A', rd='1:1') + + cls.prefixes = ( + + # IPv4 + Prefix(prefix='192.0.0.0/16'), + Prefix(prefix='192.0.2.0/24'), + Prefix(prefix='192.0.0.0/16', vrf=cls.vrf), + + # IPv6 + Prefix(prefix='2001:db8::/32'), + Prefix(prefix='2001:db8::/64'), + + ) + + for prefix in cls.prefixes: + prefix.clean() + prefix.save() def test_overlapping_range(self): iprange_192_168 = IPRange.objects.create( @@ -87,6 +107,69 @@ class TestIPRange(TestCase): ) iprange_4_198_201.clean() + def test_parent_prefix(self): + ranges = ( + IPRange( + start_address=IPNetwork('192.0.0.1/24'), + end_address=IPNetwork('192.0.0.254/24'), + prefix=self.prefixes[0] + ), + IPRange( + start_address=IPNetwork('192.0.2.1/24'), + end_address=IPNetwork('192.0.2.254/24'), + prefix=self.prefixes[1] + ), + IPRange( + start_address=IPNetwork('192.0.2.1/24'), + end_address=IPNetwork('192.0.2.254/24'), + vrf=self.vrf, + prefix=self.prefixes[2] + ), + IPRange( + start_address=IPNetwork('2001:db8::/64'), + end_address=IPNetwork('2001:db8::ffff/64'), + prefix=self.prefixes[4] + ), + IPRange( + start_address=IPNetwork('2001:db8:2::/64'), + end_address=IPNetwork('2001:db8:2::ffff/64'), + prefix=self.prefixes[3] + ), + ) + + for range in ranges: + range.clean() + range.save() + + self.assertEqual(ranges[0].prefix, self.prefixes[0]) + self.assertEqual(ranges[1].prefix, self.prefixes[1]) + self.assertEqual(ranges[2].prefix, self.prefixes[2]) + self.assertEqual(ranges[3].prefix, self.prefixes[4]) + + def test_parent_prefix_change(self): + + range = IPRange( + start_address=IPNetwork('192.0.1.1/24'), + end_address=IPNetwork('192.0.1.254/24'), + prefix=self.prefixes[0] + ) + range.clean() + range.save() + + prefix = Prefix(prefix='192.0.1.0/17') + prefix.clean() + prefix.save() + + range.refresh_from_db() + + self.assertEqual(range.prefix, prefix) + + # TODO: Prefix Altered + # TODO: Prefix Deleted + + # TODO: Prefix falls outside range + # TODO: Prefix VRF does not match range VRF + class TestPrefix(TestCase): @@ -344,17 +427,21 @@ class TestPrefixHierarchy(TestCase): prefixes = ( # IPv4 - Prefix(prefix='10.0.0.0/8', _depth=0, _children=2), - Prefix(prefix='10.0.0.0/16', _depth=1, _children=1), - Prefix(prefix='10.0.0.0/24', _depth=2, _children=0), + Prefix(prefix='10.0.0.0/8'), + Prefix(prefix='10.0.0.0/16'), + Prefix(prefix='10.0.0.0/24'), + Prefix(prefix='192.168.0.0/16'), # IPv6 - Prefix(prefix='2001:db8::/32', _depth=0, _children=2), - Prefix(prefix='2001:db8::/40', _depth=1, _children=1), - Prefix(prefix='2001:db8::/48', _depth=2, _children=0), + Prefix(prefix='2001:db8::/32'), + Prefix(prefix='2001:db8::/40'), + Prefix(prefix='2001:db8::/48'), ) - Prefix.objects.bulk_create(prefixes) + + for prefix in prefixes: + prefix.clean() + prefix.save() def test_create_prefix4(self): # Create 10.0.0.0/12 @@ -362,15 +449,19 @@ class TestPrefixHierarchy(TestCase): prefixes = Prefix.objects.filter(prefix__family=4) self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8')) + self.assertEqual(prefixes[0].parent, None) self.assertEqual(prefixes[0]._depth, 0) self.assertEqual(prefixes[0]._children, 3) self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/12')) + self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8')) self.assertEqual(prefixes[1]._depth, 1) self.assertEqual(prefixes[1]._children, 2) self.assertEqual(prefixes[2].prefix, IPNetwork('10.0.0.0/16')) + self.assertEqual(prefixes[2].parent.prefix, IPNetwork('10.0.0.0/12')) self.assertEqual(prefixes[2]._depth, 2) self.assertEqual(prefixes[2]._children, 1) self.assertEqual(prefixes[3].prefix, IPNetwork('10.0.0.0/24')) + self.assertEqual(prefixes[3].parent.prefix, IPNetwork('10.0.0.0/16')) self.assertEqual(prefixes[3]._depth, 3) self.assertEqual(prefixes[3]._children, 0) @@ -380,15 +471,19 @@ class TestPrefixHierarchy(TestCase): prefixes = Prefix.objects.filter(prefix__family=6) self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32')) + self.assertEqual(prefixes[0].parent, None) self.assertEqual(prefixes[0]._depth, 0) self.assertEqual(prefixes[0]._children, 3) self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/36')) + self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32')) self.assertEqual(prefixes[1]._depth, 1) self.assertEqual(prefixes[1]._children, 2) self.assertEqual(prefixes[2].prefix, IPNetwork('2001:db8::/40')) + self.assertEqual(prefixes[2].parent.prefix, IPNetwork('2001:db8::/36')) self.assertEqual(prefixes[2]._depth, 2) self.assertEqual(prefixes[2]._children, 1) self.assertEqual(prefixes[3].prefix, IPNetwork('2001:db8::/48')) + self.assertEqual(prefixes[3].parent.prefix, IPNetwork('2001:db8::/40')) self.assertEqual(prefixes[3]._depth, 3) self.assertEqual(prefixes[3]._children, 0) @@ -400,12 +495,15 @@ class TestPrefixHierarchy(TestCase): prefixes = Prefix.objects.filter(prefix__family=4) self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8')) + self.assertEqual(prefixes[0].parent, None) self.assertEqual(prefixes[0]._depth, 0) self.assertEqual(prefixes[0]._children, 2) self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/12')) + self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8')) self.assertEqual(prefixes[1]._depth, 1) self.assertEqual(prefixes[1]._children, 1) self.assertEqual(prefixes[2].prefix, IPNetwork('10.0.0.0/16')) + self.assertEqual(prefixes[2].parent.prefix, IPNetwork('10.0.0.0/12')) self.assertEqual(prefixes[2]._depth, 2) self.assertEqual(prefixes[2]._children, 0) @@ -417,12 +515,15 @@ class TestPrefixHierarchy(TestCase): prefixes = Prefix.objects.filter(prefix__family=6) self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32')) + self.assertEqual(prefixes[0].parent, None) self.assertEqual(prefixes[0]._depth, 0) self.assertEqual(prefixes[0]._children, 2) self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/36')) + self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32')) self.assertEqual(prefixes[1]._depth, 1) self.assertEqual(prefixes[1]._children, 1) self.assertEqual(prefixes[2].prefix, IPNetwork('2001:db8::/40')) + self.assertEqual(prefixes[2].parent.prefix, IPNetwork('2001:db8::/36')) self.assertEqual(prefixes[2]._depth, 2) self.assertEqual(prefixes[2]._children, 0) @@ -437,14 +538,17 @@ class TestPrefixHierarchy(TestCase): prefixes = Prefix.objects.filter(vrf__isnull=True, prefix__family=4) self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8')) + self.assertEqual(prefixes[0].parent, None) self.assertEqual(prefixes[0]._depth, 0) self.assertEqual(prefixes[0]._children, 1) self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/24')) + self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8')) self.assertEqual(prefixes[1]._depth, 1) self.assertEqual(prefixes[1]._children, 0) prefixes = Prefix.objects.filter(vrf=vrf) self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/16')) + self.assertEqual(prefixes[0].parent, None) self.assertEqual(prefixes[0]._depth, 0) self.assertEqual(prefixes[0]._children, 0) @@ -459,14 +563,17 @@ class TestPrefixHierarchy(TestCase): prefixes = Prefix.objects.filter(vrf__isnull=True, prefix__family=6) self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32')) + self.assertEqual(prefixes[0].parent, None) self.assertEqual(prefixes[0]._depth, 0) self.assertEqual(prefixes[0]._children, 1) self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/48')) + self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32')) self.assertEqual(prefixes[1]._depth, 1) self.assertEqual(prefixes[1]._children, 0) prefixes = Prefix.objects.filter(vrf=vrf) self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/40')) + self.assertEqual(prefixes[0].parent, None) self.assertEqual(prefixes[0]._depth, 0) self.assertEqual(prefixes[0]._children, 0) @@ -476,9 +583,11 @@ class TestPrefixHierarchy(TestCase): prefixes = Prefix.objects.filter(prefix__family=4) self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8')) + self.assertEqual(prefixes[0].parent, None) self.assertEqual(prefixes[0]._depth, 0) self.assertEqual(prefixes[0]._children, 1) self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/24')) + self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8')) self.assertEqual(prefixes[1]._depth, 1) self.assertEqual(prefixes[1]._children, 0) @@ -488,9 +597,11 @@ class TestPrefixHierarchy(TestCase): prefixes = Prefix.objects.filter(prefix__family=6) self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32')) + self.assertEqual(prefixes[0].parent, None) self.assertEqual(prefixes[0]._depth, 0) self.assertEqual(prefixes[0]._children, 1) self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/48')) + self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32')) self.assertEqual(prefixes[1]._depth, 1) self.assertEqual(prefixes[1]._children, 0) @@ -500,15 +611,20 @@ class TestPrefixHierarchy(TestCase): prefixes = Prefix.objects.filter(prefix__family=4) self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8')) + self.assertEqual(prefixes[0].parent, None) self.assertEqual(prefixes[0]._depth, 0) self.assertEqual(prefixes[0]._children, 3) self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/16')) + self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8')) self.assertEqual(prefixes[1]._depth, 1) self.assertEqual(prefixes[1]._children, 1) self.assertEqual(prefixes[2].prefix, IPNetwork('10.0.0.0/16')) + self.assertEqual(prefixes[2].parent.prefix, IPNetwork('10.0.0.0/8')) self.assertEqual(prefixes[2]._depth, 1) self.assertEqual(prefixes[2]._children, 1) self.assertEqual(prefixes[3].prefix, IPNetwork('10.0.0.0/24')) + # TODO: How to we resolve the parent for duplicate prefixes + self.assertEqual(prefixes[3].parent.prefix, IPNetwork('10.0.0.0/16')) self.assertEqual(prefixes[3]._depth, 2) self.assertEqual(prefixes[3]._children, 0) @@ -518,20 +634,48 @@ class TestPrefixHierarchy(TestCase): prefixes = Prefix.objects.filter(prefix__family=6) self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32')) + self.assertEqual(prefixes[0].parent, None) self.assertEqual(prefixes[0]._depth, 0) self.assertEqual(prefixes[0]._children, 3) self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/40')) + self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32')) self.assertEqual(prefixes[1]._depth, 1) self.assertEqual(prefixes[1]._children, 1) self.assertEqual(prefixes[2].prefix, IPNetwork('2001:db8::/40')) + self.assertEqual(prefixes[2].parent.prefix, IPNetwork('2001:db8::/32')) self.assertEqual(prefixes[2]._depth, 1) self.assertEqual(prefixes[2]._children, 1) self.assertEqual(prefixes[3].prefix, IPNetwork('2001:db8::/48')) + self.assertEqual(prefixes[3].parent.prefix, IPNetwork('2001:db8::/40')) self.assertEqual(prefixes[3]._depth, 2) self.assertEqual(prefixes[3]._children, 0) class TestIPAddress(TestCase): + """ + Test the automatic updating of depth and child count in response to changes made within + the prefix hierarchy. + """ + @classmethod + def setUpTestData(cls): + cls.vrf = VRF.objects.create(name='VRF A', rd='1:1') + + cls.prefixes = ( + + # IPv4 + Prefix(prefix='192.0.0.0/16'), + Prefix(prefix='192.0.2.0/24'), + Prefix(prefix='192.0.0.0/16', vrf=cls.vrf), + + # IPv6 + Prefix(prefix='2001:db8::/32'), + Prefix(prefix='2001:db8::/64'), + + ) + + for prefix in cls.prefixes: + prefix.clean() + prefix.save() def test_get_duplicates(self): ips = IPAddress.objects.bulk_create(( @@ -543,6 +687,44 @@ class TestIPAddress(TestCase): self.assertSetEqual(set(duplicate_ip_pks), {ips[1].pk, ips[2].pk}) + def test_parent_prefix(self): + ips = ( + IPAddress(address=IPNetwork('192.0.0.1/24'), prefix=self.prefixes[0]), + IPAddress(address=IPNetwork('192.0.2.1/24'), prefix=self.prefixes[1]), + IPAddress(address=IPNetwork('192.0.2.1/24'), vrf=self.vrf, prefix=self.prefixes[2]), + IPAddress(address=IPNetwork('2001:db8::/64'), prefix=self.prefixes[4]), + IPAddress(address=IPNetwork('2001:db8:2::/64'), prefix=self.prefixes[3]), + ) + + for ip in ips: + ip.clean() + ip.save() + + self.assertEqual(ips[0].prefix, self.prefixes[0]) + self.assertEqual(ips[1].prefix, self.prefixes[1]) + self.assertEqual(ips[2].prefix, self.prefixes[2]) + self.assertEqual(ips[3].prefix, self.prefixes[4]) + self.assertEqual(ips[4].prefix, self.prefixes[3]) + + def test_parent_prefix_change(self): + ip = IPAddress(address=IPNetwork('192.0.1.1/24'), prefix=self.prefixes[0]) + ip.clean() + ip.save() + + prefix = Prefix(prefix='192.0.1.0/17') + prefix.clean() + prefix.save() + + ip.refresh_from_db() + + self.assertEqual(ip.prefix, prefix) + + # TODO: Prefix Altered + # TODO: Prefix Deleted + + # TODO: Prefix does not contain IP Address + # TODO: Prefix VRF does not match IP Address VRF + # # Uniqueness enforcement tests # diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 8666eafdd..a9b0dd227 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -421,6 +421,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): tags = create_tags('Alpha', 'Bravo', 'Charlie') + # TODO: Alter for prefix cls.form_data = { 'prefix': IPNetwork('192.0.2.0/24'), 'scope_type': ContentType.objects.get_for_model(Site).pk, @@ -436,6 +437,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): } site = sites[0].pk + # TODO: Alter for prefix cls.csv_data = ( "vrf,prefix,status,scope_type,scope_id", f"VRF 1,10.4.0.0/16,active,dcim.site,{site}", @@ -443,6 +445,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): f"VRF 1,10.6.0.0/16,active,dcim.site,{site}", ) + # TODO: Alter for prefix cls.csv_update_data = ( "id,description,status", f"{prefixes[0].pk},New description 7,{PrefixStatusChoices.STATUS_RESERVED}", @@ -450,6 +453,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): f"{prefixes[2].pk},New description 9,{PrefixStatusChoices.STATUS_RESERVED}", ) + # TODO: Alter for prefix cls.bulk_edit_data = { 'vrf': vrfs[1].pk, 'tenant': None, @@ -594,6 +598,7 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase): tags = create_tags('Alpha', 'Bravo', 'Charlie') + # TODO: Alter for prefix cls.form_data = { 'start_address': IPNetwork('192.0.5.10/24'), 'end_address': IPNetwork('192.0.5.100/24'), @@ -607,6 +612,7 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'tags': [t.pk for t in tags], } + # TODO: Alter for prefix cls.csv_data = ( "vrf,start_address,end_address,status", "VRF 1,10.1.0.1/16,10.1.9.254/16,active", @@ -614,6 +620,7 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase): "VRF 1,10.3.0.1/16,10.3.9.254/16,active", ) + # TODO: Alter for prefix cls.csv_update_data = ( "id,description,status", f"{ip_ranges[0].pk},New description 7,{IPRangeStatusChoices.STATUS_RESERVED}", @@ -621,6 +628,7 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase): f"{ip_ranges[2].pk},New description 9,{IPRangeStatusChoices.STATUS_RESERVED}", ) + # TODO: Alter for prefix cls.bulk_edit_data = { 'vrf': vrfs[1].pk, 'tenant': None, @@ -687,6 +695,7 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase): ), ) FHRPGroup.objects.bulk_create(fhrp_groups) + # TODO: Alter for prefix cls.form_data = { 'vrf': vrfs[1].pk, 'address': IPNetwork('192.0.2.99/24'), @@ -699,6 +708,7 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'tags': [t.pk for t in tags], } + # TODO: Alter for prefix cls.csv_data = ( "vrf,address,status,fhrp_group", "VRF 1,192.0.2.4/24,active,FHRP Group 1", @@ -706,6 +716,7 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase): "VRF 1,192.0.2.6/24,active,FHRP Group 3", ) + # TODO: Alter for prefix cls.csv_update_data = ( "id,description,status", f"{ipaddresses[0].pk},New description 7,{IPAddressStatusChoices.STATUS_RESERVED}", @@ -713,6 +724,7 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase): f"{ipaddresses[2].pk},New description 9,{IPAddressStatusChoices.STATUS_RESERVED}", ) + # TODO: Alter for prefix cls.bulk_edit_data = { 'vrf': vrfs[1].pk, 'tenant': None,