mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-13 16:47:34 -06:00
More work on IP Address/Range and Prefix relationship
This commit is contained in:
parent
c5e7b21147
commit
b19f81cede
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
|
@ -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),
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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',
|
||||
),
|
||||
),
|
||||
]
|
@ -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),
|
||||
]
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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 = [
|
||||
{
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user