mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 09:28:38 -06:00
* Replace site FK on Prefix with scope GFK * Add denormalized relations * Update prefix filters * Add generic relations for Prefix * Update GraphQL type for Prefix model * Fix tests; misc cleanup * Remove prefix_count from SiteSerializer * Remove site field from PrefixBulkEditForm * Restore scope filters for prefixes * Fix scope population on PrefixForm init * Show scope type * Assign scope during bulk import of prefixes * Correct handling of GenericForeignKey in PrefixForm * Add prefix counts to all scoped objects * Fix migration; linter fix * Add limit_choices_to on scope_type * Clean up cache_related_objects() * Enable bulk editing prefix scope
This commit is contained in:
parent
c78da79ce6
commit
75270c1aef
@ -21,12 +21,13 @@ __all__ = (
|
|||||||
class RegionSerializer(NestedGroupModelSerializer):
|
class RegionSerializer(NestedGroupModelSerializer):
|
||||||
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
|
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
|
||||||
site_count = serializers.IntegerField(read_only=True, default=0)
|
site_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
|
prefix_count = RelatedObjectCountField('_prefixes')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
|
'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
|
||||||
'created', 'last_updated', 'site_count', '_depth',
|
'created', 'last_updated', 'site_count', 'prefix_count', '_depth',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
|
||||||
|
|
||||||
@ -34,12 +35,13 @@ class RegionSerializer(NestedGroupModelSerializer):
|
|||||||
class SiteGroupSerializer(NestedGroupModelSerializer):
|
class SiteGroupSerializer(NestedGroupModelSerializer):
|
||||||
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
|
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
site_count = serializers.IntegerField(read_only=True, default=0)
|
site_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
|
prefix_count = RelatedObjectCountField('_prefixes')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
|
'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
|
||||||
'created', 'last_updated', 'site_count', '_depth',
|
'created', 'last_updated', 'site_count', 'prefix_count', '_depth',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
|
||||||
|
|
||||||
@ -61,7 +63,7 @@ class SiteSerializer(NetBoxModelSerializer):
|
|||||||
# Related object counts
|
# Related object counts
|
||||||
circuit_count = RelatedObjectCountField('circuit_terminations')
|
circuit_count = RelatedObjectCountField('circuit_terminations')
|
||||||
device_count = RelatedObjectCountField('devices')
|
device_count = RelatedObjectCountField('devices')
|
||||||
prefix_count = RelatedObjectCountField('prefixes')
|
prefix_count = RelatedObjectCountField('_prefixes')
|
||||||
rack_count = RelatedObjectCountField('racks')
|
rack_count = RelatedObjectCountField('racks')
|
||||||
vlan_count = RelatedObjectCountField('vlans')
|
vlan_count = RelatedObjectCountField('vlans')
|
||||||
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||||
@ -84,11 +86,13 @@ class LocationSerializer(NestedGroupModelSerializer):
|
|||||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
rack_count = serializers.IntegerField(read_only=True, default=0)
|
rack_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
device_count = serializers.IntegerField(read_only=True, default=0)
|
device_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
|
prefix_count = RelatedObjectCountField('_prefixes')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Location
|
model = Location
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility',
|
'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility',
|
||||||
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
|
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count',
|
||||||
|
'prefix_count', '_depth',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')
|
||||||
|
@ -28,6 +28,12 @@ class Region(ContactsMixin, NestedGroupModel):
|
|||||||
states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are
|
states, and/or cities. Regions are recursively nested into a hierarchy: all sites belonging to a child region are
|
||||||
also considered to be members of its parent and ancestor region(s).
|
also considered to be members of its parent and ancestor region(s).
|
||||||
"""
|
"""
|
||||||
|
prefixes = GenericRelation(
|
||||||
|
to='ipam.Prefix',
|
||||||
|
content_type_field='scope_type',
|
||||||
|
object_id_field='scope_id',
|
||||||
|
related_query_name='region'
|
||||||
|
)
|
||||||
vlan_groups = GenericRelation(
|
vlan_groups = GenericRelation(
|
||||||
to='ipam.VLANGroup',
|
to='ipam.VLANGroup',
|
||||||
content_type_field='scope_type',
|
content_type_field='scope_type',
|
||||||
@ -78,6 +84,12 @@ class SiteGroup(ContactsMixin, NestedGroupModel):
|
|||||||
within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be
|
within corporate sites you might distinguish between offices and data centers. Like regions, site groups can be
|
||||||
nested recursively to form a hierarchy.
|
nested recursively to form a hierarchy.
|
||||||
"""
|
"""
|
||||||
|
prefixes = GenericRelation(
|
||||||
|
to='ipam.Prefix',
|
||||||
|
content_type_field='scope_type',
|
||||||
|
object_id_field='scope_id',
|
||||||
|
related_query_name='site_group'
|
||||||
|
)
|
||||||
vlan_groups = GenericRelation(
|
vlan_groups = GenericRelation(
|
||||||
to='ipam.VLANGroup',
|
to='ipam.VLANGroup',
|
||||||
content_type_field='scope_type',
|
content_type_field='scope_type',
|
||||||
@ -214,6 +226,12 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Generic relations
|
# Generic relations
|
||||||
|
prefixes = GenericRelation(
|
||||||
|
to='ipam.Prefix',
|
||||||
|
content_type_field='scope_type',
|
||||||
|
object_id_field='scope_id',
|
||||||
|
related_query_name='site'
|
||||||
|
)
|
||||||
vlan_groups = GenericRelation(
|
vlan_groups = GenericRelation(
|
||||||
to='ipam.VLANGroup',
|
to='ipam.VLANGroup',
|
||||||
content_type_field='scope_type',
|
content_type_field='scope_type',
|
||||||
@ -273,6 +291,12 @@ class Location(ContactsMixin, ImageAttachmentsMixin, NestedGroupModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Generic relations
|
# Generic relations
|
||||||
|
prefixes = GenericRelation(
|
||||||
|
to='ipam.Prefix',
|
||||||
|
content_type_field='scope_type',
|
||||||
|
object_id_field='scope_id',
|
||||||
|
related_query_name='location'
|
||||||
|
)
|
||||||
vlan_groups = GenericRelation(
|
vlan_groups = GenericRelation(
|
||||||
to='ipam.VLANGroup',
|
to='ipam.VLANGroup',
|
||||||
content_type_field='scope_type',
|
content_type_field='scope_type',
|
||||||
|
@ -2,9 +2,8 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from dcim.api.serializers_.sites import SiteSerializer
|
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
|
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, PREFIX_SCOPE_TYPES
|
||||||
from ipam.models import Aggregate, IPAddress, IPRange, Prefix
|
from ipam.models import Aggregate, IPAddress, IPRange, Prefix
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
@ -45,8 +44,17 @@ class AggregateSerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
class PrefixSerializer(NetBoxModelSerializer):
|
class PrefixSerializer(NetBoxModelSerializer):
|
||||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||||
site = SiteSerializer(nested=True, required=False, allow_null=True)
|
|
||||||
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
|
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
|
||||||
|
scope_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(
|
||||||
|
model__in=PREFIX_SCOPE_TYPES
|
||||||
|
),
|
||||||
|
allow_null=True,
|
||||||
|
required=False,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||||
|
scope = serializers.SerializerMethodField(read_only=True)
|
||||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
vlan = VLANSerializer(nested=True, required=False, allow_null=True)
|
vlan = VLANSerializer(nested=True, required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=PrefixStatusChoices, required=False)
|
status = ChoiceField(choices=PrefixStatusChoices, required=False)
|
||||||
@ -58,12 +66,20 @@ class PrefixSerializer(NetBoxModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status',
|
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'vrf', 'scope_type', 'scope_id', 'scope',
|
||||||
'role', 'is_pool', 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields',
|
'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'comments', 'tags',
|
||||||
'created', 'last_updated', 'children', '_depth',
|
'custom_fields', 'created', 'last_updated', 'children', '_depth',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth')
|
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth')
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_scope(self, obj):
|
||||||
|
if obj.scope_id is None:
|
||||||
|
return None
|
||||||
|
serializer = get_serializer_for_model(obj.scope)
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.scope, nested=True, context=context).data
|
||||||
|
|
||||||
|
|
||||||
class PrefixLengthSerializer(serializers.Serializer):
|
class PrefixLengthSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
from netbox import denormalized
|
||||||
|
|
||||||
|
|
||||||
class IPAMConfig(AppConfig):
|
class IPAMConfig(AppConfig):
|
||||||
name = "ipam"
|
name = "ipam"
|
||||||
@ -8,6 +10,16 @@ class IPAMConfig(AppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
from netbox.models.features import register_models
|
from netbox.models.features import register_models
|
||||||
from . import signals, search # noqa: F401
|
from . import signals, search # noqa: F401
|
||||||
|
from .models import Prefix
|
||||||
|
|
||||||
# Register models
|
# Register models
|
||||||
register_models(*self.get_models())
|
register_models(*self.get_models())
|
||||||
|
|
||||||
|
# Register denormalized fields
|
||||||
|
denormalized.register(Prefix, '_site', {
|
||||||
|
'_region': 'region',
|
||||||
|
'_sitegroup': 'group',
|
||||||
|
})
|
||||||
|
denormalized.register(Prefix, '_location', {
|
||||||
|
'_site': 'site',
|
||||||
|
})
|
||||||
|
@ -23,6 +23,11 @@ VRF_RD_MAX_LENGTH = 21
|
|||||||
PREFIX_LENGTH_MIN = 1
|
PREFIX_LENGTH_MIN = 1
|
||||||
PREFIX_LENGTH_MAX = 127 # IPv6
|
PREFIX_LENGTH_MAX = 127 # IPv6
|
||||||
|
|
||||||
|
# models values for ContentTypes which may be Prefix scope types
|
||||||
|
PREFIX_SCOPE_TYPES = (
|
||||||
|
'region', 'sitegroup', 'site', 'location',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# IPAddresses
|
# IPAddresses
|
||||||
|
@ -9,7 +9,7 @@ from drf_spectacular.utils import extend_schema_field
|
|||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
|
|
||||||
from circuits.models import Provider
|
from circuits.models import Provider
|
||||||
from dcim.models import Device, Interface, Region, Site, SiteGroup
|
from dcim.models import Device, Interface, Location, Region, Site, SiteGroup
|
||||||
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
|
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
|
||||||
from tenancy.filtersets import TenancyFilterSet
|
from tenancy.filtersets import TenancyFilterSet
|
||||||
from utilities.filters import (
|
from utilities.filters import (
|
||||||
@ -332,42 +332,57 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
to_field_name='rd',
|
to_field_name='rd',
|
||||||
label=_('VRF (RD)'),
|
label=_('VRF (RD)'),
|
||||||
)
|
)
|
||||||
|
scope_type = ContentTypeFilter()
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
field_name='site__region',
|
field_name='_region',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
label=_('Region (ID)'),
|
label=_('Region (ID)'),
|
||||||
)
|
)
|
||||||
region = TreeNodeMultipleChoiceFilter(
|
region = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
field_name='site__region',
|
field_name='_region',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Region (slug)'),
|
label=_('Region (slug)'),
|
||||||
)
|
)
|
||||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
field_name='site__group',
|
field_name='_sitegroup',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
label=_('Site group (ID)'),
|
label=_('Site group (ID)'),
|
||||||
)
|
)
|
||||||
site_group = TreeNodeMultipleChoiceFilter(
|
site_group = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
field_name='site__group',
|
field_name='_sitegroup',
|
||||||
lookup_expr='in',
|
lookup_expr='in',
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Site group (slug)'),
|
label=_('Site group (slug)'),
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
|
field_name='_site',
|
||||||
label=_('Site (ID)'),
|
label=_('Site (ID)'),
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='site__slug',
|
field_name='_site__slug',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Site (slug)'),
|
label=_('Site (slug)'),
|
||||||
)
|
)
|
||||||
|
location_id = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
field_name='_location',
|
||||||
|
lookup_expr='in',
|
||||||
|
label=_('Location (ID)'),
|
||||||
|
)
|
||||||
|
location = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
field_name='_location',
|
||||||
|
lookup_expr='in',
|
||||||
|
to_field_name='slug',
|
||||||
|
label=_('Location (slug)'),
|
||||||
|
)
|
||||||
vlan_id = django_filters.ModelMultipleChoiceFilter(
|
vlan_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
label=_('VLAN (ID)'),
|
label=_('VLAN (ID)'),
|
||||||
@ -393,7 +408,7 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ('id', 'is_pool', 'mark_utilized', 'description')
|
fields = ('id', 'scope_id', 'is_pool', 'mark_utilized', 'description')
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -204,24 +204,18 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
|
|
||||||
class PrefixBulkEditForm(NetBoxModelBulkEditForm):
|
class PrefixBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
region = DynamicModelChoiceField(
|
scope_type = ContentTypeChoiceField(
|
||||||
label=_('Region'),
|
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
||||||
queryset=Region.objects.all(),
|
widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
|
||||||
required=False
|
|
||||||
)
|
|
||||||
site_group = DynamicModelChoiceField(
|
|
||||||
label=_('Site group'),
|
|
||||||
queryset=SiteGroup.objects.all(),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
site = DynamicModelChoiceField(
|
|
||||||
label=_('Site'),
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
required=False,
|
required=False,
|
||||||
query_params={
|
label=_('Scope type')
|
||||||
'region_id': '$region',
|
)
|
||||||
'group_id': '$site_group',
|
scope = DynamicModelChoiceField(
|
||||||
}
|
label=_('Scope'),
|
||||||
|
queryset=Site.objects.none(), # Initial queryset
|
||||||
|
required=False,
|
||||||
|
disabled=True,
|
||||||
|
selector=True
|
||||||
)
|
)
|
||||||
vlan_group = DynamicModelChoiceField(
|
vlan_group = DynamicModelChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
@ -282,14 +276,28 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
model = Prefix
|
model = Prefix
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('tenant', 'status', 'role', 'description'),
|
FieldSet('tenant', 'status', 'role', 'description'),
|
||||||
FieldSet('region', 'site_group', 'site', name=_('Site')),
|
|
||||||
FieldSet('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')),
|
FieldSet('vlan_group', 'vlan', name=_('VLAN Assignment')),
|
||||||
)
|
)
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'site', 'vlan', 'vrf', 'tenant', 'role', 'description', 'comments',
|
'vlan', 'vrf', 'tenant', 'role', 'scope', 'description', 'comments',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if scope_type_id := get_field_value(self, 'scope_type'):
|
||||||
|
try:
|
||||||
|
scope_type = ContentType.objects.get(pk=scope_type_id)
|
||||||
|
model = scope_type.model_class()
|
||||||
|
self.fields['scope'].queryset = model.objects.all()
|
||||||
|
self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
|
||||||
|
self.fields['scope'].disabled = False
|
||||||
|
self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
|
class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
vrf = DynamicModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
|
@ -167,12 +167,10 @@ class PrefixImportForm(NetBoxModelImportForm):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text=_('Assigned tenant')
|
help_text=_('Assigned tenant')
|
||||||
)
|
)
|
||||||
site = CSVModelChoiceField(
|
scope_type = CSVContentTypeField(
|
||||||
label=_('Site'),
|
queryset=ContentType.objects.filter(model__in=PREFIX_SCOPE_TYPES),
|
||||||
queryset=Site.objects.all(),
|
|
||||||
required=False,
|
required=False,
|
||||||
to_field_name='name',
|
label=_('Scope type (app & model)')
|
||||||
help_text=_('Assigned site')
|
|
||||||
)
|
)
|
||||||
vlan_group = CSVModelChoiceField(
|
vlan_group = CSVModelChoiceField(
|
||||||
label=_('VLAN group'),
|
label=_('VLAN group'),
|
||||||
@ -204,9 +202,12 @@ class PrefixImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = (
|
fields = (
|
||||||
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized',
|
'prefix', 'vrf', 'tenant', 'vlan_group', 'vlan', 'status', 'role', 'scope_type', 'scope_id', 'is_pool',
|
||||||
'description', 'comments', 'tags',
|
'mark_utilized', 'description', 'comments', 'tags',
|
||||||
)
|
)
|
||||||
|
labels = {
|
||||||
|
'scope_id': 'Scope ID',
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, data=None, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
super().__init__(data, *args, **kwargs)
|
super().__init__(data, *args, **kwargs)
|
||||||
|
@ -170,7 +170,7 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
),
|
),
|
||||||
FieldSet('vlan_id', name=_('VLAN Assignment')),
|
FieldSet('vlan_id', name=_('VLAN Assignment')),
|
||||||
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
|
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
)
|
)
|
||||||
mask_length__lte = forms.IntegerField(
|
mask_length__lte = forms.IntegerField(
|
||||||
@ -224,12 +224,13 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
site_id = DynamicModelMultipleChoiceField(
|
site_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
null_option='None',
|
|
||||||
query_params={
|
|
||||||
'region_id': '$region_id'
|
|
||||||
},
|
|
||||||
label=_('Site')
|
label=_('Site')
|
||||||
)
|
)
|
||||||
|
location_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Location')
|
||||||
|
)
|
||||||
role_id = DynamicModelMultipleChoiceField(
|
role_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -201,12 +201,18 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
|
|||||||
required=False,
|
required=False,
|
||||||
label=_('VRF')
|
label=_('VRF')
|
||||||
)
|
)
|
||||||
site = DynamicModelChoiceField(
|
scope_type = ContentTypeChoiceField(
|
||||||
label=_('Site'),
|
queryset=ContentType.objects.filter(model__in=PREFIX_SCOPE_TYPES),
|
||||||
queryset=Site.objects.all(),
|
widget=HTMXSelect(),
|
||||||
required=False,
|
required=False,
|
||||||
selector=True,
|
label=_('Scope type')
|
||||||
null_option='None'
|
)
|
||||||
|
scope = DynamicModelChoiceField(
|
||||||
|
label=_('Scope'),
|
||||||
|
queryset=Site.objects.none(), # Initial queryset
|
||||||
|
required=False,
|
||||||
|
disabled=True,
|
||||||
|
selector=True
|
||||||
)
|
)
|
||||||
vlan = DynamicModelChoiceField(
|
vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
@ -228,17 +234,48 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
|
|||||||
FieldSet(
|
FieldSet(
|
||||||
'prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix')
|
'prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', name=_('Prefix')
|
||||||
),
|
),
|
||||||
FieldSet('site', 'vlan', name=_('Site/VLAN Assignment')),
|
FieldSet('scope_type', 'scope', name=_('Scope')),
|
||||||
|
FieldSet('vlan', name=_('VLAN Assignment')),
|
||||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = [
|
fields = [
|
||||||
'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'tenant_group', 'tenant',
|
'prefix', 'vrf', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'scope_type', 'tenant_group',
|
||||||
'description', 'comments', 'tags',
|
'tenant', 'description', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
instance = kwargs.get('instance')
|
||||||
|
initial = kwargs.get('initial', {})
|
||||||
|
|
||||||
|
if instance is not None and instance.scope:
|
||||||
|
initial['scope'] = instance.scope
|
||||||
|
kwargs['initial'] = initial
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if scope_type_id := get_field_value(self, 'scope_type'):
|
||||||
|
try:
|
||||||
|
scope_type = ContentType.objects.get(pk=scope_type_id)
|
||||||
|
model = scope_type.model_class()
|
||||||
|
self.fields['scope'].queryset = model.objects.all()
|
||||||
|
self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
|
||||||
|
self.fields['scope'].disabled = False
|
||||||
|
self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.instance and scope_type_id != self.instance.scope_type_id:
|
||||||
|
self.initial['scope'] = None
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Assign the selected scope (if any)
|
||||||
|
self.instance.scope = self.cleaned_data.get('scope')
|
||||||
|
|
||||||
|
|
||||||
class IPRangeForm(TenancyForm, NetBoxModelForm):
|
class IPRangeForm(TenancyForm, NetBoxModelForm):
|
||||||
vrf = DynamicModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
|
@ -152,17 +152,25 @@ class IPRangeType(NetBoxObjectType):
|
|||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
models.Prefix,
|
models.Prefix,
|
||||||
fields='__all__',
|
exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_sitegroup'),
|
||||||
filters=PrefixFilter
|
filters=PrefixFilter
|
||||||
)
|
)
|
||||||
class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType):
|
class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType):
|
||||||
prefix: str
|
prefix: str
|
||||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
|
|
||||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||||
vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
vlan: Annotated["VLANType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
role: Annotated["RoleType", strawberry.lazy('ipam.graphql.types')] | None
|
role: Annotated["RoleType", strawberry.lazy('ipam.graphql.types')] | None
|
||||||
|
|
||||||
|
@strawberry_django.field
|
||||||
|
def scope(self) -> Annotated[Union[
|
||||||
|
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
|
||||||
|
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
|
||||||
|
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
|
||||||
|
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
|
||||||
|
], strawberry.union("PrefixScopeType")] | None:
|
||||||
|
return self.scope
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
models.RIR,
|
models.RIR,
|
||||||
|
51
netbox/ipam/migrations/0071_prefix_scope.py
Normal file
51
netbox/ipam/migrations/0071_prefix_scope.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def copy_site_assignments(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Copy site ForeignKey values to the scope GFK.
|
||||||
|
"""
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
Prefix = apps.get_model('ipam', 'Prefix')
|
||||||
|
Site = apps.get_model('dcim', 'Site')
|
||||||
|
|
||||||
|
Prefix.objects.filter(site__isnull=False).update(
|
||||||
|
scope_type=ContentType.objects.get_for_model(Site),
|
||||||
|
scope_id=models.F('site_id')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('ipam', '0070_vlangroup_vlan_id_ranges'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Add the `scope` GenericForeignKey
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='scope_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='scope_type',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
limit_choices_to=models.Q(('model__in', ('region', 'sitegroup', 'site', 'location'))),
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name='+',
|
||||||
|
to='contenttypes.contenttype'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
# Copy over existing site assignments
|
||||||
|
migrations.RunPython(
|
||||||
|
code=copy_site_assignments,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
61
netbox/ipam/migrations/0072_prefix_cached_relations.py
Normal file
61
netbox/ipam/migrations/0072_prefix_cached_relations.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def populate_denormalized_fields(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Copy site ForeignKey values to the scope GFK.
|
||||||
|
"""
|
||||||
|
Prefix = apps.get_model('ipam', 'Prefix')
|
||||||
|
|
||||||
|
prefixes = Prefix.objects.filter(site__isnull=False).prefetch_related('site')
|
||||||
|
for prefix in prefixes:
|
||||||
|
prefix._region_id = prefix.site.region_id
|
||||||
|
prefix._sitegroup_id = prefix.site.group_id
|
||||||
|
prefix._site_id = prefix.site_id
|
||||||
|
# Note: Location cannot be set prior to migration
|
||||||
|
|
||||||
|
Prefix.objects.bulk_update(prefixes, ['_region', '_sitegroup', '_site'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0193_poweroutlet_color'),
|
||||||
|
('ipam', '0071_prefix_scope'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='_location',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.location'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='_region',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.region'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='_site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.site'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='_sitegroup',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.sitegroup'),
|
||||||
|
),
|
||||||
|
|
||||||
|
# Populate denormalized FK values
|
||||||
|
migrations.RunPython(
|
||||||
|
code=populate_denormalized_fields,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
|
||||||
|
# Delete the site ForeignKey
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='prefix',
|
||||||
|
name='site',
|
||||||
|
),
|
||||||
|
]
|
@ -1,4 +1,5 @@
|
|||||||
import netaddr
|
import netaddr
|
||||||
|
from django.apps import apps
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -199,21 +200,30 @@ class Role(OrganizationalModel):
|
|||||||
|
|
||||||
class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
|
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be scoped to certain
|
||||||
VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role. A Prefix can also be
|
areas and/or assigned to VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role.
|
||||||
assigned to a VLAN where appropriate.
|
A Prefix can also be assigned to a VLAN where appropriate.
|
||||||
"""
|
"""
|
||||||
prefix = IPNetworkField(
|
prefix = IPNetworkField(
|
||||||
verbose_name=_('prefix'),
|
verbose_name=_('prefix'),
|
||||||
help_text=_('IPv4 or IPv6 network with mask')
|
help_text=_('IPv4 or IPv6 network with mask')
|
||||||
)
|
)
|
||||||
site = models.ForeignKey(
|
scope_type = models.ForeignKey(
|
||||||
to='dcim.Site',
|
to='contenttypes.ContentType',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='prefixes',
|
limit_choices_to=Q(model__in=PREFIX_SCOPE_TYPES),
|
||||||
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
scope_id = models.PositiveBigIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
scope = GenericForeignKey(
|
||||||
|
ct_field='scope_type',
|
||||||
|
fk_field='scope_id'
|
||||||
|
)
|
||||||
vrf = models.ForeignKey(
|
vrf = models.ForeignKey(
|
||||||
to='ipam.VRF',
|
to='ipam.VRF',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@ -262,6 +272,36 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
|||||||
help_text=_("Treat as fully utilized")
|
help_text=_("Treat as fully utilized")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Cached associations to enable efficient filtering
|
||||||
|
_location = models.ForeignKey(
|
||||||
|
to='dcim.Location',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='_prefixes',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
_site = models.ForeignKey(
|
||||||
|
to='dcim.Site',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='_prefixes',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
_region = models.ForeignKey(
|
||||||
|
to='dcim.Region',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='_prefixes',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
_sitegroup = models.ForeignKey(
|
||||||
|
to='dcim.SiteGroup',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='_prefixes',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
# Cached depth & child counts
|
# Cached depth & child counts
|
||||||
_depth = models.PositiveSmallIntegerField(
|
_depth = models.PositiveSmallIntegerField(
|
||||||
default=0,
|
default=0,
|
||||||
@ -275,7 +315,7 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
|||||||
objects = PrefixQuerySet.as_manager()
|
objects = PrefixQuerySet.as_manager()
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
|
'scope_type', 'scope_id', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -323,8 +363,30 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
|||||||
# Clear host bits from prefix
|
# Clear host bits from prefix
|
||||||
self.prefix = self.prefix.cidr
|
self.prefix = self.prefix.cidr
|
||||||
|
|
||||||
|
# Cache objects associated with the terminating object (for filtering)
|
||||||
|
self.cache_related_objects()
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def cache_related_objects(self):
|
||||||
|
self._region = self._sitegroup = self._site = self._location = None
|
||||||
|
if self.scope_type:
|
||||||
|
scope_type = self.scope_type.model_class()
|
||||||
|
if scope_type == apps.get_model('dcim', 'region'):
|
||||||
|
self._region = self.scope
|
||||||
|
elif scope_type == apps.get_model('dcim', 'sitegroup'):
|
||||||
|
self._sitegroup = self.scope
|
||||||
|
elif scope_type == apps.get_model('dcim', 'site'):
|
||||||
|
self._region = self.scope.region
|
||||||
|
self._sitegroup = self.scope.group
|
||||||
|
self._site = self.scope
|
||||||
|
elif scope_type == apps.get_model('dcim', 'location'):
|
||||||
|
self._region = self.scope.site.region
|
||||||
|
self._sitegroup = self.scope.site.group
|
||||||
|
self._site = self.scope.site
|
||||||
|
self._location = self.scope
|
||||||
|
cache_related_objects.alters_data = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def family(self):
|
def family(self):
|
||||||
return self.prefix.version if self.prefix else None
|
return self.prefix.version if self.prefix else None
|
||||||
|
@ -241,8 +241,11 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
template_code=VRF_LINK,
|
template_code=VRF_LINK,
|
||||||
verbose_name=_('VRF')
|
verbose_name=_('VRF')
|
||||||
)
|
)
|
||||||
site = tables.Column(
|
scope_type = columns.ContentTypeColumn(
|
||||||
verbose_name=_('Site'),
|
verbose_name=_('Scope Type'),
|
||||||
|
)
|
||||||
|
scope = tables.Column(
|
||||||
|
verbose_name=_('Scope'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
vlan_group = tables.Column(
|
vlan_group = tables.Column(
|
||||||
@ -285,11 +288,12 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
model = Prefix
|
model = Prefix
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group',
|
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group',
|
||||||
'site', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'comments', 'tags',
|
'scope', 'scope_type', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'comments',
|
||||||
'created', 'last_updated',
|
'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description',
|
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'scope', 'vlan', 'role',
|
||||||
|
'description',
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': lambda record: 'success' if not record.pk else '',
|
'class': lambda record: 'success' if not record.pk else '',
|
||||||
|
@ -656,14 +656,14 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
prefixes = (
|
prefixes = (
|
||||||
Prefix(prefix='10.0.0.0/24', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True, description='foobar1'),
|
Prefix(prefix='10.0.0.0/24', tenant=None, scope=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True, description='foobar1'),
|
||||||
Prefix(prefix='10.0.1.0/24', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0], description='foobar2'),
|
Prefix(prefix='10.0.1.0/24', tenant=tenants[0], scope=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0], description='foobar2'),
|
||||||
Prefix(prefix='10.0.2.0/24', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
|
Prefix(prefix='10.0.2.0/24', tenant=tenants[1], scope=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
|
||||||
Prefix(prefix='10.0.3.0/24', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
|
Prefix(prefix='10.0.3.0/24', tenant=tenants[2], scope=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
|
||||||
Prefix(prefix='2001:db8::/64', tenant=None, site=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True),
|
Prefix(prefix='2001:db8::/64', tenant=None, scope=None, vrf=None, vlan=None, role=None, is_pool=True, mark_utilized=True),
|
||||||
Prefix(prefix='2001:db8:0:1::/64', tenant=tenants[0], site=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]),
|
Prefix(prefix='2001:db8:0:1::/64', tenant=tenants[0], scope=sites[0], vrf=vrfs[0], vlan=vlans[0], role=roles[0]),
|
||||||
Prefix(prefix='2001:db8:0:2::/64', tenant=tenants[1], site=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
|
Prefix(prefix='2001:db8:0:2::/64', tenant=tenants[1], scope=sites[1], vrf=vrfs[1], vlan=vlans[1], role=roles[1], status=PrefixStatusChoices.STATUS_DEPRECATED),
|
||||||
Prefix(prefix='2001:db8:0:3::/64', tenant=tenants[2], site=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
|
Prefix(prefix='2001:db8:0:3::/64', tenant=tenants[2], scope=sites[2], vrf=vrfs[2], vlan=vlans[2], role=roles[2], status=PrefixStatusChoices.STATUS_RESERVED),
|
||||||
Prefix(prefix='10.0.0.0/16'),
|
Prefix(prefix='10.0.0.0/16'),
|
||||||
Prefix(prefix='2001:db8::/32'),
|
Prefix(prefix='2001:db8::/32'),
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from netaddr import IPNetwork
|
from netaddr import IPNetwork
|
||||||
@ -409,9 +410,9 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
Role.objects.bulk_create(roles)
|
Role.objects.bulk_create(roles)
|
||||||
|
|
||||||
prefixes = (
|
prefixes = (
|
||||||
Prefix(prefix=IPNetwork('10.1.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]),
|
Prefix(prefix=IPNetwork('10.1.0.0/16'), vrf=vrfs[0], scope=sites[0], role=roles[0]),
|
||||||
Prefix(prefix=IPNetwork('10.2.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]),
|
Prefix(prefix=IPNetwork('10.2.0.0/16'), vrf=vrfs[0], scope=sites[0], role=roles[0]),
|
||||||
Prefix(prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]),
|
Prefix(prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], scope=sites[0], role=roles[0]),
|
||||||
)
|
)
|
||||||
Prefix.objects.bulk_create(prefixes)
|
Prefix.objects.bulk_create(prefixes)
|
||||||
|
|
||||||
@ -419,7 +420,8 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'prefix': IPNetwork('192.0.2.0/24'),
|
'prefix': IPNetwork('192.0.2.0/24'),
|
||||||
'site': sites[1].pk,
|
'scope_type': ContentType.objects.get_for_model(Site).pk,
|
||||||
|
'scope': sites[1].pk,
|
||||||
'vrf': vrfs[1].pk,
|
'vrf': vrfs[1].pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'vlan': None,
|
'vlan': None,
|
||||||
@ -430,11 +432,12 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
site = sites[0].pk
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"vrf,prefix,status",
|
"vrf,prefix,status,scope_type,scope_id",
|
||||||
"VRF 1,10.4.0.0/16,active",
|
f"VRF 1,10.4.0.0/16,active,dcim.site,{site}",
|
||||||
"VRF 1,10.5.0.0/16,active",
|
f"VRF 1,10.5.0.0/16,active,dcim.site,{site}",
|
||||||
"VRF 1,10.6.0.0/16,active",
|
f"VRF 1,10.6.0.0/16,active,dcim.site,{site}",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.csv_update_data = (
|
cls.csv_update_data = (
|
||||||
@ -445,7 +448,6 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
'site': sites[1].pk,
|
|
||||||
'vrf': vrfs[1].pk,
|
'vrf': vrfs[1].pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'status': PrefixStatusChoices.STATUS_RESERVED,
|
'status': PrefixStatusChoices.STATUS_RESERVED,
|
||||||
@ -501,11 +503,13 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
"""
|
"""
|
||||||
Custom import test for YAML-based imports (versus CSV)
|
Custom import test for YAML-based imports (versus CSV)
|
||||||
"""
|
"""
|
||||||
IMPORT_DATA = """
|
site = Site.objects.get(name='Site 1')
|
||||||
|
IMPORT_DATA = f"""
|
||||||
prefix: 10.1.1.0/24
|
prefix: 10.1.1.0/24
|
||||||
status: active
|
status: active
|
||||||
vlan: 101
|
vlan: 101
|
||||||
site: Site 1
|
scope_type: dcim.site
|
||||||
|
scope_id: {site.pk}
|
||||||
"""
|
"""
|
||||||
# Note, a site is not tied to the VLAN to verify the fix for #12622
|
# Note, a site is not tied to the VLAN to verify the fix for #12622
|
||||||
VLAN.objects.create(vid=101, name='VLAN101')
|
VLAN.objects.create(vid=101, name='VLAN101')
|
||||||
@ -523,19 +527,21 @@ site: Site 1
|
|||||||
prefix = Prefix.objects.get(prefix='10.1.1.0/24')
|
prefix = Prefix.objects.get(prefix='10.1.1.0/24')
|
||||||
self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_ACTIVE)
|
self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_ACTIVE)
|
||||||
self.assertEqual(prefix.vlan.vid, 101)
|
self.assertEqual(prefix.vlan.vid, 101)
|
||||||
self.assertEqual(prefix.site.name, "Site 1")
|
self.assertEqual(prefix.scope, site)
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
def test_prefix_import_with_vlan_group(self):
|
def test_prefix_import_with_vlan_group(self):
|
||||||
"""
|
"""
|
||||||
This test covers a unique import edge case where VLAN group is specified during the import.
|
This test covers a unique import edge case where VLAN group is specified during the import.
|
||||||
"""
|
"""
|
||||||
IMPORT_DATA = """
|
site = Site.objects.get(name='Site 1')
|
||||||
|
IMPORT_DATA = f"""
|
||||||
prefix: 10.1.2.0/24
|
prefix: 10.1.2.0/24
|
||||||
status: active
|
status: active
|
||||||
vlan: 102
|
scope_type: dcim.site
|
||||||
site: Site 1
|
scope_id: {site.pk}
|
||||||
vlan_group: Group 1
|
vlan_group: Group 1
|
||||||
|
vlan: 102
|
||||||
"""
|
"""
|
||||||
vlan_group = VLANGroup.objects.create(name='Group 1', slug='group-1', scope=Site.objects.get(name="Site 1"))
|
vlan_group = VLANGroup.objects.create(name='Group 1', slug='group-1', scope=Site.objects.get(name="Site 1"))
|
||||||
VLAN.objects.create(vid=102, name='VLAN102', group=vlan_group)
|
VLAN.objects.create(vid=102, name='VLAN102', group=vlan_group)
|
||||||
@ -553,7 +559,7 @@ vlan_group: Group 1
|
|||||||
prefix = Prefix.objects.get(prefix='10.1.2.0/24')
|
prefix = Prefix.objects.get(prefix='10.1.2.0/24')
|
||||||
self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_ACTIVE)
|
self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_ACTIVE)
|
||||||
self.assertEqual(prefix.vlan.vid, 102)
|
self.assertEqual(prefix.vlan.vid, 102)
|
||||||
self.assertEqual(prefix.site.name, "Site 1")
|
self.assertEqual(prefix.scope, site)
|
||||||
|
|
||||||
|
|
||||||
class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
|
@ -352,7 +352,7 @@ class AggregatePrefixesView(generic.ObjectChildrenView):
|
|||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
return Prefix.objects.restrict(request.user, 'view').filter(
|
return Prefix.objects.restrict(request.user, 'view').filter(
|
||||||
prefix__net_contained_or_equal=str(parent.prefix)
|
prefix__net_contained_or_equal=str(parent.prefix)
|
||||||
).prefetch_related('site', 'role', 'tenant', 'tenant__group', 'vlan')
|
).prefetch_related('scope', 'role', 'tenant', 'tenant__group', 'vlan')
|
||||||
|
|
||||||
def prep_table_data(self, request, queryset, parent):
|
def prep_table_data(self, request, queryset, parent):
|
||||||
# Determine whether to show assigned prefixes, available prefixes, or both
|
# Determine whether to show assigned prefixes, available prefixes, or both
|
||||||
@ -492,7 +492,7 @@ class PrefixView(generic.ObjectView):
|
|||||||
).filter(
|
).filter(
|
||||||
prefix__net_contains=str(instance.prefix)
|
prefix__net_contains=str(instance.prefix)
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'site', 'role', 'tenant', 'vlan',
|
'scope', 'role', 'tenant', 'vlan',
|
||||||
)
|
)
|
||||||
parent_prefix_table = tables.PrefixTable(
|
parent_prefix_table = tables.PrefixTable(
|
||||||
list(parent_prefixes),
|
list(parent_prefixes),
|
||||||
@ -506,7 +506,7 @@ class PrefixView(generic.ObjectView):
|
|||||||
).exclude(
|
).exclude(
|
||||||
pk=instance.pk
|
pk=instance.pk
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'site', 'role', 'tenant', 'vlan',
|
'scope', 'role', 'tenant', 'vlan',
|
||||||
)
|
)
|
||||||
duplicate_prefix_table = tables.PrefixTable(
|
duplicate_prefix_table = tables.PrefixTable(
|
||||||
list(duplicate_prefixes),
|
list(duplicate_prefixes),
|
||||||
@ -538,7 +538,7 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
|
|||||||
|
|
||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
|
return parent.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
|
||||||
'site', 'vrf', 'vlan', 'role', 'tenant', 'tenant__group'
|
'scope', 'vrf', 'vlan', 'role', 'tenant', 'tenant__group'
|
||||||
)
|
)
|
||||||
|
|
||||||
def prep_table_data(self, request, queryset, parent):
|
def prep_table_data(self, request, queryset, parent):
|
||||||
|
@ -4,12 +4,10 @@ from django.conf import settings
|
|||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from netaddr import IPNetwork
|
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
from dcim.models import Site
|
from dcim.models import Rack, Site
|
||||||
from ipam.models import Prefix
|
|
||||||
from users.models import Group, ObjectPermission, Token, User
|
from users.models import Group, ObjectPermission, Token, User
|
||||||
from utilities.testing import TestCase
|
from utilities.testing import TestCase
|
||||||
from utilities.testing.api import APITestCase
|
from utilities.testing.api import APITestCase
|
||||||
@ -410,18 +408,18 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
Site.objects.bulk_create(cls.sites)
|
Site.objects.bulk_create(cls.sites)
|
||||||
|
|
||||||
cls.prefixes = (
|
cls.racks = (
|
||||||
Prefix(prefix=IPNetwork('10.0.0.0/24'), site=cls.sites[0]),
|
Rack(name='Rack 1', site=cls.sites[0]),
|
||||||
Prefix(prefix=IPNetwork('10.0.1.0/24'), site=cls.sites[0]),
|
Rack(name='Rack 2', site=cls.sites[0]),
|
||||||
Prefix(prefix=IPNetwork('10.0.2.0/24'), site=cls.sites[0]),
|
Rack(name='Rack 3', site=cls.sites[0]),
|
||||||
Prefix(prefix=IPNetwork('10.0.3.0/24'), site=cls.sites[1]),
|
Rack(name='Rack 4', site=cls.sites[1]),
|
||||||
Prefix(prefix=IPNetwork('10.0.4.0/24'), site=cls.sites[1]),
|
Rack(name='Rack 5', site=cls.sites[1]),
|
||||||
Prefix(prefix=IPNetwork('10.0.5.0/24'), site=cls.sites[1]),
|
Rack(name='Rack 6', site=cls.sites[1]),
|
||||||
Prefix(prefix=IPNetwork('10.0.6.0/24'), site=cls.sites[2]),
|
Rack(name='Rack 7', site=cls.sites[2]),
|
||||||
Prefix(prefix=IPNetwork('10.0.7.0/24'), site=cls.sites[2]),
|
Rack(name='Rack 8', site=cls.sites[2]),
|
||||||
Prefix(prefix=IPNetwork('10.0.8.0/24'), site=cls.sites[2]),
|
Rack(name='Rack 9', site=cls.sites[2]),
|
||||||
)
|
)
|
||||||
Prefix.objects.bulk_create(cls.prefixes)
|
Rack.objects.bulk_create(cls.racks)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""
|
"""
|
||||||
@ -435,8 +433,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
def test_get_object(self):
|
def test_get_object(self):
|
||||||
|
|
||||||
# Attempt to retrieve object without permission
|
# Attempt to retrieve object without permission
|
||||||
url = reverse('ipam-api:prefix-detail',
|
url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk})
|
||||||
kwargs={'pk': self.prefixes[0].pk})
|
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@ -448,23 +445,21 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
obj_perm.users.add(self.user)
|
||||||
obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix))
|
obj_perm.object_types.add(ObjectType.objects.get_for_model(Rack))
|
||||||
|
|
||||||
# Retrieve permitted object
|
# Retrieve permitted object
|
||||||
url = reverse('ipam-api:prefix-detail',
|
url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk})
|
||||||
kwargs={'pk': self.prefixes[0].pk})
|
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Attempt to retrieve non-permitted object
|
# Attempt to retrieve non-permitted object
|
||||||
url = reverse('ipam-api:prefix-detail',
|
url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[3].pk})
|
||||||
kwargs={'pk': self.prefixes[3].pk})
|
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||||
def test_list_objects(self):
|
def test_list_objects(self):
|
||||||
url = reverse('ipam-api:prefix-list')
|
url = reverse('dcim-api:rack-list')
|
||||||
|
|
||||||
# Attempt to list objects without permission
|
# Attempt to list objects without permission
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
@ -478,7 +473,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
obj_perm.users.add(self.user)
|
||||||
obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix))
|
obj_perm.object_types.add(ObjectType.objects.get_for_model(Rack))
|
||||||
|
|
||||||
# Retrieve all objects. Only permitted objects should be returned.
|
# Retrieve all objects. Only permitted objects should be returned.
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
@ -487,12 +482,12 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||||
def test_create_object(self):
|
def test_create_object(self):
|
||||||
url = reverse('ipam-api:prefix-list')
|
url = reverse('dcim-api:rack-list')
|
||||||
data = {
|
data = {
|
||||||
'prefix': '10.0.9.0/24',
|
'name': 'Rack 10',
|
||||||
'site': self.sites[1].pk,
|
'site': self.sites[1].pk,
|
||||||
}
|
}
|
||||||
initial_count = Prefix.objects.count()
|
initial_count = Rack.objects.count()
|
||||||
|
|
||||||
# Attempt to create an object without permission
|
# Attempt to create an object without permission
|
||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
@ -506,26 +501,25 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
obj_perm.users.add(self.user)
|
||||||
obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix))
|
obj_perm.object_types.add(ObjectType.objects.get_for_model(Rack))
|
||||||
|
|
||||||
# Attempt to create a non-permitted object
|
# Attempt to create a non-permitted object
|
||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
self.assertEqual(Prefix.objects.count(), initial_count)
|
self.assertEqual(Rack.objects.count(), initial_count)
|
||||||
|
|
||||||
# Create a permitted object
|
# Create a permitted object
|
||||||
data['site'] = self.sites[0].pk
|
data['site'] = self.sites[0].pk
|
||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
self.assertEqual(response.status_code, 201)
|
self.assertEqual(response.status_code, 201)
|
||||||
self.assertEqual(Prefix.objects.count(), initial_count + 1)
|
self.assertEqual(Rack.objects.count(), initial_count + 1)
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
||||||
def test_edit_object(self):
|
def test_edit_object(self):
|
||||||
|
|
||||||
# Attempt to edit an object without permission
|
# Attempt to edit an object without permission
|
||||||
data = {'site': self.sites[0].pk}
|
data = {'site': self.sites[0].pk}
|
||||||
url = reverse('ipam-api:prefix-detail',
|
url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk})
|
||||||
kwargs={'pk': self.prefixes[0].pk})
|
|
||||||
response = self.client.patch(url, data, format='json', **self.header)
|
response = self.client.patch(url, data, format='json', **self.header)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@ -537,26 +531,23 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
obj_perm.users.add(self.user)
|
||||||
obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix))
|
obj_perm.object_types.add(ObjectType.objects.get_for_model(Rack))
|
||||||
|
|
||||||
# Attempt to edit a non-permitted object
|
# Attempt to edit a non-permitted object
|
||||||
data = {'site': self.sites[0].pk}
|
data = {'site': self.sites[0].pk}
|
||||||
url = reverse('ipam-api:prefix-detail',
|
url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[3].pk})
|
||||||
kwargs={'pk': self.prefixes[3].pk})
|
|
||||||
response = self.client.patch(url, data, format='json', **self.header)
|
response = self.client.patch(url, data, format='json', **self.header)
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
# Edit a permitted object
|
# Edit a permitted object
|
||||||
data['status'] = 'reserved'
|
data['status'] = 'reserved'
|
||||||
url = reverse('ipam-api:prefix-detail',
|
url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk})
|
||||||
kwargs={'pk': self.prefixes[0].pk})
|
|
||||||
response = self.client.patch(url, data, format='json', **self.header)
|
response = self.client.patch(url, data, format='json', **self.header)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Attempt to modify a permitted object to a non-permitted object
|
# Attempt to modify a permitted object to a non-permitted object
|
||||||
data['site'] = self.sites[1].pk
|
data['site'] = self.sites[1].pk
|
||||||
url = reverse('ipam-api:prefix-detail',
|
url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk})
|
||||||
kwargs={'pk': self.prefixes[0].pk})
|
|
||||||
response = self.client.patch(url, data, format='json', **self.header)
|
response = self.client.patch(url, data, format='json', **self.header)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@ -564,8 +555,7 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
def test_delete_object(self):
|
def test_delete_object(self):
|
||||||
|
|
||||||
# Attempt to delete an object without permission
|
# Attempt to delete an object without permission
|
||||||
url = reverse('ipam-api:prefix-detail',
|
url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk})
|
||||||
kwargs={'pk': self.prefixes[0].pk})
|
|
||||||
response = self.client.delete(url, format='json', **self.header)
|
response = self.client.delete(url, format='json', **self.header)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@ -577,16 +567,14 @@ class ObjectPermissionAPIViewTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.users.add(self.user)
|
obj_perm.users.add(self.user)
|
||||||
obj_perm.object_types.add(ObjectType.objects.get_for_model(Prefix))
|
obj_perm.object_types.add(ObjectType.objects.get_for_model(Rack))
|
||||||
|
|
||||||
# Attempt to delete a non-permitted object
|
# Attempt to delete a non-permitted object
|
||||||
url = reverse('ipam-api:prefix-detail',
|
url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[3].pk})
|
||||||
kwargs={'pk': self.prefixes[3].pk})
|
|
||||||
response = self.client.delete(url, format='json', **self.header)
|
response = self.client.delete(url, format='json', **self.header)
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
# Delete a permitted object
|
# Delete a permitted object
|
||||||
url = reverse('ipam-api:prefix-detail',
|
url = reverse('dcim-api:rack-detail', kwargs={'pk': self.racks[0].pk})
|
||||||
kwargs={'pk': self.prefixes[0].pk})
|
|
||||||
response = self.client.delete(url, format='json', **self.header)
|
response = self.client.delete(url, format='json', **self.header)
|
||||||
self.assertEqual(response.status_code, 204)
|
self.assertEqual(response.status_code, 204)
|
||||||
|
@ -44,17 +44,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if object.site.region %}
|
|
||||||
<tr>
|
|
||||||
<th scope="row">{% trans "Region" %}</th>
|
|
||||||
<td>
|
|
||||||
{% nested_tree object.site.region %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Site" %}</th>
|
<th scope="row">{% trans "Scope" %}</th>
|
||||||
<td>{{ object.site|linkify|placeholder }}</td>
|
{% if object.scope %}
|
||||||
|
<td>{{ object.scope|linkify }} ({% trans object.scope_type.name %})</td>
|
||||||
|
{% else %}
|
||||||
|
<td>{{ ''|placeholder }}</td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "VLAN" %}</th>
|
<th scope="row">{% trans "VLAN" %}</th>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.postgres.fields import ArrayField, RangeField
|
from django.contrib.postgres.fields import ArrayField, RangeField
|
||||||
from django.core.exceptions import FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
@ -120,6 +121,10 @@ class ModelTestCase(TestCase):
|
|||||||
else:
|
else:
|
||||||
model_dict[key] = sorted([obj.pk for obj in value])
|
model_dict[key] = sorted([obj.pk for obj in value])
|
||||||
|
|
||||||
|
# Handle GenericForeignKeys
|
||||||
|
elif value and type(field) is GenericForeignKey:
|
||||||
|
model_dict[key] = value.pk
|
||||||
|
|
||||||
elif api:
|
elif api:
|
||||||
|
|
||||||
# Replace ContentType numeric IDs with <app_label>.<model>
|
# Replace ContentType numeric IDs with <app_label>.<model>
|
||||||
|
Loading…
Reference in New Issue
Block a user