diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 377fd9eaa..e3ce962d3 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -155,18 +155,6 @@ class RoleImportForm(OrganizationalModelImportForm): class PrefixImportForm(ScopedImportForm, PrimaryModelImportForm): - aggregate = CSVModelChoiceField( - label=_('Aggregate'), - queryset=Aggregate.objects.all(), - to_field_name='prefix', - required=False - ) - parent = CSVModelChoiceField( - label=_('Prefix'), - queryset=Prefix.objects.all(), - to_field_name='prefix', - required=False - ) vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -250,26 +238,8 @@ class PrefixImportForm(ScopedImportForm, PrimaryModelImportForm): queryset = self.fields['vlan'].queryset.filter(query) self.fields['vlan'].queryset = queryset - # Limit Prefix queryset by assigned vrf - vrf = data.get('vrf') - query = Q() - if vrf: - query &= Q(**{ - f"vrf__{self.fields['vrf'].to_field_name}": vrf - }) - - queryset = self.fields['parent'].queryset.filter(query) - self.fields['parent'].queryset = queryset - class IPRangeImportForm(PrimaryModelImportForm): - prefix = CSVModelChoiceField( - label=_('Prefix'), - queryset=Prefix.objects.all(), - to_field_name='prefix', - required=True, - help_text=_('Assigned prefix') - ) vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -304,29 +274,8 @@ class IPRangeImportForm(PrimaryModelImportForm): 'description', 'owner', 'comments', 'tags', ) - def __init__(self, data=None, *args, **kwargs): - super().__init__(data, *args, **kwargs) - - # Limit Prefix queryset by assigned vrf - vrf = data.get('vrf') - query = Q() - if vrf: - query &= Q(**{ - f"vrf__{self.fields['vrf'].to_field_name}": vrf - }) - - queryset = self.fields['prefix'].queryset.filter(query) - self.fields['prefix'].queryset = queryset - class IPAddressImportForm(PrimaryModelImportForm): - prefix = CSVModelChoiceField( - label=_('Prefix'), - queryset=Prefix.objects.all(), - required=False, - to_field_name='prefix', - help_text=_('Assigned prefix') - ) vrf = CSVModelChoiceField( label=_('VRF'), queryset=VRF.objects.all(), @@ -403,15 +352,6 @@ class IPAddressImportForm(PrimaryModelImportForm): if data: - # Limit Prefix queryset by assigned vrf - vrf = data.get('vrf') - query = Q() - if vrf: - query &= Q(**{f"vrf__{self.fields['vrf'].to_field_name}": vrf}) - - queryset = self.fields['prefix'].queryset.filter(query) - self.fields['prefix'].queryset = queryset - # Limit interface queryset by assigned device if data.get('device'): self.fields['interface'].queryset = Interface.objects.filter( diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index e4e75683c..404d07191 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -241,11 +241,6 @@ class PrefixForm(TenancyForm, ScopedForm, PrimaryModelForm): class IPRangeForm(TenancyForm, PrimaryModelForm): - prefix = DynamicModelChoiceField( - queryset=Prefix.objects.all(), - required=False, - label=_('Prefix') - ) vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -260,7 +255,7 @@ class IPRangeForm(TenancyForm, PrimaryModelForm): fieldsets = ( FieldSet( - 'prefix', 'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized', + 'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized', 'description', 'tags', name=_('IP Range') ), FieldSet('tenant_group', 'tenant', name=_('Tenancy')), @@ -269,21 +264,12 @@ class IPRangeForm(TenancyForm, PrimaryModelForm): class Meta: model = IPRange fields = [ - 'prefix', 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', + 'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_populated', 'mark_utilized', 'description', 'owner', 'comments', 'tags', ] class IPAddressForm(TenancyForm, PrimaryModelForm): - prefix = DynamicModelChoiceField( - queryset=Prefix.objects.all(), - required=False, - context={ - 'vrf': 'vrf', - }, - selector=True, - label=_('Prefix'), - ) interface = DynamicModelChoiceField( queryset=Interface.objects.all(), required=False, @@ -329,7 +315,7 @@ class IPAddressForm(TenancyForm, PrimaryModelForm): ) fieldsets = ( - FieldSet('prefix', 'address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')), + FieldSet('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')), FieldSet( TabbedGroups( @@ -345,7 +331,7 @@ class IPAddressForm(TenancyForm, PrimaryModelForm): class Meta: model = IPAddress fields = [ - 'prefix', 'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent', + 'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent', 'nat_inside', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags', ] @@ -471,15 +457,6 @@ class IPAddressForm(TenancyForm, PrimaryModelForm): class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm): - prefix = DynamicModelChoiceField( - queryset=Prefix.objects.all(), - required=False, - context={ - 'vrf': 'vrf', - }, - selector=True, - label=_('Prefix'), - ) vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -489,7 +466,7 @@ class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm): class Meta: model = IPAddress fields = [ - 'address', 'prefix', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags', + 'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags', ] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index d009118cd..20182cfd8 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -335,17 +335,6 @@ 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() @@ -359,13 +348,9 @@ 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) + if not self.pk or not self.parent or (self.prefix != self._prefix) or (self.vrf_id != self._vrf_id): + parent = self.find_parent_prefix(network=self.prefix, vrf=self.vrf, exclude=self.pk) self.parent = parent if isinstance(self.prefix, netaddr.IPNetwork): @@ -537,17 +522,40 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary return min(utilization, 100) @classmethod - def find_parent_prefix(cls, network): + def find_parent_prefix(cls, network, vrf=None, exclude=None): prefixes = Prefix.objects.filter( models.Q( - vrf=network.vrf, - prefix__net_contains=str(network.prefix) + vrf=vrf, + prefix__net_contains=str(network) ) | models.Q( vrf=None, status=PrefixStatusChoices.STATUS_CONTAINER, - prefix__net_contains=str(network.prefix), + prefix__net_contains=str(network), ) ) + if exclude: + prefixes = prefixes.exclude(pk=exclude) + return prefixes.last() + + @classmethod + def find_parent_prefix_range(cls, networks, vrf=None, exclude=None): + network_filter = models.Q() + for network in networks: + network_filter &= models.Q( + prefix__net_contains=network + ) + prefixes = Prefix.objects.filter( + models.Q( + network_filter, + vrf=vrf + ) | models.Q( + network_filter, + vrf=None, + status=PrefixStatusChoices.STATUS_CONTAINER, + ) + ) + if exclude: + prefixes = prefixes.exclude(pk=exclude) return prefixes.last() @@ -734,6 +742,12 @@ class IPRange(ContactsMixin, PrimaryModel): # Record the range's size (number of IP addresses) self.size = int(self.end_address.ip - self.start_address.ip) + 1 + # Set the parent prefix + self.prefix = Prefix.find_parent_prefix_range( + networks=[self.start_address.ip, self.end_address.ip], + vrf=self.vrf + ) + super().save(*args, **kwargs) @property @@ -828,14 +842,6 @@ class IPRange(ContactsMixin, PrimaryModel): return min(float(child_count) / self.size * 100, 100) - @classmethod - def find_prefix(self, address): - prefixes = Prefix.objects.filter( - models.Q(prefix__net_contains=address.start_address) & Q(prefix__net_contains=address.end_address), - vrf=address.vrf, - ) - return prefixes.last() - class IPAddress(ContactsMixin, PrimaryModel): """ @@ -1093,6 +1099,9 @@ class IPAddress(ContactsMixin, PrimaryModel): # Force dns_name to lowercase self.dns_name = self.dns_name.lower() + # Set the parent prefix + self.prefix = Prefix.find_parent_prefix(self.address.ip, vrf=self.vrf) + super().save(*args, **kwargs) def clone(self): diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 7a0680757..f8ef8fa16 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -457,12 +457,11 @@ class TestPrefix(TestCase): # Global container should return all children self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk, ips[2].pk, ips[3].pk}) - parent_prefix.prefix = '10.0.0.0/25' + parent_prefix.prefix = '10.0.0.0/23' parent_prefix.save() parent_prefix.refresh_from_db() child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()} - self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk}) @@ -853,7 +852,7 @@ class TestIPAddress(TestCase): 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]), + IPAddress(address=IPNetwork('2001:db8:2::/64')), ) for ip in ips: