mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-13 12:27:44 +01:00
Compare commits
26 Commits
feature-ip
...
20911-drop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be32bc8a74 | ||
|
|
d18ef98b00 | ||
|
|
8aab80b56f | ||
|
|
ee6cbdcefe | ||
|
|
905d17294a | ||
|
|
44e5a4c177 | ||
|
|
de19447317 | ||
|
|
f195af206b | ||
|
|
b0ac55ed6a | ||
|
|
91ab818411 | ||
|
|
62b9367ae3 | ||
|
|
0c091aa80e | ||
|
|
94836e5a37 | ||
|
|
c92912ff03 | ||
|
|
ef0bc18095 | ||
|
|
99f727e685 | ||
|
|
6a5aced4bc | ||
|
|
46f9a12a87 | ||
|
|
be1a008216 | ||
|
|
c4c3518bb4 | ||
|
|
5a1282e326 | ||
|
|
cb13eb277f | ||
|
|
24642be351 | ||
|
|
89af9efd85 | ||
|
|
99d678502f | ||
|
|
e6300ee06d |
@@ -35,11 +35,6 @@ django-mptt==0.17.0
|
||||
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
|
||||
django-pglocks
|
||||
|
||||
|
||||
# Manager for managing PostgreSQL triggers
|
||||
# https://github.com/AmbitionEng/django-pgtrigger/blob/main/CHANGELOG.md
|
||||
django-pgtrigger
|
||||
|
||||
# Prometheus metrics library for Django
|
||||
# https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md
|
||||
django-prometheus
|
||||
|
||||
@@ -516,7 +516,7 @@ class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||
|
||||
|
||||
class ModuleBayViewSet(NetBoxModelViewSet):
|
||||
queryset = ModuleBay.objects.all()
|
||||
queryset = ModuleBay.objects.order_by('device', 'module', 'name')
|
||||
serializer_class = serializers.ModuleBaySerializer
|
||||
filterset_class = filtersets.ModuleBayFilterSet
|
||||
|
||||
|
||||
@@ -745,7 +745,8 @@ class ModuleForm(ModuleCommonForm, PrimaryModelForm):
|
||||
label=_('Module bay'),
|
||||
queryset=ModuleBay.objects.all(),
|
||||
query_params={
|
||||
'device_id': '$device'
|
||||
'device_id': '$device',
|
||||
'ordering': 'module,name',
|
||||
},
|
||||
context={
|
||||
'disabled': 'installed_module',
|
||||
|
||||
32
netbox/dcim/migrations/0226_modulebay_rebuild_tree.py
Normal file
32
netbox/dcim/migrations/0226_modulebay_rebuild_tree.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.db import migrations
|
||||
import mptt.managers
|
||||
import mptt.models
|
||||
|
||||
|
||||
def rebuild_mptt(apps, schema_editor):
|
||||
"""
|
||||
Rebuild the MPTT tree for ModuleBay to apply new ordering.
|
||||
"""
|
||||
ModuleBay = apps.get_model('dcim', 'ModuleBay')
|
||||
|
||||
# Set MPTTMeta with the correct order_insertion_by
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ('module', 'name',)
|
||||
|
||||
ModuleBay.MPTTMeta = MPTTMeta
|
||||
ModuleBay._mptt_meta = mptt.models.MPTTOptions(MPTTMeta)
|
||||
|
||||
manager = mptt.managers.TreeManager()
|
||||
manager.model = ModuleBay
|
||||
manager.contribute_to_class(ModuleBay, 'objects')
|
||||
manager.rebuild()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('dcim', '0225_gfk_indexes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(code=rebuild_mptt, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
@@ -1273,7 +1273,7 @@ class ModuleBay(ModularComponentModel, TrackingModelMixin, MPTTModel):
|
||||
verbose_name_plural = _('module bays')
|
||||
|
||||
class MPTTMeta:
|
||||
order_insertion_by = ('module',)
|
||||
order_insertion_by = ('module', 'name',)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from jsonschema.exceptions import ValidationError as JSONValidationError
|
||||
from mptt.models import MPTTModel
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.utils import create_port_mappings, update_interface_bridges
|
||||
@@ -331,7 +332,8 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
|
||||
component._location = self.device.location
|
||||
component._rack = self.device.rack
|
||||
|
||||
if component_model is not ModuleBay:
|
||||
# we handle create and update separately - this is for create
|
||||
if not issubclass(component_model, MPTTModel):
|
||||
component_model.objects.bulk_create(create_instances)
|
||||
# Emit the post_save signal for each newly created object
|
||||
for component in create_instances:
|
||||
@@ -344,11 +346,13 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
|
||||
update_fields=None
|
||||
)
|
||||
else:
|
||||
# ModuleBays must be saved individually for MPTT
|
||||
# MPTT models must be saved individually to maintain tree structure
|
||||
for instance in create_instances:
|
||||
instance.save()
|
||||
|
||||
update_fields = ['module']
|
||||
|
||||
# we handle create and update separately - this is for update
|
||||
component_model.objects.bulk_update(update_instances, update_fields)
|
||||
# Emit the post_save signal for each updated object
|
||||
for component in update_instances:
|
||||
@@ -361,7 +365,12 @@ class Module(TrackingModelMixin, PrimaryModel, ConfigContextModel):
|
||||
update_fields=update_fields
|
||||
)
|
||||
|
||||
# Rebuild MPTT tree if needed (bulk_update bypasses model save)
|
||||
if issubclass(component_model, MPTTModel) and update_instances:
|
||||
component_model.objects.rebuild()
|
||||
|
||||
# Replicate any front/rear port mappings from the ModuleType
|
||||
create_port_mappings(self.device, self.module_type, self)
|
||||
|
||||
# Interface bridges have to be set after interface instantiation
|
||||
update_interface_bridges(self.device, self.module_type.interfacetemplates, self)
|
||||
|
||||
@@ -373,7 +373,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, TrackingModelMixin, RackBase):
|
||||
super().clean()
|
||||
|
||||
# Validate location/site assignment
|
||||
if self.site and self.location and self.location.site != self.site:
|
||||
if self.site_id and self.location_id and self.location.site_id != self.site_id:
|
||||
raise ValidationError(_("Assigned location must belong to parent site ({site}).").format(site=self.site))
|
||||
|
||||
# Validate outer dimensions and unit
|
||||
|
||||
@@ -60,24 +60,18 @@ class PrefixSerializer(PrimaryModelSerializer):
|
||||
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', 'aggregate', 'parent', 'prefix', 'vrf', 'scope_type',
|
||||
'scope_id', 'scope', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
|
||||
'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_children', '_depth',
|
||||
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'vrf', 'scope_type', 'scope_id', 'scope',
|
||||
'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'children', '_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
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth')
|
||||
|
||||
|
||||
class PrefixLengthSerializer(serializers.Serializer):
|
||||
@@ -131,9 +125,7 @@ class AvailablePrefixSerializer(serializers.Serializer):
|
||||
# IP ranges
|
||||
#
|
||||
|
||||
|
||||
class IPRangeSerializer(PrimaryModelSerializer):
|
||||
prefix = PrefixSerializer(nested=True, required=False, allow_null=True)
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
start_address = IPAddressField()
|
||||
end_address = IPAddressField()
|
||||
@@ -145,11 +137,11 @@ class IPRangeSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = IPRange
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'start_address', 'end_address', 'size', 'vrf',
|
||||
'tenant', 'status', 'role', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'mark_populated', 'mark_utilized',
|
||||
'id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant',
|
||||
'status', 'role', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'mark_populated', 'mark_utilized',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'start_address', 'end_address', 'description')
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description')
|
||||
|
||||
|
||||
#
|
||||
@@ -194,7 +186,6 @@ class AvailableIPRequestSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class IPAddressSerializer(PrimaryModelSerializer):
|
||||
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)
|
||||
@@ -213,11 +204,11 @@ class IPAddressSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'address', 'vrf', 'tenant', 'status', 'role',
|
||||
'id', 'url', 'display_url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role',
|
||||
'assigned_object_type', 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside',
|
||||
'dns_name', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'address', 'description')
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'address', 'description')
|
||||
|
||||
|
||||
class AvailableIPSerializer(serializers.Serializer):
|
||||
|
||||
@@ -340,26 +340,6 @@ class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet,
|
||||
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'),
|
||||
@@ -504,16 +484,6 @@ class IPRangeFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
||||
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'),
|
||||
@@ -599,16 +569,6 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
||||
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'),
|
||||
|
||||
@@ -168,11 +168,6 @@ class RoleBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
|
||||
|
||||
class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
label=_('Parent Prefix')
|
||||
)
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
@@ -226,7 +221,7 @@ class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
|
||||
model = Prefix
|
||||
fieldsets = (
|
||||
FieldSet('tenant', 'status', 'role', 'description'),
|
||||
FieldSet('parent', 'vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')),
|
||||
FieldSet('vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')),
|
||||
FieldSet('scope_type', 'scope', name=_('Scope')),
|
||||
FieldSet('vlan_group', 'vlan', name=_('VLAN Assignment')),
|
||||
)
|
||||
@@ -236,11 +231,6 @@ class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
|
||||
|
||||
|
||||
class IPRangeBulkEditForm(PrimaryModelBulkEditForm):
|
||||
prefix = DynamicModelChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
label=_('Prefix')
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
@@ -282,16 +272,6 @@ class IPRangeBulkEditForm(PrimaryModelBulkEditForm):
|
||||
|
||||
|
||||
class IPAddressBulkEditForm(PrimaryModelBulkEditForm):
|
||||
prefix = DynamicModelChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
label=_('Prefix')
|
||||
)
|
||||
prefix = DynamicModelChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
label=_('Prefix')
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
@@ -327,10 +307,10 @@ class IPAddressBulkEditForm(PrimaryModelBulkEditForm):
|
||||
model = IPAddress
|
||||
fieldsets = (
|
||||
FieldSet('status', 'role', 'tenant', 'description'),
|
||||
FieldSet('prefix', 'vrf', 'mask_length', 'dns_name', name=_('Addressing')),
|
||||
FieldSet('vrf', 'mask_length', 'dns_name', name=_('Addressing')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'prefix', 'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
|
||||
'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -343,8 +343,8 @@ class IPAddressImportForm(PrimaryModelImportForm):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'prefix', 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface',
|
||||
'fhrp_group', 'is_primary', 'is_oob', 'dns_name', 'owner', 'description', 'comments', 'tags',
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'fhrp_group',
|
||||
'is_primary', 'is_oob', 'dns_name', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
|
||||
@@ -219,12 +219,6 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFi
|
||||
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,
|
||||
@@ -298,20 +292,12 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFi
|
||||
class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = IPRange
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet(
|
||||
'prefix', 'family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')
|
||||
),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
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),
|
||||
@@ -356,7 +342,7 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet(
|
||||
'prefix', 'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
|
||||
'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
|
||||
@@ -365,7 +351,7 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'prefix_id', 'parent', 'status', 'role')
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
|
||||
parent = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
@@ -385,11 +371,6 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
|
||||
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,
|
||||
|
||||
@@ -255,8 +255,8 @@ class IPRangeForm(TenancyForm, PrimaryModelForm):
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized',
|
||||
'description', 'tags', name=_('IP Range')
|
||||
'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized', 'description',
|
||||
'tags', name=_('IP Range')
|
||||
),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
@@ -264,8 +264,8 @@ class IPRangeForm(TenancyForm, PrimaryModelForm):
|
||||
class Meta:
|
||||
model = IPRange
|
||||
fields = [
|
||||
'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant',
|
||||
'mark_populated', 'mark_utilized', 'description', 'owner', 'comments', 'tags',
|
||||
'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_populated',
|
||||
'mark_utilized', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -331,8 +331,8 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent',
|
||||
'nat_inside', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
|
||||
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent', 'nat_inside',
|
||||
'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -170,7 +170,6 @@ class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.IPAddress, lookups=True)
|
||||
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
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()
|
||||
@@ -222,7 +221,6 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
|
||||
@strawberry_django.filter_type(models.IPRange, lookups=True)
|
||||
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
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 = (
|
||||
@@ -277,10 +275,6 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Prefix, lookups=True)
|
||||
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
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()
|
||||
|
||||
@@ -143,7 +143,6 @@ class FHRPGroupAssignmentType(BaseObjectType):
|
||||
)
|
||||
class IPAddressType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
|
||||
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,7 +167,6 @@ class IPAddressType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
|
||||
pagination=True
|
||||
)
|
||||
class IPRangeType(ContactsMixin, PrimaryObjectType):
|
||||
prefix: Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
start_address: str
|
||||
end_address: str
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
@@ -183,8 +181,6 @@ class IPRangeType(ContactsMixin, PrimaryObjectType):
|
||||
pagination=True
|
||||
)
|
||||
class PrefixType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
|
||||
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
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# 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', '0086_gfk_indexes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='parent',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
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',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,132 +0,0 @@
|
||||
# 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_attr='prefix', 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')
|
||||
print(f'Migrating {parent_model}')
|
||||
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,
|
||||
)
|
||||
|
||||
setattr(address, parent_attr, prefixes.last())
|
||||
try:
|
||||
address.save()
|
||||
except Exception as e:
|
||||
print(f'Error at {address}')
|
||||
raise e
|
||||
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', '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', '0087_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),
|
||||
]
|
||||
@@ -1,57 +0,0 @@
|
||||
# Generated by Django 5.2.5 on 2026-02-06 21:30
|
||||
|
||||
import pgtrigger.compiler
|
||||
import pgtrigger.migrations
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0088_ipaddress_iprange_prefix_parent_data'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name='prefix',
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name='ipam_prefix_delete',
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func="\n-- Update Child Prefix's with Prefix's PARENT This is a safe assumption based on the fact that the parent would be the\n-- next direct parent for anything else that could contain this prefix\nUPDATE ipam_prefix SET parent_id=OLD.parent_id WHERE parent_id=OLD.id;\nRETURN OLD;\n",
|
||||
hash='ee3f890009c05a3617428158e7b6f3d77317885d',
|
||||
operation='DELETE',
|
||||
pgid='pgtrigger_ipam_prefix_delete_e7810',
|
||||
table='ipam_prefix',
|
||||
when='BEFORE',
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name='prefix',
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name='ipam_prefix_insert',
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func="\n-- Update the prefix with the new parent if the parent is the most appropriate prefix\nUPDATE ipam_prefix\nSET parent_id=NEW.id\nWHERE\n prefix << NEW.prefix\n AND\n (\n (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n OR\n (\n NEW.vrf_id IS NULL\n AND\n NEW.status = 'container'\n AND\n NOT EXISTS(\n SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id\n )\n )\n )\n AND id != NEW.id\n AND NOT EXISTS (\n SELECT 1 FROM ipam_prefix p\n WHERE\n p.prefix >> ipam_prefix.prefix\n AND p.prefix << NEW.prefix\n AND (\n (p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))\n OR\n (p.vrf_id IS NULL AND p.status = 'container')\n )\n AND p.id != NEW.id\n )\n;\nRETURN NEW;\n",
|
||||
hash='1d71498f09e767183d3b0d29c06c9ac9e2cc000a',
|
||||
operation='INSERT',
|
||||
pgid='pgtrigger_ipam_prefix_insert_46c72',
|
||||
table='ipam_prefix',
|
||||
when='AFTER',
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name='prefix',
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name='ipam_prefix_update',
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func="\n-- When a prefix changes, reassign any child prefixes that no longer\n-- fall within the new prefix range to the parent prefix (or set null if no parent exists)\nUPDATE ipam_prefix\nSET parent_id = OLD.parent_id\nWHERE\n parent_id = NEW.id\n -- IP address no longer contained within the updated prefix\n AND NOT (prefix << NEW.prefix);\n\n-- When a prefix changes, reassign any ip addresses that no longer\n-- fall within the new prefix range to the parent prefix (or set null if no parent exists)\nUPDATE ipam_ipaddress\nSET prefix_id = OLD.parent_id\nWHERE\n prefix_id = NEW.id\n -- IP address no longer contained within the updated prefix\n AND\n NOT (address << NEW.prefix)\n;\n\n-- When a prefix changes, reassign any ip ranges that no longer\n-- fall within the new prefix range to the parent prefix (or set null if no parent exists)\nUPDATE ipam_iprange\nSET prefix_id = OLD.parent_id\nWHERE\n prefix_id = NEW.id\n -- IP address no longer contained within the updated prefix\n AND\n NOT (start_address << NEW.prefix)\n AND\n NOT (end_address << NEW.prefix)\n;\n\n-- When a prefix changes, reassign any ip addresses that are in-scope but\n-- no longer within the same VRF\nUPDATE ipam_ipaddress\n SET prefix_id = OLD.parent_id\n WHERE\n prefix_id = NEW.id\n AND\n address << OLD.prefix\n AND\n (\n NOT address << NEW.prefix\n OR\n (\n vrf_id is NULL\n AND\n NEW.vrf_id IS NOT NULL\n )\n OR\n (\n OLD.vrf_id IS NULL\n AND\n NEW.vrf_id IS NOT NULL\n AND\n NEW.vrf_id != vrf_id\n )\n )\n;\n\n-- When a prefix changes, reassign any ip ranges that are in-scope but\n-- no longer within the same VRF\nUPDATE ipam_iprange\n SET prefix_id = OLD.parent_id\n WHERE\n prefix_id = NEW.id\n AND\n start_address << OLD.prefix\n AND\n end_address << OLD.prefix\n AND\n (\n NOT start_address << NEW.prefix\n OR\n NOT end_address << NEW.prefix\n OR\n (\n vrf_id is NULL\n AND\n NEW.vrf_id IS NOT NULL\n )\n OR\n (\n OLD.vrf_id IS NULL\n AND\n NEW.vrf_id IS NOT NULL\n AND\n NEW.vrf_id != vrf_id\n )\n )\n;\n\n-- Update the prefix with the new parent if the parent is the most appropriate prefix\nUPDATE ipam_prefix\n SET parent_id=NEW.id\n WHERE\n prefix << NEW.prefix\n AND\n (\n (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n OR\n (\n NEW.vrf_id IS NULL\n AND\n NEW.status = 'container'\n AND\n NOT EXISTS(\n SELECT 1 FROM ipam_prefix p WHERE p.prefix >> prefix AND p.vrf_id = vrf_id\n )\n )\n )\n AND id != NEW.id\n AND NOT EXISTS (\n SELECT 1 FROM ipam_prefix p\n WHERE\n p.prefix >> ipam_prefix.prefix\n AND p.prefix << NEW.prefix\n AND (\n (p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))\n OR\n (p.vrf_id IS NULL AND p.status = 'container')\n )\n AND p.id != NEW.id\n )\n;\nUPDATE ipam_ipaddress\n SET prefix_id = NEW.id\n WHERE\n prefix_id != NEW.id\n AND\n address << NEW.prefix\n AND (\n (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n OR (\n NEW.vrf_id IS NULL\n AND\n NEW.status = 'container'\n AND\n NOT EXISTS(\n SELECT 1 FROM ipam_prefix p WHERE p.prefix >> address AND p.vrf_id = vrf_id\n )\n )\n )\n;\nUPDATE ipam_iprange\n SET prefix_id = NEW.id\n WHERE\n prefix_id != NEW.id\n AND\n start_address << NEW.prefix\n AND\n end_address << NEW.prefix\n AND (\n (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n OR (\n NEW.vrf_id IS NULL\n AND\n NEW.status = 'container'\n AND\n NOT EXISTS(\n SELECT 1 FROM ipam_prefix p WHERE\n p.prefix >> start_address\n AND\n p.prefix >> end_address\n AND\n p.vrf_id = vrf_id\n )\n )\n )\n;\nRETURN NEW;\n",
|
||||
hash='7dce524151c88aa9864aad70a24cb5982b05aa28',
|
||||
operation='UPDATE',
|
||||
pgid='pgtrigger_ipam_prefix_update_e5fca',
|
||||
table='ipam_prefix',
|
||||
when='AFTER',
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,4 @@
|
||||
import netaddr
|
||||
import pgtrigger
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.indexes import GistIndex
|
||||
@@ -9,7 +8,6 @@ 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 dcim.models.mixins import CachedScopeMixin
|
||||
from ipam.choices import *
|
||||
@@ -18,8 +16,6 @@ from ipam.fields import IPNetworkField, IPAddressField
|
||||
from ipam.lookups import Host
|
||||
from ipam.managers import IPAddressManager
|
||||
from ipam.querysets import PrefixQuerySet
|
||||
from ipam.triggers import ipam_prefix_delete_adjust_prefix_parent, ipam_prefix_insert_adjust_prefix_parent, \
|
||||
ipam_prefix_update_adjust_prefix_parent
|
||||
from ipam.validators import DNSValidator
|
||||
from netbox.config import get_config
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
@@ -189,28 +185,31 @@ class Aggregate(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
||||
return min(utilization, 100)
|
||||
|
||||
|
||||
class Role(OrganizationalModel):
|
||||
"""
|
||||
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
|
||||
"Management."
|
||||
"""
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('weight'),
|
||||
default=1000
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'name')
|
||||
verbose_name = _('role')
|
||||
verbose_name_plural = _('roles')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, PrimaryModel):
|
||||
"""
|
||||
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be scoped to certain
|
||||
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, # This is handled by triggers
|
||||
related_name='prefixes',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('aggregate')
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
to='ipam.Prefix',
|
||||
on_delete=models.DO_NOTHING,
|
||||
related_name='children',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Prefix')
|
||||
)
|
||||
prefix = IPNetworkField(
|
||||
verbose_name=_('prefix'),
|
||||
help_text=_('IPv4 or IPv6 network with mask')
|
||||
@@ -285,32 +284,8 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
verbose_name_plural = _('prefixes')
|
||||
indexes = (
|
||||
models.Index(fields=('scope_type', 'scope_id')),
|
||||
GistIndex(
|
||||
fields=['prefix'],
|
||||
name='ipam_prefix_gist_idx',
|
||||
opclasses=['inet_ops'],
|
||||
),
|
||||
)
|
||||
triggers = (
|
||||
pgtrigger.Trigger(
|
||||
name='ipam_prefix_delete',
|
||||
operation=pgtrigger.Delete,
|
||||
when=pgtrigger.Before,
|
||||
func=ipam_prefix_delete_adjust_prefix_parent,
|
||||
),
|
||||
pgtrigger.Trigger(
|
||||
name='ipam_prefix_insert',
|
||||
operation=pgtrigger.Insert,
|
||||
when=pgtrigger.After,
|
||||
func=ipam_prefix_insert_adjust_prefix_parent,
|
||||
),
|
||||
pgtrigger.Trigger(
|
||||
name='ipam_prefix_update',
|
||||
operation=pgtrigger.Update,
|
||||
when=pgtrigger.After,
|
||||
func=ipam_prefix_update_adjust_prefix_parent,
|
||||
),
|
||||
)
|
||||
GistIndex(fields=['prefix'], name='ipam_prefix_gist_idx', opclasses=['inet_ops']),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -326,8 +301,6 @@ 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:
|
||||
@@ -349,10 +322,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
if not self.pk or not self.parent or (self.prefix != self._prefix) or (self.vrf_id != self._vrf_id):
|
||||
parent = self.find_parent_prefix(networks=self.prefix, vrf=self.vrf, exclude=self.pk)
|
||||
self.parent = parent
|
||||
|
||||
if isinstance(self.prefix, netaddr.IPNetwork):
|
||||
|
||||
# Clear host bits from prefix
|
||||
@@ -377,11 +346,11 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
return netaddr.IPAddress(self.prefix).format(netaddr.ipv6_full)
|
||||
|
||||
@property
|
||||
def depth_count(self):
|
||||
def depth(self):
|
||||
return self._depth
|
||||
|
||||
@property
|
||||
def children_count(self):
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
def _set_prefix_length(self, value):
|
||||
@@ -521,63 +490,11 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
|
||||
return min(utilization, 100)
|
||||
|
||||
@classmethod
|
||||
def find_parent_prefix(cls, networks, vrf=None, exclude=None):
|
||||
# TODO: Document
|
||||
if type(networks) in [netaddr.IPAddress, netaddr.IPNetwork, str]:
|
||||
networks = [networks, ]
|
||||
|
||||
network_filter = models.Q()
|
||||
for network in networks:
|
||||
network_filter &= models.Q(
|
||||
prefix__net_contains_or_equals=network
|
||||
)
|
||||
prefixes = Prefix.objects.filter(
|
||||
models.Q(
|
||||
network_filter,
|
||||
vrf=vrf
|
||||
) | models.Q(
|
||||
network_filter,
|
||||
vrf=None,
|
||||
status=PrefixStatusChoices.STATUS_CONTAINER,
|
||||
)
|
||||
)
|
||||
if exclude:
|
||||
prefixes = prefixes.exclude(pk=exclude)
|
||||
return prefixes.last()
|
||||
|
||||
|
||||
class Role(OrganizationalModel):
|
||||
"""
|
||||
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
|
||||
"Management."
|
||||
"""
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('weight'),
|
||||
default=1000
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'name')
|
||||
verbose_name = _('role')
|
||||
verbose_name_plural = _('roles')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
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)')
|
||||
@@ -647,27 +564,6 @@ 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:
|
||||
@@ -730,12 +626,6 @@ class IPRange(ContactsMixin, PrimaryModel):
|
||||
# Record the range's size (number of IP addresses)
|
||||
self.size = int(self.end_address.ip - self.start_address.ip) + 1
|
||||
|
||||
# Set the parent prefix
|
||||
self.prefix = Prefix.find_parent_prefix(
|
||||
networks=[self.start_address, self.end_address],
|
||||
vrf=self.vrf
|
||||
)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
@@ -842,14 +732,6 @@ 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)')
|
||||
@@ -937,7 +819,6 @@ 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')
|
||||
@@ -984,16 +865,6 @@ 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:
|
||||
@@ -1087,9 +958,6 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
# Force dns_name to lowercase
|
||||
self.dns_name = self.dns_name.lower()
|
||||
|
||||
# Set the parent prefix
|
||||
self.prefix = Prefix.find_parent_prefix(networks=self.address, vrf=self.vrf)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clone(self):
|
||||
@@ -1144,8 +1012,3 @@ 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()
|
||||
|
||||
@@ -53,12 +53,11 @@ class IPAddressIndex(SearchIndex):
|
||||
model = models.IPAddress
|
||||
fields = (
|
||||
('address', 100),
|
||||
('prefix', 200),
|
||||
('dns_name', 300),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('prefix', 'vrf', 'tenant', 'status', 'role', 'description')
|
||||
display_attrs = ('vrf', 'tenant', 'status', 'role', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
@@ -67,11 +66,10 @@ class IPRangeIndex(SearchIndex):
|
||||
fields = (
|
||||
('start_address', 100),
|
||||
('end_address', 300),
|
||||
('prefix', 400),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('prefix', 'vrf', 'tenant', 'status', 'role', 'description')
|
||||
display_attrs = ('vrf', 'tenant', 'status', 'role', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
@@ -79,12 +77,10 @@ class PrefixIndex(SearchIndex):
|
||||
model = models.Prefix
|
||||
fields = (
|
||||
('prefix', 110),
|
||||
('parent', 200),
|
||||
('aggregate', 300),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('scope', 'aggregate', 'parent', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description')
|
||||
display_attrs = ('scope', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
|
||||
@@ -152,10 +152,6 @@ class PrefixUtilizationColumn(columns.UtilizationColumn):
|
||||
|
||||
|
||||
class PrefixTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
parent = tables.Column(
|
||||
verbose_name=_('Parent'),
|
||||
linkify=True
|
||||
)
|
||||
prefix = columns.TemplateColumn(
|
||||
verbose_name=_('Prefix'),
|
||||
template_code=PREFIX_LINK_WITH_DEPTH,
|
||||
@@ -234,9 +230,9 @@ class PrefixTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
class Meta(PrimaryModelTable.Meta):
|
||||
model = Prefix
|
||||
fields = (
|
||||
'pk', 'id', 'prefix', 'status', 'parent', 'prefix', 'prefix_flat', 'children', 'vrf', 'utilization',
|
||||
'tenant', 'tenant_group', 'scope', 'scope_type', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized',
|
||||
'contacts', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group',
|
||||
'scope', 'scope_type', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'contacts',
|
||||
'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'scope', 'vlan', 'role',
|
||||
@@ -250,11 +246,8 @@ class PrefixTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
#
|
||||
# IP ranges
|
||||
#
|
||||
|
||||
class IPRangeTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
prefix = tables.Column(
|
||||
verbose_name=_('Prefix'),
|
||||
linkify=True
|
||||
)
|
||||
start_address = tables.Column(
|
||||
verbose_name=_('Start address'),
|
||||
linkify=True
|
||||
@@ -291,9 +284,9 @@ class IPRangeTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
class Meta(PrimaryModelTable.Meta):
|
||||
model = IPRange
|
||||
fields = (
|
||||
'pk', 'id', 'start_address', 'end_address', 'prefix', 'size', 'vrf', 'status', 'role', 'tenant',
|
||||
'tenant_group', 'mark_populated', 'mark_utilized', 'utilization', 'description', 'contacts',
|
||||
'comments', 'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group',
|
||||
'mark_populated', 'mark_utilized', 'utilization', 'description', 'contacts', 'comments', 'tags',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
||||
@@ -308,18 +301,10 @@ class IPRangeTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
#
|
||||
|
||||
class IPAddressTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
prefix = tables.Column(
|
||||
verbose_name=_('Prefix'),
|
||||
linkify=True
|
||||
)
|
||||
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')
|
||||
@@ -368,9 +353,8 @@ class IPAddressTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable
|
||||
class Meta(PrimaryModelTable.Meta):
|
||||
model = IPAddress
|
||||
fields = (
|
||||
'pk', 'id', 'address', 'vrf', 'prefix', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside',
|
||||
'nat_outside', 'assigned', 'dns_name', 'description', 'comments', 'contacts', 'tags', 'created',
|
||||
'last_updated',
|
||||
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside',
|
||||
'assigned', 'dns_name', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
|
||||
|
||||
@@ -16,20 +16,12 @@ PREFIX_COPY_BUTTON = """
|
||||
|
||||
PREFIX_LINK_WITH_DEPTH = """
|
||||
{% load helpers %}
|
||||
{% if record.depth_count %}
|
||||
{% if object %}
|
||||
<div class="record-depth">
|
||||
{% for i in record.depth_count|parent_depth:object|as_range %}
|
||||
<span>•</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="record-depth">
|
||||
{% for i in record.depth_count|as_range %}
|
||||
<span>•</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if record.depth %}
|
||||
<div class="record-depth">
|
||||
{% for i in record.depth|as_range %}
|
||||
<span>•</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
""" + PREFIX_LINK
|
||||
|
||||
|
||||
@@ -407,8 +407,7 @@ class RoleTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Prefix
|
||||
# TODO: Alter for parent prefix
|
||||
brief_fields = ['_depth', 'aggregate', 'description', 'display', 'family', 'id', 'parent', 'prefix', 'url']
|
||||
brief_fields = ['_depth', 'description', 'display', 'family', 'id', 'prefix', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'prefix': '192.168.4.0/24',
|
||||
@@ -648,8 +647,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class IPRangeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = IPRange
|
||||
# TODO: Alter for parent prefix
|
||||
brief_fields = ['description', 'display', 'end_address', 'family', 'id', 'prefix', 'start_address', 'url']
|
||||
brief_fields = ['description', 'display', 'end_address', 'family', 'id', 'start_address', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'start_address': '192.168.4.10/24',
|
||||
@@ -807,8 +805,7 @@ class IPRangeTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class IPAddressTest(APIViewTestCases.APIViewTestCase):
|
||||
model = IPAddress
|
||||
# TODO: Alter for parent prefix
|
||||
brief_fields = ['address', 'description', 'display', 'family', 'id', 'prefix', 'url']
|
||||
brief_fields = ['address', 'description', 'display', 'family', 'id', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'address': '192.168.0.4/24',
|
||||
|
||||
@@ -901,10 +901,6 @@ 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()
|
||||
@@ -1083,7 +1079,6 @@ 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
|
||||
@@ -1323,7 +1318,6 @@ 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,26 +39,6 @@ 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(
|
||||
@@ -107,69 +87,6 @@ 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):
|
||||
|
||||
@@ -252,16 +169,23 @@ class TestPrefix(TestCase):
|
||||
prefix=IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER
|
||||
)
|
||||
ips = IPAddress.objects.bulk_create((
|
||||
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]),
|
||||
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]),
|
||||
))
|
||||
child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
|
||||
child_ip_pks = {p.pk for p in parent_prefix.get_child_ips()}
|
||||
|
||||
# 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()}
|
||||
|
||||
# VRF container is limited to its own VRF
|
||||
self.assertSetEqual(child_ip_pks, {ips[1].pk})
|
||||
|
||||
def test_get_available_prefixes(self):
|
||||
|
||||
prefixes = Prefix.objects.bulk_create((
|
||||
@@ -408,62 +332,6 @@ class TestPrefix(TestCase):
|
||||
duplicate_prefix = Prefix(vrf=vrf, prefix=IPNetwork('192.0.2.0/24'))
|
||||
self.assertRaises(ValidationError, duplicate_prefix.clean)
|
||||
|
||||
def test_parent_container_prefix_change(self):
|
||||
vrfs = VRF.objects.bulk_create((
|
||||
VRF(name='VRF 1'),
|
||||
VRF(name='VRF 2'),
|
||||
VRF(name='VRF 3'),
|
||||
))
|
||||
parent_prefix = Prefix.objects.create(
|
||||
prefix=IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER
|
||||
)
|
||||
ips = IPAddress.objects.bulk_create((
|
||||
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.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()
|
||||
|
||||
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})
|
||||
|
||||
def test_parent_container_vrf_change(self):
|
||||
vrfs = VRF.objects.bulk_create((
|
||||
VRF(name='VRF 1'),
|
||||
VRF(name='VRF 2'),
|
||||
VRF(name='VRF 3'),
|
||||
))
|
||||
parent_prefix = Prefix.objects.create(
|
||||
prefix=IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER
|
||||
)
|
||||
ips = IPAddress.objects.bulk_create((
|
||||
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.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.prefix = '10.0.0.0/23'
|
||||
parent_prefix.save()
|
||||
|
||||
parent_prefix.refresh_from_db()
|
||||
child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
|
||||
self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk})
|
||||
|
||||
|
||||
class TestPrefixHierarchy(TestCase):
|
||||
"""
|
||||
@@ -476,21 +344,17 @@ class TestPrefixHierarchy(TestCase):
|
||||
prefixes = (
|
||||
|
||||
# IPv4
|
||||
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'),
|
||||
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),
|
||||
|
||||
# IPv6
|
||||
Prefix(prefix='2001:db8::/32'),
|
||||
Prefix(prefix='2001:db8::/40'),
|
||||
Prefix(prefix='2001:db8::/48'),
|
||||
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),
|
||||
|
||||
)
|
||||
|
||||
for prefix in prefixes:
|
||||
prefix.clean()
|
||||
prefix.save()
|
||||
Prefix.objects.bulk_create(prefixes)
|
||||
|
||||
def test_create_prefix4(self):
|
||||
# Create 10.0.0.0/12
|
||||
@@ -498,19 +362,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, 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)
|
||||
|
||||
@@ -520,19 +380,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, 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)
|
||||
|
||||
@@ -544,15 +400,12 @@ 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)
|
||||
|
||||
@@ -564,15 +417,12 @@ 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)
|
||||
|
||||
@@ -587,17 +437,14 @@ 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)
|
||||
|
||||
@@ -612,17 +459,14 @@ 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)
|
||||
|
||||
@@ -632,11 +476,9 @@ 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)
|
||||
|
||||
@@ -646,11 +488,9 @@ 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)
|
||||
|
||||
@@ -660,20 +500,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, 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)
|
||||
|
||||
@@ -683,158 +518,20 @@ 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 TestTriggers(TestCase):
|
||||
"""
|
||||
Test the automatic updating of depth and child count in response to changes made within
|
||||
the prefix hierarchy.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
vrfs = (
|
||||
VRF(name='VRF A'),
|
||||
VRF(name='VRF B'),
|
||||
)
|
||||
|
||||
for vrf in vrfs:
|
||||
vrf.clean()
|
||||
vrf.save()
|
||||
|
||||
cls.prefixes = (
|
||||
# IPv4
|
||||
Prefix(prefix='10.0.0.0/8'),
|
||||
Prefix(prefix='10.0.0.0/16'),
|
||||
Prefix(prefix='10.0.0.0/22'),
|
||||
Prefix(prefix='10.0.0.0/23'),
|
||||
Prefix(prefix='10.0.2.0/23'),
|
||||
Prefix(prefix='10.0.0.0/24'),
|
||||
Prefix(prefix='10.0.1.0/24'),
|
||||
Prefix(prefix='10.0.2.0/24'),
|
||||
Prefix(prefix='10.0.3.0/24'),
|
||||
Prefix(prefix='10.1.0.0/16', status='container'),
|
||||
Prefix(prefix='10.1.0.0/22', vrf=vrfs[0]),
|
||||
Prefix(prefix='10.1.0.0/23', vrf=vrfs[0]),
|
||||
Prefix(prefix='10.1.2.0/23', vrf=vrfs[0]),
|
||||
Prefix(prefix='10.1.0.0/24', vrf=vrfs[0]),
|
||||
Prefix(prefix='10.1.1.0/24', vrf=vrfs[0]),
|
||||
Prefix(prefix='10.1.2.0/24', vrf=vrfs[0]),
|
||||
Prefix(prefix='10.1.3.0/24', vrf=vrfs[0]),
|
||||
)
|
||||
|
||||
for prefix in cls.prefixes:
|
||||
prefix.clean()
|
||||
prefix.save()
|
||||
|
||||
def test_current_hierarchy(self):
|
||||
self.assertIsNone(Prefix.objects.get(prefix='10.0.0.0/8').parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/16').parent, Prefix.objects.get(prefix='10.0.0.0/8'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/22').parent, Prefix.objects.get(prefix='10.0.0.0/16'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/23').parent, Prefix.objects.get(prefix='10.0.0.0/22'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.2.0/23').parent, Prefix.objects.get(prefix='10.0.0.0/22'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/24').parent, Prefix.objects.get(prefix='10.0.0.0/23'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.1.0/24').parent, Prefix.objects.get(prefix='10.0.0.0/23'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.2.0/24').parent, Prefix.objects.get(prefix='10.0.2.0/23'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.3.0/24').parent, Prefix.objects.get(prefix='10.0.2.0/23'))
|
||||
|
||||
def test_basic_insert(self):
|
||||
pfx = Prefix.objects.create(prefix='10.0.0.0/21')
|
||||
self.assertIsNotNone(Prefix.objects.get(prefix='10.0.0.0/22').parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/22').parent, pfx)
|
||||
|
||||
def test_vrf_insert(self):
|
||||
vrf = VRF.objects.get(name='VRF A')
|
||||
pfx = Prefix.objects.create(prefix='10.1.0.0/21', vrf=vrf)
|
||||
parent = Prefix.objects.get(prefix='10.1.0.0/16')
|
||||
|
||||
self.assertIsNotNone(Prefix.objects.get(prefix='10.1.0.0/21').parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.0.0/21').parent, parent)
|
||||
self.assertIsNotNone(Prefix.objects.get(prefix='10.1.0.0/22').parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.0.0/22').parent, pfx)
|
||||
|
||||
def test_basic_delete(self):
|
||||
Prefix.objects.get(prefix='10.0.0.0/23').delete()
|
||||
parent = Prefix.objects.get(prefix='10.0.0.0/22')
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.1.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.2.0/24').parent, Prefix.objects.get(prefix='10.0.2.0/23'))
|
||||
|
||||
def test_vrf_delete(self):
|
||||
Prefix.objects.get(prefix='10.1.0.0/23').delete()
|
||||
parent = Prefix.objects.get(prefix='10.1.0.0/22')
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.0.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.1.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.2.0/24').parent, Prefix.objects.get(prefix='10.1.2.0/23'))
|
||||
|
||||
def test_basic_update(self):
|
||||
pfx = Prefix.objects.get(prefix='10.0.0.0/23')
|
||||
parent = Prefix.objects.get(prefix='10.0.0.0/22')
|
||||
pfx.prefix = '10.3.0.0/23'
|
||||
pfx.parent = Prefix.objects.get(prefix='10.0.0.0/8')
|
||||
pfx.clean()
|
||||
pfx.save()
|
||||
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.1.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.2.0/24').parent, Prefix.objects.get(prefix='10.0.2.0/23'))
|
||||
|
||||
def test_vrf_update(self):
|
||||
pfx = Prefix.objects.get(prefix='10.1.0.0/23')
|
||||
parent = Prefix.objects.get(prefix='10.1.0.0/22')
|
||||
pfx.prefix = '10.3.0.0/23'
|
||||
pfx.parent = None
|
||||
pfx.clean()
|
||||
pfx.save()
|
||||
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.0.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.1.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.2.0/24').parent, Prefix.objects.get(prefix='10.1.2.0/23'))
|
||||
|
||||
# TODO: Test VRF Changes
|
||||
|
||||
|
||||
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((
|
||||
@@ -846,44 +543,6 @@ 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')),
|
||||
)
|
||||
|
||||
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
|
||||
#
|
||||
@@ -900,20 +559,13 @@ class TestIPAddress(TestCase):
|
||||
self.assertRaises(ValidationError, duplicate_ip.clean)
|
||||
|
||||
def test_duplicate_vrf(self):
|
||||
vrf = VRF.objects.get(rd='1:1')
|
||||
vrf.enforce_unique = False
|
||||
vrf.clean()
|
||||
vrf.save()
|
||||
|
||||
vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=False)
|
||||
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.get(rd='1:1')
|
||||
vrf.enforce_unique = True
|
||||
vrf.clean()
|
||||
vrf.save()
|
||||
vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=True)
|
||||
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,7 +421,6 @@ 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,
|
||||
@@ -437,7 +436,6 @@ 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}",
|
||||
@@ -445,7 +443,6 @@ 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}",
|
||||
@@ -453,7 +450,6 @@ 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,
|
||||
@@ -481,9 +477,9 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
def test_prefix_ipranges(self):
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16'))
|
||||
ip_ranges = (
|
||||
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(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.objects.bulk_create(ip_ranges)
|
||||
self.assertEqual(prefix.get_child_ranges().count(), 3)
|
||||
@@ -495,12 +491,12 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
def test_prefix_ipaddresses(self):
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16'))
|
||||
ip_addresses = (
|
||||
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(address=IPNetwork('192.168.0.1/16')),
|
||||
IPAddress(address=IPNetwork('192.168.0.2/16')),
|
||||
IPAddress(address=IPNetwork('192.168.0.3/16')),
|
||||
)
|
||||
IPAddress.objects.bulk_create(ip_addresses)
|
||||
self.assertEqual(prefix.ip_addresses.all().count(), 3)
|
||||
self.assertEqual(prefix.get_child_ips().count(), 3)
|
||||
|
||||
url = reverse('ipam:prefix_ipaddresses', kwargs={'pk': prefix.pk})
|
||||
self.assertHttpStatus(self.client.get(url), 200)
|
||||
@@ -674,7 +670,6 @@ 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'),
|
||||
@@ -688,7 +683,6 @@ 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",
|
||||
@@ -696,7 +690,6 @@ 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}",
|
||||
@@ -704,7 +697,6 @@ 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,
|
||||
@@ -771,7 +763,6 @@ 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'),
|
||||
@@ -784,7 +775,6 @@ 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",
|
||||
@@ -792,7 +782,6 @@ 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}",
|
||||
@@ -800,7 +789,6 @@ 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,
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
ipam_prefix_delete_adjust_prefix_parent = """
|
||||
-- Update Child Prefix's with Prefix's PARENT This is a safe assumption based on the fact that the parent would be the
|
||||
-- next direct parent for anything else that could contain this prefix
|
||||
UPDATE ipam_prefix SET parent_id=OLD.parent_id WHERE parent_id=OLD.id;
|
||||
RETURN OLD;
|
||||
"""
|
||||
|
||||
|
||||
ipam_prefix_insert_adjust_prefix_parent = """
|
||||
-- Update the prefix with the new parent if the parent is the most appropriate prefix
|
||||
UPDATE ipam_prefix
|
||||
SET parent_id=NEW.id
|
||||
WHERE
|
||||
prefix << NEW.prefix
|
||||
AND
|
||||
(
|
||||
(vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
|
||||
OR
|
||||
(
|
||||
NEW.vrf_id IS NULL
|
||||
AND
|
||||
NEW.status = 'container'
|
||||
AND
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id
|
||||
)
|
||||
)
|
||||
)
|
||||
AND id != NEW.id
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ipam_prefix p
|
||||
WHERE
|
||||
p.prefix >> ipam_prefix.prefix
|
||||
AND p.prefix << NEW.prefix
|
||||
AND (
|
||||
(p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))
|
||||
OR
|
||||
(p.vrf_id IS NULL AND p.status = 'container')
|
||||
)
|
||||
AND p.id != NEW.id
|
||||
)
|
||||
;
|
||||
RETURN NEW;
|
||||
"""
|
||||
|
||||
|
||||
ipam_prefix_update_adjust_prefix_parent = """
|
||||
-- When a prefix changes, reassign any child prefixes that no longer
|
||||
-- fall within the new prefix range to the parent prefix (or set null if no parent exists)
|
||||
UPDATE ipam_prefix
|
||||
SET parent_id = OLD.parent_id
|
||||
WHERE
|
||||
parent_id = NEW.id
|
||||
-- IP address no longer contained within the updated prefix
|
||||
AND NOT (prefix << NEW.prefix);
|
||||
|
||||
-- When a prefix changes, reassign any ip addresses that no longer
|
||||
-- fall within the new prefix range to the parent prefix (or set null if no parent exists)
|
||||
UPDATE ipam_ipaddress
|
||||
SET prefix_id = OLD.parent_id
|
||||
WHERE
|
||||
prefix_id = NEW.id
|
||||
-- IP address no longer contained within the updated prefix
|
||||
AND
|
||||
NOT (address << NEW.prefix)
|
||||
;
|
||||
|
||||
-- When a prefix changes, reassign any ip ranges that no longer
|
||||
-- fall within the new prefix range to the parent prefix (or set null if no parent exists)
|
||||
UPDATE ipam_iprange
|
||||
SET prefix_id = OLD.parent_id
|
||||
WHERE
|
||||
prefix_id = NEW.id
|
||||
-- IP address no longer contained within the updated prefix
|
||||
AND
|
||||
NOT (start_address << NEW.prefix)
|
||||
AND
|
||||
NOT (end_address << NEW.prefix)
|
||||
;
|
||||
|
||||
-- When a prefix changes, reassign any ip addresses that are in-scope but
|
||||
-- no longer within the same VRF
|
||||
UPDATE ipam_ipaddress
|
||||
SET prefix_id = OLD.parent_id
|
||||
WHERE
|
||||
prefix_id = NEW.id
|
||||
AND
|
||||
address << OLD.prefix
|
||||
AND
|
||||
(
|
||||
NOT address << NEW.prefix
|
||||
OR
|
||||
(
|
||||
vrf_id is NULL
|
||||
AND
|
||||
NEW.vrf_id IS NOT NULL
|
||||
)
|
||||
OR
|
||||
(
|
||||
OLD.vrf_id IS NULL
|
||||
AND
|
||||
NEW.vrf_id IS NOT NULL
|
||||
AND
|
||||
NEW.vrf_id != vrf_id
|
||||
)
|
||||
)
|
||||
;
|
||||
|
||||
-- When a prefix changes, reassign any ip ranges that are in-scope but
|
||||
-- no longer within the same VRF
|
||||
UPDATE ipam_iprange
|
||||
SET prefix_id = OLD.parent_id
|
||||
WHERE
|
||||
prefix_id = NEW.id
|
||||
AND
|
||||
start_address << OLD.prefix
|
||||
AND
|
||||
end_address << OLD.prefix
|
||||
AND
|
||||
(
|
||||
NOT start_address << NEW.prefix
|
||||
OR
|
||||
NOT end_address << NEW.prefix
|
||||
OR
|
||||
(
|
||||
vrf_id is NULL
|
||||
AND
|
||||
NEW.vrf_id IS NOT NULL
|
||||
)
|
||||
OR
|
||||
(
|
||||
OLD.vrf_id IS NULL
|
||||
AND
|
||||
NEW.vrf_id IS NOT NULL
|
||||
AND
|
||||
NEW.vrf_id != vrf_id
|
||||
)
|
||||
)
|
||||
;
|
||||
|
||||
-- Update the prefix with the new parent if the parent is the most appropriate prefix
|
||||
UPDATE ipam_prefix
|
||||
SET parent_id=NEW.id
|
||||
WHERE
|
||||
prefix << NEW.prefix
|
||||
AND
|
||||
(
|
||||
(vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
|
||||
OR
|
||||
(
|
||||
NEW.vrf_id IS NULL
|
||||
AND
|
||||
NEW.status = 'container'
|
||||
AND
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM ipam_prefix p WHERE p.prefix >> prefix AND p.vrf_id = vrf_id
|
||||
)
|
||||
)
|
||||
)
|
||||
AND id != NEW.id
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ipam_prefix p
|
||||
WHERE
|
||||
p.prefix >> ipam_prefix.prefix
|
||||
AND p.prefix << NEW.prefix
|
||||
AND (
|
||||
(p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))
|
||||
OR
|
||||
(p.vrf_id IS NULL AND p.status = 'container')
|
||||
)
|
||||
AND p.id != NEW.id
|
||||
)
|
||||
;
|
||||
UPDATE ipam_ipaddress
|
||||
SET prefix_id = NEW.id
|
||||
WHERE
|
||||
prefix_id != NEW.id
|
||||
AND
|
||||
address << NEW.prefix
|
||||
AND (
|
||||
(vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
|
||||
OR (
|
||||
NEW.vrf_id IS NULL
|
||||
AND
|
||||
NEW.status = 'container'
|
||||
AND
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM ipam_prefix p WHERE p.prefix >> address AND p.vrf_id = vrf_id
|
||||
)
|
||||
)
|
||||
)
|
||||
;
|
||||
UPDATE ipam_iprange
|
||||
SET prefix_id = NEW.id
|
||||
WHERE
|
||||
prefix_id != NEW.id
|
||||
AND
|
||||
start_address << NEW.prefix
|
||||
AND
|
||||
end_address << NEW.prefix
|
||||
AND (
|
||||
(vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
|
||||
OR (
|
||||
NEW.vrf_id IS NULL
|
||||
AND
|
||||
NEW.status = 'container'
|
||||
AND
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM ipam_prefix p WHERE
|
||||
p.prefix >> start_address
|
||||
AND
|
||||
p.prefix >> end_address
|
||||
AND
|
||||
p.vrf_id = vrf_id
|
||||
)
|
||||
)
|
||||
)
|
||||
;
|
||||
RETURN NEW;
|
||||
"""
|
||||
@@ -687,13 +687,13 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
|
||||
template_name = 'ipam/prefix/ip_addresses.html'
|
||||
tab = ViewTab(
|
||||
label=_('IP Addresses'),
|
||||
badge=lambda x: x.ip_addresses.count(),
|
||||
badge=lambda x: x.get_child_ips().count(),
|
||||
permission='ipam.view_ipaddress',
|
||||
weight=700
|
||||
)
|
||||
|
||||
def get_children(self, request, parent):
|
||||
return parent.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
|
||||
return parent.get_child_ips().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):
|
||||
|
||||
@@ -463,7 +463,6 @@ INSTALLED_APPS = [
|
||||
'sorl.thumbnail',
|
||||
'taggit',
|
||||
'timezone_field',
|
||||
'pgtrigger',
|
||||
'core',
|
||||
'account',
|
||||
'circuits',
|
||||
|
||||
@@ -437,30 +437,12 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
"""
|
||||
return object_form.save()
|
||||
|
||||
def create_and_update_objects(self, form, request):
|
||||
def _process_import_records(self, form, request, records, prefetched_objects):
|
||||
"""
|
||||
Process CSV import records and save objects.
|
||||
"""
|
||||
saved_objects = []
|
||||
|
||||
records = list(form.cleaned_data['data'])
|
||||
|
||||
# Prefetch objects to be updated, if any
|
||||
prefetch_ids = [int(record['id']) for record in records if record.get('id')]
|
||||
|
||||
# check for duplicate IDs
|
||||
duplicate_pks = [pk for pk, count in Counter(prefetch_ids).items() if count > 1]
|
||||
if duplicate_pks:
|
||||
error_msg = _(
|
||||
"Duplicate objects found: {model} with ID(s) {ids} appears multiple times"
|
||||
).format(
|
||||
model=title(self.queryset.model._meta.verbose_name),
|
||||
ids=', '.join(str(pk) for pk in sorted(duplicate_pks))
|
||||
)
|
||||
raise ValidationError(error_msg)
|
||||
|
||||
prefetched_objects = {
|
||||
obj.pk: obj
|
||||
for obj in self.queryset.model.objects.filter(id__in=prefetch_ids)
|
||||
} if prefetch_ids else {}
|
||||
|
||||
for i, record in enumerate(records, start=1):
|
||||
object_id = int(record.pop('id')) if record.get('id') else None
|
||||
|
||||
@@ -524,6 +506,37 @@ class BulkImportView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
|
||||
return saved_objects
|
||||
|
||||
def create_and_update_objects(self, form, request):
|
||||
records = list(form.cleaned_data['data'])
|
||||
|
||||
# Prefetch objects to be updated, if any
|
||||
prefetch_ids = [int(record['id']) for record in records if record.get('id')]
|
||||
|
||||
# check for duplicate IDs
|
||||
duplicate_pks = [pk for pk, count in Counter(prefetch_ids).items() if count > 1]
|
||||
if duplicate_pks:
|
||||
error_msg = _(
|
||||
"Duplicate objects found: {model} with ID(s) {ids} appears multiple times"
|
||||
).format(
|
||||
model=title(self.queryset.model._meta.verbose_name),
|
||||
ids=', '.join(str(pk) for pk in sorted(duplicate_pks))
|
||||
)
|
||||
raise ValidationError(error_msg)
|
||||
|
||||
prefetched_objects = {
|
||||
obj.pk: obj
|
||||
for obj in self.queryset.model.objects.filter(id__in=prefetch_ids)
|
||||
} if prefetch_ids else {}
|
||||
|
||||
# For MPTT models, delay tree updates until all saves are complete
|
||||
if issubclass(self.queryset.model, MPTTModel):
|
||||
with self.queryset.model.objects.delay_mptt_updates():
|
||||
saved_objects = self._process_import_records(form, request, records, prefetched_objects)
|
||||
else:
|
||||
saved_objects = self._process_import_records(form, request, records, prefetched_objects)
|
||||
|
||||
return saved_objects
|
||||
|
||||
#
|
||||
# Request handlers
|
||||
#
|
||||
@@ -893,9 +906,16 @@ class BulkRenameView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
renamed_pks = self._rename_objects(form, selected_objects)
|
||||
|
||||
if '_apply' in request.POST:
|
||||
for obj in selected_objects:
|
||||
setattr(obj, self.field_name, obj.new_name)
|
||||
obj.save()
|
||||
# For MPTT models, delay tree updates until all saves are complete
|
||||
if issubclass(self.queryset.model, MPTTModel):
|
||||
with self.queryset.model.objects.delay_mptt_updates():
|
||||
for obj in selected_objects:
|
||||
setattr(obj, self.field_name, obj.new_name)
|
||||
obj.save()
|
||||
else:
|
||||
for obj in selected_objects:
|
||||
setattr(obj, self.field_name, obj.new_name)
|
||||
obj.save()
|
||||
|
||||
# Enforce constrained permissions
|
||||
if self.queryset.filter(pk__in=renamed_pks).count() != len(selected_objects):
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
<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,7 +14,6 @@
|
||||
<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 %}
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
<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 "Starting Address" %}</th>
|
||||
<td>{{ object.start_address }}</td>
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% with child_ip_count=object.ip_addresses.count %}
|
||||
{% with child_ip_count=object.get_child_ips.count %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Child IPs" %}</th>
|
||||
<td>
|
||||
|
||||
@@ -280,26 +280,6 @@ def as_range(n):
|
||||
return range(n)
|
||||
|
||||
|
||||
@register.filter()
|
||||
def parent_depth(n, parent=None):
|
||||
"""
|
||||
Return the depth of a node based on the parent's depth
|
||||
"""
|
||||
parent_depth = 0
|
||||
if parent and hasattr(parent, 'depth_count'):
|
||||
parent_depth = parent.depth_count + 1
|
||||
elif parent and hasattr(parent, 'depth'):
|
||||
try:
|
||||
parent_depth = int(parent.depth) + 1
|
||||
except TypeError:
|
||||
pass
|
||||
try:
|
||||
depth = int(n) - int(parent_depth)
|
||||
except TypeError:
|
||||
return n
|
||||
return depth
|
||||
|
||||
|
||||
@register.filter()
|
||||
def meters_to_feet(n):
|
||||
"""
|
||||
|
||||
@@ -7,7 +7,6 @@ django-graphiql-debug-toolbar==0.2.0
|
||||
django-htmx==1.27.0
|
||||
django-mptt==0.17.0
|
||||
django-pglocks==1.0.4
|
||||
django-pgtrigger==4.15.4
|
||||
django-prometheus==2.4.1
|
||||
django-redis==6.0.0
|
||||
django-rich==2.2.0
|
||||
|
||||
Reference in New Issue
Block a user