mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-13 08:44:51 -06:00
Merge 0d31449df8
into 875a641687
This commit is contained in:
commit
5d90fb8ce4
@ -60,18 +60,24 @@ class PrefixSerializer(NetBoxModelSerializer):
|
||||
vlan = VLANSerializer(nested=True, required=False, allow_null=True)
|
||||
status = ChoiceField(choices=PrefixStatusChoices, required=False)
|
||||
role = RoleSerializer(nested=True, required=False, allow_null=True)
|
||||
children = serializers.IntegerField(read_only=True)
|
||||
_children = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(read_only=True)
|
||||
prefix = IPNetworkField()
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'vrf', 'scope_type', 'scope_id', 'scope',
|
||||
'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'children', '_depth',
|
||||
'id', 'url', 'display_url', 'display', 'family', 'aggregate', 'parent', 'prefix', 'vrf', 'scope_type',
|
||||
'scope_id', 'scope', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_children', '_depth',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth')
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'aggregate', 'parent', 'prefix', 'description', '_depth')
|
||||
|
||||
def get_fields(self):
|
||||
fields = super(PrefixSerializer, self).get_fields()
|
||||
fields['parent'] = PrefixSerializer(nested=True, read_only=True)
|
||||
|
||||
return fields
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_scope(self, obj):
|
||||
@ -134,6 +140,7 @@ class AvailablePrefixSerializer(serializers.Serializer):
|
||||
#
|
||||
|
||||
class IPRangeSerializer(NetBoxModelSerializer):
|
||||
prefix = PrefixSerializer(nested=True, required=False, allow_null=True)
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
start_address = IPAddressField()
|
||||
end_address = IPAddressField()
|
||||
@ -145,11 +152,11 @@ class IPRangeSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = IPRange
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant',
|
||||
'status', 'role', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'start_address', 'end_address', 'size', 'vrf',
|
||||
'tenant', 'status', 'role', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'mark_populated', 'mark_utilized',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description')
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'start_address', 'end_address', 'description')
|
||||
|
||||
|
||||
#
|
||||
@ -157,6 +164,7 @@ class IPRangeSerializer(NetBoxModelSerializer):
|
||||
#
|
||||
|
||||
class IPAddressSerializer(NetBoxModelSerializer):
|
||||
prefix = PrefixSerializer(nested=True, required=False, allow_null=True)
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
address = IPAddressField()
|
||||
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
|
||||
@ -175,11 +183,11 @@ class IPAddressSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role',
|
||||
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'address', 'vrf', 'tenant', 'status', 'role',
|
||||
'assigned_object_type', 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside',
|
||||
'dns_name', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'address', 'description')
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'address', 'description')
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_assigned_object(self, obj):
|
||||
|
@ -330,6 +330,26 @@ class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet, C
|
||||
field_name='prefix',
|
||||
lookup_expr='net_mask_length__lte'
|
||||
)
|
||||
aggregate_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Aggregate.objects.all(),
|
||||
label=_('Aggregate'),
|
||||
)
|
||||
aggregate = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='aggregate__prefix',
|
||||
queryset=Aggregate.objects.all(),
|
||||
to_field_name='prefix',
|
||||
label=_('Aggregate (Prefix)'),
|
||||
)
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Prefix.objects.all(),
|
||||
label=_('Parent Prefix'),
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent__prefix',
|
||||
queryset=Prefix.objects.all(),
|
||||
to_field_name='prefix',
|
||||
label=_('Parent Prefix (Prefix)'),
|
||||
)
|
||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VRF.objects.all(),
|
||||
label=_('VRF'),
|
||||
@ -473,6 +493,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'),
|
||||
@ -557,6 +587,16 @@ class IPAddressFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFil
|
||||
method='search_by_parent',
|
||||
label=_('Parent prefix'),
|
||||
)
|
||||
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 (prefix)'),
|
||||
)
|
||||
address = MultiValueCharFilter(
|
||||
method='filter_address',
|
||||
label=_('Address'),
|
||||
|
@ -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,12 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
||||
|
||||
class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
|
||||
# TODO: Alter for prefix
|
||||
prefix = DynamicModelChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
label=_('Prefix')
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
@ -364,10 +372,10 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm):
|
||||
model = IPAddress
|
||||
fieldsets = (
|
||||
FieldSet('status', 'role', 'tenant', 'description'),
|
||||
FieldSet('vrf', 'mask_length', 'dns_name', name=_('Addressing')),
|
||||
FieldSet('prefix', 'vrf', 'mask_length', 'dns_name', name=_('Addressing')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
|
||||
'prefix', 'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
|
@ -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,14 @@ class IPRangeImportForm(NetBoxModelImportForm):
|
||||
|
||||
|
||||
class IPAddressImportForm(NetBoxModelImportForm):
|
||||
# TODO: Alter for prefix
|
||||
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(),
|
||||
@ -348,8 +359,8 @@ class IPAddressImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'fhrp_group',
|
||||
'is_primary', 'is_oob', 'dns_name', 'description', 'comments', 'tags',
|
||||
'prefix', 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface',
|
||||
'fhrp_group', 'is_primary', 'is_oob', 'dns_name', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
|
@ -204,6 +204,12 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModelFil
|
||||
choices=PREFIX_MASK_LENGTH_CHOICES,
|
||||
label=_('Mask length')
|
||||
)
|
||||
aggregate_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Aggregate.objects.all(),
|
||||
required=False,
|
||||
label=_('Aggregate'),
|
||||
null_option='Global'
|
||||
)
|
||||
vrf_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
@ -278,10 +284,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),
|
||||
@ -326,7 +340,7 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModel
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet(
|
||||
'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
|
||||
'prefix', 'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
|
||||
@ -334,7 +348,7 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModel
|
||||
FieldSet('device_id', 'virtual_machine_id', name=_('Device/VM')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'prefix_id', 'parent', 'status', 'role')
|
||||
parent = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
@ -354,6 +368,11 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, NetBoxModel
|
||||
choices=IPADDRESS_MASK_LENGTH_CHOICES,
|
||||
label=_('Mask length')
|
||||
)
|
||||
prefix_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
label=_('Prefix'),
|
||||
)
|
||||
vrf_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
|
@ -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,12 +279,21 @@ 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',
|
||||
]
|
||||
|
||||
|
||||
class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
prefix = DynamicModelChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
context={
|
||||
'vrf': 'vrf',
|
||||
},
|
||||
selector=True,
|
||||
label=_('Prefix'),
|
||||
)
|
||||
interface = DynamicModelChoiceField(
|
||||
queryset=Interface.objects.all(),
|
||||
required=False,
|
||||
@ -326,7 +340,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')),
|
||||
FieldSet('prefix', 'address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags', name=_('IP Address')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
FieldSet(
|
||||
TabbedGroups(
|
||||
@ -342,8 +356,8 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent', 'nat_inside',
|
||||
'tenant_group', 'tenant', 'description', 'comments', 'tags',
|
||||
'prefix', 'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent',
|
||||
'nat_inside', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -468,6 +482,15 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
|
||||
|
||||
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,
|
||||
@ -477,7 +500,7 @@ class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'address', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags',
|
||||
'address', 'prefix', 'vrf', 'status', 'role', 'dns_name', 'description', 'tenant_group', 'tenant', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
@ -145,6 +145,7 @@ class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin)
|
||||
|
||||
@strawberry_django.filter_type(models.IPAddress, lookups=True)
|
||||
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
prefix: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
vrf_id: ID | None = strawberry_django.filter_field()
|
||||
@ -196,6 +197,7 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
|
||||
@strawberry_django.filter_type(models.IPRange, lookups=True)
|
||||
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
prefix: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
start_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
end_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
@ -225,6 +227,10 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMi
|
||||
|
||||
@strawberry_django.filter_type(models.Prefix, lookups=True)
|
||||
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
aggregate: Annotated['AggregateFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
parent: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
prefix: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
vrf_id: ID | None = strawberry_django.filter_field()
|
||||
|
@ -144,6 +144,7 @@ class FHRPGroupAssignmentType(BaseObjectType):
|
||||
)
|
||||
class IPAddressType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType):
|
||||
address: str
|
||||
prefix: Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
nat_inside: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
@ -168,6 +169,7 @@ class IPAddressType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType):
|
||||
pagination=True
|
||||
)
|
||||
class IPRangeType(NetBoxObjectType, ContactsMixin):
|
||||
prefix: Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
start_address: str
|
||||
end_address: str
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
@ -182,6 +184,8 @@ class IPRangeType(NetBoxObjectType, ContactsMixin):
|
||||
pagination=True
|
||||
)
|
||||
class PrefixType(NetBoxObjectType, ContactsMixin, BaseIPAddressFamilyType):
|
||||
aggregate: Annotated["AggregateType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
parent: Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
prefix: str
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
|
@ -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',
|
||||
),
|
||||
),
|
||||
]
|
@ -0,0 +1,129 @@
|
||||
# Generated by Django 5.0.9 on 2025-02-20 16:49
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
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)
|
||||
sys.stdout.write('\r')
|
||||
sys.stdout.write(f"[{'=' * bar:{length}s}] {percent}%")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def set_prefix(apps, schema_editor, model, attr='address', parent_model='Prefix'):
|
||||
start = time.time()
|
||||
ChildModel = apps.get_model('ipam', model)
|
||||
ParentModel = apps.get_model('ipam', parent_model)
|
||||
|
||||
addresses = ChildModel.objects.all()
|
||||
total = addresses.count()
|
||||
if total == 0:
|
||||
return
|
||||
|
||||
print('\r\n')
|
||||
i = 0
|
||||
draw_progress(i, total, 50)
|
||||
for address in addresses:
|
||||
i += 1
|
||||
address_attr = getattr(address, attr)
|
||||
prefixes = ParentModel.objects.filter(
|
||||
prefix__net_contains_or_equals=str(address_attr.ip),
|
||||
prefix__net_mask_length__lte=address_attr.prefixlen,
|
||||
)
|
||||
if hasattr(ParentModel, 'vrf'):
|
||||
prefixes = prefixes.filter(vrf=address.vrf)
|
||||
address.prefix = prefixes.last()
|
||||
address.save()
|
||||
draw_progress(i, total, 50)
|
||||
|
||||
end = time.time()
|
||||
print(f"\r\nElapsed Time: {end - start:.2f}s")
|
||||
|
||||
|
||||
def set_ipaddress_prefix(apps, schema_editor):
|
||||
set_prefix(apps, schema_editor, 'IPAddress')
|
||||
|
||||
|
||||
def unset_ipaddress_prefix(apps, schema_editor):
|
||||
IPAddress = apps.get_model('ipam', 'IPAddress')
|
||||
IPAddress.objects.update(prefix=None)
|
||||
|
||||
|
||||
def set_iprange_prefix(apps, schema_editor):
|
||||
set_prefix(apps, schema_editor, 'IPRange', 'start_address')
|
||||
|
||||
|
||||
def unset_iprange_prefix(apps, schema_editor):
|
||||
IPRange = apps.get_model('ipam', 'IPRange')
|
||||
IPRange.objects.update(prefix=None)
|
||||
|
||||
|
||||
def set_prefix_aggregate(apps, schema_editor):
|
||||
set_prefix(apps, schema_editor, 'Prefix', 'prefix', 'Aggregate')
|
||||
|
||||
|
||||
def unset_prefix_aggregate(apps, schema_editor):
|
||||
Prefix = apps.get_model('ipam', 'Prefix')
|
||||
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', '0082_ipaddress_iprange_prefix_parent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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
|
||||
@ -204,6 +205,22 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
areas and/or assigned to VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role.
|
||||
A Prefix can also be assigned to a VLAN where appropriate.
|
||||
"""
|
||||
aggregate = models.ForeignKey(
|
||||
to='ipam.Aggregate',
|
||||
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')
|
||||
@ -291,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:
|
||||
@ -298,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()
|
||||
@ -311,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):
|
||||
|
||||
@ -331,11 +369,11 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
return self.prefix.prefixlen if self.prefix else None
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
def depth_count(self):
|
||||
return self._depth
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
def children_count(self):
|
||||
return self._children
|
||||
|
||||
def _set_prefix_length(self, value):
|
||||
@ -475,11 +513,33 @@ 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):
|
||||
"""
|
||||
A range of IP addresses, defined by start and end addresses.
|
||||
"""
|
||||
prefix = models.ForeignKey(
|
||||
to='ipam.Prefix',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='ip_ranges',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_('prefix'),
|
||||
)
|
||||
start_address = IPAddressField(
|
||||
verbose_name=_('start address'),
|
||||
help_text=_('IPv4 or IPv6 address (with mask)')
|
||||
@ -549,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:
|
||||
@ -705,6 +786,14 @@ 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):
|
||||
"""
|
||||
@ -717,6 +806,14 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
for example, when mapping public addresses to private addresses. When an Interface has been assigned an IPAddress
|
||||
which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP.
|
||||
"""
|
||||
prefix = models.ForeignKey(
|
||||
to='ipam.Prefix',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='ip_addresses',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Prefix')
|
||||
)
|
||||
address = IPAddressField(
|
||||
verbose_name=_('address'),
|
||||
help_text=_('IPv4 or IPv6 address (with mask)')
|
||||
@ -804,6 +901,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')
|
||||
@ -845,6 +943,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:
|
||||
@ -985,3 +1093,8 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
|
||||
def get_role_color(self):
|
||||
return IPAddressRoleChoices.colors.get(self.role)
|
||||
|
||||
@classmethod
|
||||
def find_prefix(self, address):
|
||||
prefixes = Prefix.objects.filter(prefix__net_contains=address.address, vrf=address.vrf)
|
||||
return prefixes.last()
|
||||
|
@ -52,11 +52,12 @@ class IPAddressIndex(SearchIndex):
|
||||
model = models.IPAddress
|
||||
fields = (
|
||||
('address', 100),
|
||||
('prefix', 200),
|
||||
('dns_name', 300),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('vrf', 'tenant', 'status', 'role', 'description')
|
||||
display_attrs = ('prefix', 'vrf', 'tenant', 'status', 'role', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
@ -65,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
|
||||
@ -76,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,9 +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):
|
||||
@ -26,12 +29,140 @@ def update_children_depth(prefix):
|
||||
Prefix.objects.bulk_update(children, ['_depth'], batch_size=100)
|
||||
|
||||
|
||||
def update_object_prefix(prefix, delete=False, parent_model=Prefix, child_model=IPAddress):
|
||||
if delete:
|
||||
# Get all possible addresses
|
||||
addresses = child_model.objects.filter(prefix=prefix)
|
||||
prefix = parent_model.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:
|
||||
filter = Q(prefix=prefix)
|
||||
|
||||
if child_model == IPAddress:
|
||||
filter |= Q(address__net_contained_or_equal=prefix.prefix, vrf=prefix.vrf)
|
||||
elif child_model == IPRange:
|
||||
filter |= Q(
|
||||
start_address__net_contained_or_equal=prefix.prefix,
|
||||
end_address__net_contained_or_equal=prefix.prefix,
|
||||
vrf=prefix.vrf
|
||||
)
|
||||
|
||||
addresses = child_model.objects.filter(filter)
|
||||
for address in addresses:
|
||||
# If addresses prefix is not set then this model is the only option
|
||||
if not address.prefix:
|
||||
address.prefix = prefix
|
||||
# This address has a different VRF so the prefix cannot be the parent prefix
|
||||
elif address.prefix != address.find_prefix(address):
|
||||
address.prefix = address.find_prefix(address)
|
||||
else:
|
||||
pass
|
||||
|
||||
# Update the addresses
|
||||
child_model.objects.bulk_update(addresses, ['prefix'], batch_size=100)
|
||||
|
||||
|
||||
def update_ipaddress_prefix(prefix, delete=False):
|
||||
update_object_prefix(prefix, delete, child_model=IPAddress)
|
||||
|
||||
|
||||
def update_iprange_prefix(prefix, delete=False):
|
||||
update_object_prefix(prefix, delete, child_model=IPRange)
|
||||
|
||||
|
||||
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):
|
||||
|
||||
# Prefix has changed (or new instance has been created)
|
||||
if created or instance.vrf_id != instance._vrf_id or instance.prefix != instance._prefix:
|
||||
|
||||
update_ipaddress_prefix(instance)
|
||||
update_iprange_prefix(instance)
|
||||
update_prefix_parents(instance)
|
||||
update_parents_children(instance)
|
||||
update_children_depth(instance)
|
||||
|
||||
@ -42,11 +173,21 @@ def handle_prefix_saved(instance, created, **kwargs):
|
||||
update_children_depth(old_prefix)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Prefix)
|
||||
def pre_handle_prefix_deleted(instance, **kwargs):
|
||||
update_ipaddress_prefix(instance, True)
|
||||
update_iprange_prefix(instance, True)
|
||||
update_prefix_parents(instance, delete=True)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Prefix)
|
||||
def handle_prefix_deleted(instance, **kwargs):
|
||||
|
||||
update_parents_children(instance)
|
||||
update_children_depth(instance)
|
||||
update_ipaddress_prefix(instance, delete=True)
|
||||
update_iprange_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,10 +311,15 @@ class IPRangeTable(TenancyColumnsMixin, NetBoxTable):
|
||||
#
|
||||
|
||||
class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
||||
# TODO: Alter for prefix
|
||||
address = tables.TemplateColumn(
|
||||
template_code=IPADDRESS_LINK,
|
||||
verbose_name=_('IP Address')
|
||||
)
|
||||
prefix = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Prefix')
|
||||
)
|
||||
vrf = tables.TemplateColumn(
|
||||
template_code=VRF_LINK,
|
||||
verbose_name=_('VRF')
|
||||
|
@ -358,7 +358,8 @@ class RoleTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Prefix
|
||||
brief_fields = ['_depth', 'description', 'display', 'family', 'id', 'prefix', 'url']
|
||||
# TODO: Alter for parent prefix
|
||||
brief_fields = ['_depth', 'aggregate', 'description', 'display', 'family', 'id', 'parent', 'prefix', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'prefix': '192.168.4.0/24',
|
||||
@ -549,7 +550,8 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class IPRangeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = IPRange
|
||||
brief_fields = ['description', 'display', 'end_address', 'family', 'id', 'start_address', 'url']
|
||||
# TODO: Alter for parent prefix
|
||||
brief_fields = ['description', 'display', 'end_address', 'family', 'id', 'prefix', 'start_address', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'start_address': '192.168.4.10/24',
|
||||
@ -648,7 +650,8 @@ class IPRangeTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class IPAddressTest(APIViewTestCases.APIViewTestCase):
|
||||
model = IPAddress
|
||||
brief_fields = ['address', 'description', 'display', 'family', 'id', 'url']
|
||||
# TODO: Alter for parent prefix
|
||||
brief_fields = ['address', 'description', 'display', 'family', 'id', 'prefix', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'address': '192.168.0.4/24',
|
||||
|
@ -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.0.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):
|
||||
|
||||
@ -169,19 +252,21 @@ class TestPrefix(TestCase):
|
||||
prefix=IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER
|
||||
)
|
||||
ips = IPAddress.objects.bulk_create((
|
||||
IPAddress(address=IPNetwork('10.0.0.1/24'), vrf=None),
|
||||
IPAddress(address=IPNetwork('10.0.1.1/24'), vrf=vrfs[0]),
|
||||
IPAddress(address=IPNetwork('10.0.2.1/24'), vrf=vrfs[1]),
|
||||
IPAddress(address=IPNetwork('10.0.3.1/24'), vrf=vrfs[2]),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.0.1/24'), vrf=None),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.1.1/24'), vrf=vrfs[0]),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.2.1/24'), vrf=vrfs[1]),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.3.1/24'), vrf=vrfs[2]),
|
||||
))
|
||||
child_ip_pks = {p.pk for p in parent_prefix.get_child_ips()}
|
||||
child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
|
||||
|
||||
# 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.vrf = vrfs[0]
|
||||
parent_prefix.save()
|
||||
child_ip_pks = {p.pk for p in parent_prefix.get_child_ips()}
|
||||
|
||||
parent_prefix.refresh_from_db()
|
||||
child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
|
||||
|
||||
# VRF container is limited to its own VRF
|
||||
self.assertSetEqual(child_ip_pks, {ips[1].pk})
|
||||
@ -344,17 +429,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 +451,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 +473,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 +497,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 +517,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 +540,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 +565,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 +585,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 +599,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 +613,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 +636,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 +689,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
|
||||
#
|
||||
@ -559,13 +743,20 @@ class TestIPAddress(TestCase):
|
||||
self.assertRaises(ValidationError, duplicate_ip.clean)
|
||||
|
||||
def test_duplicate_vrf(self):
|
||||
vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=False)
|
||||
vrf = VRF.objects.get(rd='1:1')
|
||||
vrf.enforce_unique = False
|
||||
vrf.clean()
|
||||
vrf.save()
|
||||
|
||||
IPAddress.objects.create(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
|
||||
duplicate_ip = IPAddress(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
|
||||
self.assertIsNone(duplicate_ip.clean())
|
||||
|
||||
def test_duplicate_vrf_unique(self):
|
||||
vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=True)
|
||||
vrf = VRF.objects.get(rd='1:1')
|
||||
vrf.enforce_unique = True
|
||||
vrf.clean()
|
||||
vrf.save()
|
||||
IPAddress.objects.create(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
|
||||
duplicate_ip = IPAddress(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
|
||||
self.assertRaises(ValidationError, duplicate_ip.clean)
|
||||
|
@ -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,
|
||||
@ -477,9 +481,9 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
def test_prefix_ipranges(self):
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16'))
|
||||
ip_ranges = (
|
||||
IPRange(start_address='192.168.0.1/24', end_address='192.168.0.100/24', size=99),
|
||||
IPRange(start_address='192.168.1.1/24', end_address='192.168.1.100/24', size=99),
|
||||
IPRange(start_address='192.168.2.1/24', end_address='192.168.2.100/24', size=99),
|
||||
IPRange(prefix=prefix, start_address='192.168.0.1/24', end_address='192.168.0.100/24', size=99),
|
||||
IPRange(prefix=prefix, start_address='192.168.1.1/24', end_address='192.168.1.100/24', size=99),
|
||||
IPRange(prefix=prefix, start_address='192.168.2.1/24', end_address='192.168.2.100/24', size=99),
|
||||
)
|
||||
IPRange.objects.bulk_create(ip_ranges)
|
||||
self.assertEqual(prefix.get_child_ranges().count(), 3)
|
||||
@ -491,12 +495,12 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
def test_prefix_ipaddresses(self):
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16'))
|
||||
ip_addresses = (
|
||||
IPAddress(address=IPNetwork('192.168.0.1/16')),
|
||||
IPAddress(address=IPNetwork('192.168.0.2/16')),
|
||||
IPAddress(address=IPNetwork('192.168.0.3/16')),
|
||||
IPAddress(prefix=prefix, address=IPNetwork('192.168.0.1/16')),
|
||||
IPAddress(prefix=prefix, address=IPNetwork('192.168.0.2/16')),
|
||||
IPAddress(prefix=prefix, address=IPNetwork('192.168.0.3/16')),
|
||||
)
|
||||
IPAddress.objects.bulk_create(ip_addresses)
|
||||
self.assertEqual(prefix.get_child_ips().count(), 3)
|
||||
self.assertEqual(prefix.ip_addresses.all().count(), 3)
|
||||
|
||||
url = reverse('ipam:prefix_ipaddresses', kwargs={'pk': prefix.pk})
|
||||
self.assertHttpStatus(self.client.get(url), 200)
|
||||
@ -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,
|
||||
|
@ -663,13 +663,13 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
|
||||
template_name = 'ipam/prefix/ip_addresses.html'
|
||||
tab = ViewTab(
|
||||
label=_('IP Addresses'),
|
||||
badge=lambda x: x.get_child_ips().count(),
|
||||
badge=lambda x: x.ip_addresses.count(),
|
||||
permission='ipam.view_ipaddress',
|
||||
weight=700
|
||||
)
|
||||
|
||||
def get_children(self, request, parent):
|
||||
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
|
||||
return parent.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
|
||||
|
||||
def prep_table_data(self, request, queryset, parent):
|
||||
if not request.GET.get('q') and not get_table_ordering(request, self.table):
|
||||
|
@ -14,6 +14,10 @@
|
||||
<th scope="row">{% trans "Family" %}</th>
|
||||
<td>IPv{{ object.family }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Prefix" %}</th>
|
||||
<td>{{ object.prefix|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VRF" %}</th>
|
||||
<td>
|
||||
|
@ -14,6 +14,7 @@
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "IP Addresses" %}</h2>
|
||||
</div>
|
||||
{% render_field model_form.prefix %}
|
||||
{% render_field form.pattern %}
|
||||
{% render_field model_form.status %}
|
||||
{% render_field model_form.role %}
|
||||
|
@ -109,7 +109,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% with child_ip_count=object.get_child_ips.count %}
|
||||
{% with child_ip_count=object.ip_addresses.count %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Child IPs" %}</th>
|
||||
<td>
|
||||
|
Loading…
Reference in New Issue
Block a user