mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
Merge pull request #5985 from netbox-community/5284-vlangroup-scope
Closes #5284: Allow VLANGroup assignment beyond sites
This commit is contained in:
commit
ee7f7c877a
@ -52,6 +52,12 @@ When exporting a list of objects in NetBox, users now have the option of selecti
|
|||||||
|
|
||||||
The legacy static export behavior has been retained to ensure backward compatibility for dependent integrations. However, users are strongly encouraged to adapt custom export templates where needed as this functionality will be removed in v2.12.
|
The legacy static export behavior has been retained to ensure backward compatibility for dependent integrations. However, users are strongly encouraged to adapt custom export templates where needed as this functionality will be removed in v2.12.
|
||||||
|
|
||||||
|
#### Variable Scope Support for VLAN Groups ([#5284](https://github.com/netbox-community/netbox/issues/5284))
|
||||||
|
|
||||||
|
In previous releases, VLAN groups could be assigned only to a site. To afford more flexibility in conveying the true scope of an L2 domain, a VLAN group can now be assigned to a region, site group (new in v2.11), site, location, or rack. VLANs assigned to a group will be available only to devices and virtual machines which exist within its scope.
|
||||||
|
|
||||||
|
For example, a VLAN within a group assigned to a location will be available only to devices assigned to that location (or one of its child locations), or to a rack within that location.
|
||||||
|
|
||||||
#### New Site Group Model ([#5892](https://github.com/netbox-community/netbox/issues/5892))
|
#### New Site Group Model ([#5892](https://github.com/netbox-community/netbox/issues/5892))
|
||||||
|
|
||||||
This release introduces the new Site Group model, which can be used to organize sites similar to the existing Region model. Whereas regions are intended for geographically arranging sites into countries, states, and so on, the new site group model can be used to organize sites by role or other arbitrary classification. Using regions and site groups in conjunction provides two dimensions along which sites can be organized, offering greater flexibility to the user.
|
This release introduces the new Site Group model, which can be used to organize sites similar to the existing Region model. Whereas regions are intended for geographically arranging sites into countries, states, and so on, the new site group model can be used to organize sites by role or other arbitrary classification. Using regions and site groups in conjunction provides two dimensions along which sites can be organized, offering greater flexibility to the user.
|
||||||
@ -116,3 +122,6 @@ The ObjectChange model (which is used to record the creation, modification, and
|
|||||||
* Renamed `object_data` to `postchange_data`
|
* Renamed `object_data` to `postchange_data`
|
||||||
* extras.Webhook
|
* extras.Webhook
|
||||||
* Added the `/api/extras/webhooks/` endpoint
|
* Added the `/api/extras/webhooks/` endpoint
|
||||||
|
* ipam.VLANGroup
|
||||||
|
* Added the `scope_type`, `scope_id`, and `scope` fields (`scope` is a generic foreign key)
|
||||||
|
* Dropped the `site` foreign key field
|
||||||
|
@ -113,14 +113,21 @@ class RoleSerializer(OrganizationalModelSerializer):
|
|||||||
|
|
||||||
class VLANGroupSerializer(OrganizationalModelSerializer):
|
class VLANGroupSerializer(OrganizationalModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
scope_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(
|
||||||
|
app_label='dcim',
|
||||||
|
model__in=['region', 'sitegroup', 'site', 'location', 'rack']
|
||||||
|
),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
scope = serializers.SerializerMethodField(read_only=True)
|
||||||
vlan_count = serializers.IntegerField(read_only=True)
|
vlan_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'name', 'slug', 'site', 'description', 'custom_fields', 'created', 'last_updated',
|
'id', 'url', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'description', 'custom_fields', 'created',
|
||||||
'vlan_count',
|
'last_updated', 'vlan_count',
|
||||||
]
|
]
|
||||||
validators = []
|
validators = []
|
||||||
|
|
||||||
@ -137,6 +144,14 @@ class VLANGroupSerializer(OrganizationalModelSerializer):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def get_scope(self, obj):
|
||||||
|
if obj.scope_id is None:
|
||||||
|
return None
|
||||||
|
serializer = get_serializer_for_model(obj.scope, prefix='Nested')
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
|
||||||
|
return serializer(obj.scope, context=context).data
|
||||||
|
|
||||||
|
|
||||||
class VLANSerializer(PrimaryModelSerializer):
|
class VLANSerializer(PrimaryModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||||
|
@ -283,7 +283,7 @@ class IPAddressViewSet(CustomFieldModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VLANGroupViewSet(CustomFieldModelViewSet):
|
class VLANGroupViewSet(CustomFieldModelViewSet):
|
||||||
queryset = VLANGroup.objects.prefetch_related('site').annotate(
|
queryset = VLANGroup.objects.annotate(
|
||||||
vlan_count=count_related(VLAN, 'group')
|
vlan_count=count_related(VLAN, 'group')
|
||||||
)
|
)
|
||||||
serializer_class = serializers.VLANGroupSerializer
|
serializer_class = serializers.VLANGroupSerializer
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
import netaddr
|
import netaddr
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
@ -8,8 +9,8 @@ from dcim.models import Device, Interface, Region, Site, SiteGroup
|
|||||||
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
|
from extras.filters import CustomFieldModelFilterSet, CreatedUpdatedFilterSet
|
||||||
from tenancy.filters import TenancyFilterSet
|
from tenancy.filters import TenancyFilterSet
|
||||||
from utilities.filters import (
|
from utilities.filters import (
|
||||||
BaseFilterSet, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericArrayFilter, TagFilter,
|
BaseFilterSet, ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NameSlugSearchFilterSet,
|
||||||
TreeNodeMultipleChoiceFilter,
|
NumericArrayFilter, TagFilter, TreeNodeMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
from virtualization.models import VirtualMachine, VMInterface
|
from virtualization.models import VirtualMachine, VMInterface
|
||||||
from .choices import *
|
from .choices import *
|
||||||
@ -535,46 +536,38 @@ class IPAddressFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilter
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
class VLANGroupFilterSet(BaseFilterSet, NameSlugSearchFilterSet):
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
scope_type = ContentTypeFilter()
|
||||||
queryset=Region.objects.all(),
|
region = django_filters.NumberFilter(
|
||||||
field_name='site__region',
|
method='filter_scope'
|
||||||
lookup_expr='in',
|
|
||||||
label='Region (ID)',
|
|
||||||
)
|
)
|
||||||
region = TreeNodeMultipleChoiceFilter(
|
sitegroup = django_filters.NumberFilter(
|
||||||
queryset=Region.objects.all(),
|
method='filter_scope'
|
||||||
field_name='site__region',
|
|
||||||
lookup_expr='in',
|
|
||||||
to_field_name='slug',
|
|
||||||
label='Region (slug)',
|
|
||||||
)
|
)
|
||||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
site = django_filters.NumberFilter(
|
||||||
queryset=SiteGroup.objects.all(),
|
method='filter_scope'
|
||||||
field_name='site__group',
|
|
||||||
lookup_expr='in',
|
|
||||||
label='Site group (ID)',
|
|
||||||
)
|
)
|
||||||
site_group = TreeNodeMultipleChoiceFilter(
|
location = django_filters.NumberFilter(
|
||||||
queryset=SiteGroup.objects.all(),
|
method='filter_scope'
|
||||||
field_name='site__group',
|
|
||||||
lookup_expr='in',
|
|
||||||
to_field_name='slug',
|
|
||||||
label='Site group (slug)',
|
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
rack = django_filters.NumberFilter(
|
||||||
queryset=Site.objects.all(),
|
method='filter_scope'
|
||||||
label='Site (ID)',
|
|
||||||
)
|
)
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
clustergroup = django_filters.NumberFilter(
|
||||||
field_name='site__slug',
|
method='filter_scope'
|
||||||
queryset=Site.objects.all(),
|
)
|
||||||
to_field_name='slug',
|
cluster = django_filters.NumberFilter(
|
||||||
label='Site (slug)',
|
method='filter_scope'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = ['id', 'name', 'slug', 'description']
|
fields = ['id', 'name', 'slug', 'description', 'scope_id']
|
||||||
|
|
||||||
|
def filter_scope(self, queryset, name, value):
|
||||||
|
return queryset.filter(
|
||||||
|
scope_type=ContentType.objects.get(model=name),
|
||||||
|
scope_id=value
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, CreatedUpdatedFilterSet):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.models import Device, Interface, Rack, Region, Site, SiteGroup
|
from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
|
||||||
from extras.forms import (
|
from extras.forms import (
|
||||||
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
|
AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
|
||||||
)
|
)
|
||||||
@ -13,7 +13,7 @@ from utilities.forms import (
|
|||||||
DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, NumericArrayField,
|
DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableIPAddressField, NumericArrayField,
|
||||||
ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
ReturnURLForm, SlugField, StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
)
|
)
|
||||||
from virtualization.models import Cluster, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
from .models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||||
@ -1161,19 +1161,88 @@ class VLANGroupForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'locations': '$location'
|
||||||
|
},
|
||||||
query_params={
|
query_params={
|
||||||
'region_id': '$region',
|
'region_id': '$region',
|
||||||
'group_id': '$site_group',
|
'group_id': '$site_group',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
location = DynamicModelChoiceField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'racks': '$rack'
|
||||||
|
},
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
rack = DynamicModelChoiceField(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site',
|
||||||
|
'location_id': '$location',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cluster_group = DynamicModelChoiceField(
|
||||||
|
queryset=ClusterGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'clusters': '$cluster'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cluster = DynamicModelChoiceField(
|
||||||
|
queryset=Cluster.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'group_id': '$cluster_group',
|
||||||
|
}
|
||||||
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = [
|
fields = [
|
||||||
'region', 'site', 'name', 'slug', 'description',
|
'name', 'slug', 'description', 'region', 'site_group', 'site', 'location', 'rack', 'cluster_group',
|
||||||
|
'cluster',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
instance = kwargs.get('instance')
|
||||||
|
initial = kwargs.get('initial', {})
|
||||||
|
|
||||||
|
if instance is not None and instance.scope:
|
||||||
|
if type(instance.scope) is Rack:
|
||||||
|
initial['rack'] = instance.scope
|
||||||
|
elif type(instance.scope) is Location:
|
||||||
|
initial['location'] = instance.scope
|
||||||
|
elif type(instance.scope) is Site:
|
||||||
|
initial['site'] = instance.scope
|
||||||
|
elif type(instance.scope) is SiteGroup:
|
||||||
|
initial['site_group'] = instance.scope
|
||||||
|
elif type(instance.scope) is Region:
|
||||||
|
initial['region'] = instance.scope
|
||||||
|
elif type(instance.scope) is Cluster:
|
||||||
|
initial['cluster'] = instance.scope
|
||||||
|
elif type(instance.scope) is ClusterGroup:
|
||||||
|
initial['cluster_group'] = instance.scope
|
||||||
|
|
||||||
|
kwargs['initial'] = initial
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Assign scope object
|
||||||
|
self.instance.scope = self.cleaned_data['rack'] or self.cleaned_data['location'] or \
|
||||||
|
self.cleaned_data['site'] or self.cleaned_data['site_group'] or \
|
||||||
|
self.cleaned_data['region'] or self.cleaned_data['cluster'] or \
|
||||||
|
self.cleaned_data['cluster_group'] or None
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupCSVForm(CustomFieldModelCSVForm):
|
class VLANGroupCSVForm(CustomFieldModelCSVForm):
|
||||||
site = CSVModelChoiceField(
|
site = CSVModelChoiceField(
|
||||||
@ -1208,25 +1277,31 @@ class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Region')
|
label=_('Region')
|
||||||
)
|
)
|
||||||
site_group_id = DynamicModelMultipleChoiceField(
|
sitegroup = DynamicModelMultipleChoiceField(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Site group')
|
label=_('Site group')
|
||||||
)
|
)
|
||||||
site_id = DynamicModelMultipleChoiceField(
|
site = 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 = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Location')
|
||||||
|
)
|
||||||
|
rack = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Rack')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -1234,19 +1309,47 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||||
|
# VLANGroup assignment fields
|
||||||
|
scope_type = forms.ChoiceField(
|
||||||
|
choices=(
|
||||||
|
('', ''),
|
||||||
|
('dcim.region', 'Region'),
|
||||||
|
('dcim.sitegroup', 'Site group'),
|
||||||
|
('dcim.site', 'Site'),
|
||||||
|
('dcim.location', 'Location'),
|
||||||
|
('dcim.rack', 'Rack'),
|
||||||
|
('virtualization.clustergroup', 'Cluster group'),
|
||||||
|
('virtualization.cluster', 'Cluster'),
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
widget=StaticSelect2,
|
||||||
|
label='Group scope'
|
||||||
|
)
|
||||||
|
group = DynamicModelChoiceField(
|
||||||
|
queryset=VLANGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'scope_type': '$scope_type',
|
||||||
|
},
|
||||||
|
label='VLAN Group'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Site assignment fields
|
||||||
region = DynamicModelChoiceField(
|
region = DynamicModelChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
initial_params={
|
initial_params={
|
||||||
'sites': '$site'
|
'sites': '$site'
|
||||||
}
|
},
|
||||||
|
label='Region'
|
||||||
)
|
)
|
||||||
site_group = DynamicModelChoiceField(
|
sitegroup = DynamicModelChoiceField(
|
||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
initial_params={
|
initial_params={
|
||||||
'sites': '$site'
|
'sites': '$site'
|
||||||
}
|
},
|
||||||
|
label='Site group'
|
||||||
)
|
)
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
@ -1254,16 +1357,11 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
null_option='None',
|
null_option='None',
|
||||||
query_params={
|
query_params={
|
||||||
'region_id': '$region',
|
'region_id': '$region',
|
||||||
'group_id': '$site_group',
|
'group_id': '$sitegroup',
|
||||||
}
|
|
||||||
)
|
|
||||||
group = DynamicModelChoiceField(
|
|
||||||
queryset=VLANGroup.objects.all(),
|
|
||||||
required=False,
|
|
||||||
query_params={
|
|
||||||
'site_id': '$site'
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Other fields
|
||||||
role = DynamicModelChoiceField(
|
role = DynamicModelChoiceField(
|
||||||
queryset=Role.objects.all(),
|
queryset=Role.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
@ -1278,11 +1376,6 @@ class VLANForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags',
|
'site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags',
|
||||||
]
|
]
|
||||||
fieldsets = (
|
|
||||||
('VLAN', ('vid', 'name', 'status', 'role', 'description', 'tags')),
|
|
||||||
('Assignment', ('region', 'site_group', 'site', 'group')),
|
|
||||||
('Tenancy', ('tenant_group', 'tenant')),
|
|
||||||
)
|
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'site': "Leave blank if this VLAN spans multiple sites",
|
'site': "Leave blank if this VLAN spans multiple sites",
|
||||||
'group': "VLAN group (optional)",
|
'group': "VLAN group (optional)",
|
||||||
@ -1334,15 +1427,6 @@ class VLANCSVForm(CustomFieldModelCSVForm):
|
|||||||
'name': 'VLAN name',
|
'name': 'VLAN name',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, data=None, *args, **kwargs):
|
|
||||||
super().__init__(data, *args, **kwargs)
|
|
||||||
|
|
||||||
if data:
|
|
||||||
|
|
||||||
# Limit vlan queryset by assigned group
|
|
||||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
|
||||||
self.fields['group'].queryset = self.fields['group'].queryset.filter(**params)
|
|
||||||
|
|
||||||
|
|
||||||
class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
|
36
netbox/ipam/migrations/0045_vlangroup_scope.py
Normal file
36
netbox/ipam/migrations/0045_vlangroup_scope.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('ipam', '0044_standardize_models'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='vlangroup',
|
||||||
|
old_name='site',
|
||||||
|
new_name='scope_id',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vlangroup',
|
||||||
|
name='scope_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='vlangroup',
|
||||||
|
name='scope_type',
|
||||||
|
field=models.ForeignKey(blank=True, limit_choices_to=models.Q(model__in=['region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster']), null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='vlangroup',
|
||||||
|
options={'ordering': ('name', 'pk'), 'verbose_name': 'VLAN group', 'verbose_name_plural': 'VLAN groups'},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='vlangroup',
|
||||||
|
unique_together={('scope_type', 'scope_id', 'name'), ('scope_type', 'scope_id', 'slug')},
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,5 @@
|
|||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -29,13 +31,23 @@ class VLANGroup(OrganizationalModel):
|
|||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
max_length=100
|
max_length=100
|
||||||
)
|
)
|
||||||
site = models.ForeignKey(
|
scope_type = models.ForeignKey(
|
||||||
to='dcim.Site',
|
to=ContentType,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.CASCADE,
|
||||||
related_name='vlan_groups',
|
limit_choices_to=Q(
|
||||||
|
model__in=['region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster']
|
||||||
|
),
|
||||||
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'
|
||||||
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
@ -43,13 +55,13 @@ class VLANGroup(OrganizationalModel):
|
|||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
csv_headers = ['name', 'slug', 'site', 'description']
|
csv_headers = ['name', 'slug', 'scope_type', 'scope_id', 'description']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('site', 'name', 'pk') # (site, name) may be non-unique
|
ordering = ('name', 'pk') # Name may be non-unique
|
||||||
unique_together = [
|
unique_together = [
|
||||||
['site', 'name'],
|
['scope_type', 'scope_id', 'name'],
|
||||||
['site', 'slug'],
|
['scope_type', 'scope_id', 'slug'],
|
||||||
]
|
]
|
||||||
verbose_name = 'VLAN group'
|
verbose_name = 'VLAN group'
|
||||||
verbose_name_plural = 'VLAN groups'
|
verbose_name_plural = 'VLAN groups'
|
||||||
@ -60,11 +72,21 @@ class VLANGroup(OrganizationalModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('ipam:vlangroup_vlans', args=[self.pk])
|
return reverse('ipam:vlangroup_vlans', args=[self.pk])
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Validate scope assignment
|
||||||
|
if self.scope_type and not self.scope_id:
|
||||||
|
raise ValidationError("Cannot set scope_type without scope_id.")
|
||||||
|
if self.scope_id and not self.scope_type:
|
||||||
|
raise ValidationError("Cannot set scope_id without scope_type.")
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
self.name,
|
self.name,
|
||||||
self.slug,
|
self.slug,
|
||||||
self.site.name if self.site else None,
|
f'{self.scope_type.app_label}.{self.scope_type.model}',
|
||||||
|
self.scope_id,
|
||||||
self.description,
|
self.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -159,10 +181,11 @@ class VLAN(PrimaryModel):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate VLAN group
|
# Validate VLAN group (if assigned)
|
||||||
if self.group and self.group.site != self.site:
|
if self.group and self.site and self.group.scope != self.site:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'group': "VLAN group must belong to the assigned site ({}).".format(self.site)
|
'group': f"VLAN is assigned to group {self.group} (scope: {self.group.scope}); cannot also assign to "
|
||||||
|
f"site {self.site}."
|
||||||
})
|
})
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
|
@ -90,7 +90,7 @@ VLAN_ROLE_LINK = """
|
|||||||
VLANGROUP_ADD_VLAN = """
|
VLANGROUP_ADD_VLAN = """
|
||||||
{% with next_vid=record.get_next_available_vid %}
|
{% with next_vid=record.get_next_available_vid %}
|
||||||
{% if next_vid and perms.ipam.add_vlan %}
|
{% if next_vid and perms.ipam.add_vlan %}
|
||||||
<a href="{% url 'ipam:vlan_add' %}?site={{ record.site_id }}&group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
|
<a href="{% url 'ipam:vlan_add' %}?group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
|
||||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -417,7 +417,7 @@ class InterfaceIPAddressTable(BaseTable):
|
|||||||
class VLANGroupTable(BaseTable):
|
class VLANGroupTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.Column(linkify=True)
|
name = tables.Column(linkify=True)
|
||||||
site = tables.Column(
|
scope = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
vlan_count = LinkedCountColumn(
|
vlan_count = LinkedCountColumn(
|
||||||
@ -432,8 +432,8 @@ class VLANGroupTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = VLANGroup
|
model = VLANGroup
|
||||||
fields = ('pk', 'name', 'site', 'vlan_count', 'slug', 'description', 'actions')
|
fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'actions')
|
||||||
default_columns = ('pk', 'name', 'site', 'vlan_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.filters import *
|
from ipam.filters import *
|
||||||
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF
|
||||||
from virtualization.models import Cluster, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
|
||||||
|
|
||||||
@ -715,34 +715,39 @@ class VLANGroupTestCase(TestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
regions = (
|
region = Region(name='Region 1', slug='region-1')
|
||||||
Region(name='Test Region 1', slug='test-region-1'),
|
region.save()
|
||||||
Region(name='Test Region 2', slug='test-region-2'),
|
|
||||||
Region(name='Test Region 3', slug='test-region-3'),
|
|
||||||
)
|
|
||||||
for r in regions:
|
|
||||||
r.save()
|
|
||||||
|
|
||||||
site_groups = (
|
sitegroup = SiteGroup(name='Site Group 1', slug='site-group-1')
|
||||||
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
sitegroup.save()
|
||||||
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
|
||||||
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
|
||||||
)
|
|
||||||
for site_group in site_groups:
|
|
||||||
site_group.save()
|
|
||||||
|
|
||||||
sites = (
|
site = Site(name='Site 1', slug='site-1')
|
||||||
Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
|
site.save()
|
||||||
Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
|
|
||||||
Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]),
|
location = Location(name='Location 1', slug='location-1', site=site)
|
||||||
)
|
location.save()
|
||||||
Site.objects.bulk_create(sites)
|
|
||||||
|
rack = Rack(name='Rack 1', site=site)
|
||||||
|
rack.save()
|
||||||
|
|
||||||
|
clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1')
|
||||||
|
clustertype.save()
|
||||||
|
|
||||||
|
clustergroup = ClusterGroup(name='Cluster Group 1', slug='cluster-group-1')
|
||||||
|
clustergroup.save()
|
||||||
|
|
||||||
|
cluster = Cluster(name='Cluster 1', type=clustertype)
|
||||||
|
cluster.save()
|
||||||
|
|
||||||
vlan_groups = (
|
vlan_groups = (
|
||||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=sites[0], description='A'),
|
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=region, description='A'),
|
||||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=sites[1], description='B'),
|
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sitegroup, description='B'),
|
||||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=sites[2], description='C'),
|
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=site, description='C'),
|
||||||
VLANGroup(name='VLAN Group 4', slug='vlan-group-4', site=None),
|
VLANGroup(name='VLAN Group 4', slug='vlan-group-4', scope=location, description='D'),
|
||||||
|
VLANGroup(name='VLAN Group 5', slug='vlan-group-5', scope=rack, description='E'),
|
||||||
|
VLANGroup(name='VLAN Group 6', slug='vlan-group-6', scope=clustergroup, description='F'),
|
||||||
|
VLANGroup(name='VLAN Group 7', slug='vlan-group-7', scope=cluster, description='G'),
|
||||||
|
VLANGroup(name='VLAN Group 8', slug='vlan-group-8'),
|
||||||
)
|
)
|
||||||
VLANGroup.objects.bulk_create(vlan_groups)
|
VLANGroup.objects.bulk_create(vlan_groups)
|
||||||
|
|
||||||
@ -763,25 +768,32 @@ class VLANGroupTestCase(TestCase):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_region(self):
|
def test_region(self):
|
||||||
regions = Region.objects.all()[:2]
|
params = {'region': Region.objects.first().pk}
|
||||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
params = {'region': [regions[0].slug, regions[1].slug]}
|
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
|
|
||||||
def test_site_group(self):
|
def test_sitegroup(self):
|
||||||
site_groups = SiteGroup.objects.all()[:2]
|
params = {'sitegroup': SiteGroup.objects.first().pk}
|
||||||
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
|
|
||||||
def test_site(self):
|
def test_site(self):
|
||||||
sites = Site.objects.all()[:2]
|
params = {'site': Site.objects.first().pk}
|
||||||
params = {'site_id': [sites[0].pk, sites[1].pk]}
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
def test_location(self):
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
params = {'location': Location.objects.first().pk}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_rack(self):
|
||||||
|
params = {'rack': Rack.objects.first().pk}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_clustergroup(self):
|
||||||
|
params = {'clustergroup': ClusterGroup.objects.first().pk}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_cluster(self):
|
||||||
|
params = {'cluster': Cluster.objects.first().pk}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
class VLANTestCase(TestCase):
|
class VLANTestCase(TestCase):
|
||||||
@ -822,9 +834,9 @@ class VLANTestCase(TestCase):
|
|||||||
Role.objects.bulk_create(roles)
|
Role.objects.bulk_create(roles)
|
||||||
|
|
||||||
groups = (
|
groups = (
|
||||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=sites[0]),
|
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=sites[0]),
|
||||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=sites[1]),
|
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sites[1]),
|
||||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=None),
|
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=None),
|
||||||
)
|
)
|
||||||
VLANGroup.objects.bulk_create(groups)
|
VLANGroup.objects.bulk_create(groups)
|
||||||
|
|
||||||
|
@ -314,18 +314,22 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
sites = (
|
||||||
|
Site(name='Site 1', slug='site-1'),
|
||||||
|
Site(name='Site 2', slug='site-2'),
|
||||||
|
)
|
||||||
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
VLANGroup.objects.bulk_create([
|
VLANGroup.objects.bulk_create([
|
||||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site),
|
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=sites[0]),
|
||||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=site),
|
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sites[0]),
|
||||||
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site),
|
VLANGroup(name='VLAN Group 3', slug='vlan-group-3', scope=sites[0]),
|
||||||
])
|
])
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'VLAN Group X',
|
'name': 'VLAN Group X',
|
||||||
'slug': 'vlan-group-x',
|
'slug': 'vlan-group-x',
|
||||||
'site': site.pk,
|
'site': sites[1].pk,
|
||||||
'description': 'A new VLAN group',
|
'description': 'A new VLAN group',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,8 +358,8 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
Site.objects.bulk_create(sites)
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
vlangroups = (
|
vlangroups = (
|
||||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=sites[0]),
|
VLANGroup(name='VLAN Group 1', slug='vlan-group-1', scope=sites[0]),
|
||||||
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', site=sites[1]),
|
VLANGroup(name='VLAN Group 2', slug='vlan-group-2', scope=sites[1]),
|
||||||
)
|
)
|
||||||
VLANGroup.objects.bulk_create(vlangroups)
|
VLANGroup.objects.bulk_create(vlangroups)
|
||||||
|
|
||||||
|
@ -647,7 +647,7 @@ class IPAddressBulkDeleteView(generic.BulkDeleteView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class VLANGroupListView(generic.ObjectListView):
|
class VLANGroupListView(generic.ObjectListView):
|
||||||
queryset = VLANGroup.objects.prefetch_related('site').annotate(
|
queryset = VLANGroup.objects.annotate(
|
||||||
vlan_count=count_related(VLAN, 'group')
|
vlan_count=count_related(VLAN, 'group')
|
||||||
)
|
)
|
||||||
filterset = filters.VLANGroupFilterSet
|
filterset = filters.VLANGroupFilterSet
|
||||||
@ -658,6 +658,7 @@ class VLANGroupListView(generic.ObjectListView):
|
|||||||
class VLANGroupEditView(generic.ObjectEditView):
|
class VLANGroupEditView(generic.ObjectEditView):
|
||||||
queryset = VLANGroup.objects.all()
|
queryset = VLANGroup.objects.all()
|
||||||
model_form = forms.VLANGroupForm
|
model_form = forms.VLANGroupForm
|
||||||
|
template_name = 'ipam/vlangroup_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupDeleteView(generic.ObjectDeleteView):
|
class VLANGroupDeleteView(generic.ObjectDeleteView):
|
||||||
@ -671,7 +672,7 @@ class VLANGroupBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupBulkEditView(generic.BulkEditView):
|
class VLANGroupBulkEditView(generic.BulkEditView):
|
||||||
queryset = VLANGroup.objects.prefetch_related('site').annotate(
|
queryset = VLANGroup.objects.annotate(
|
||||||
vlan_count=count_related(VLAN, 'group')
|
vlan_count=count_related(VLAN, 'group')
|
||||||
)
|
)
|
||||||
filterset = filters.VLANGroupFilterSet
|
filterset = filters.VLANGroupFilterSet
|
||||||
@ -680,7 +681,7 @@ class VLANGroupBulkEditView(generic.BulkEditView):
|
|||||||
|
|
||||||
|
|
||||||
class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = VLANGroup.objects.prefetch_related('site').annotate(
|
queryset = VLANGroup.objects.annotate(
|
||||||
vlan_count=count_related(VLAN, 'group')
|
vlan_count=count_related(VLAN, 'group')
|
||||||
)
|
)
|
||||||
filterset = filters.VLANGroupFilterSet
|
filterset = filters.VLANGroupFilterSet
|
||||||
@ -793,6 +794,7 @@ class VLANVMInterfacesView(generic.ObjectView):
|
|||||||
class VLANEditView(generic.ObjectEditView):
|
class VLANEditView(generic.ObjectEditView):
|
||||||
queryset = VLAN.objects.all()
|
queryset = VLAN.objects.all()
|
||||||
model_form = forms.VLANForm
|
model_form = forms.VLANForm
|
||||||
|
template_name = 'ipam/vlan_edit.html'
|
||||||
|
|
||||||
|
|
||||||
class VLANDeleteView(generic.ObjectDeleteView):
|
class VLANDeleteView(generic.ObjectDeleteView):
|
||||||
|
57
netbox/templates/ipam/vlan_edit.html
Normal file
57
netbox/templates/ipam/vlan_edit.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{% extends 'generic/object_edit.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>VLAN</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.vid %}
|
||||||
|
{% render_field form.name %}
|
||||||
|
{% render_field form.status %}
|
||||||
|
{% render_field form.role %}
|
||||||
|
{% render_field form.description %}
|
||||||
|
{% render_field form.tags %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Tenancy</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.tenant_group %}
|
||||||
|
{% render_field form.tenant %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Assignment</strong>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% with site_tab_active=form.initial.site %}
|
||||||
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
|
<li role="presentation"{% if not site_tab_active %} class="active"{% endif %}><a href="#group" role="tab" data-toggle="tab">VLAN Group</a></li>
|
||||||
|
<li role="presentation"{% if site_tab_active %} class="active"{% endif %}><a href="#site" role="tab" data-toggle="tab">Site</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane{% if not vm_tab_active %} active{% endif %}" id="group">
|
||||||
|
{% render_field form.scope_type %}
|
||||||
|
{% render_field form.group %}
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane{% if vm_tab_active %} active{% endif %}" id="site">
|
||||||
|
{% render_field form.region %}
|
||||||
|
{% render_field form.sitegroup %}
|
||||||
|
{% render_field form.site %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if form.custom_fields %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_custom_fields form %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
49
netbox/templates/ipam/vlangroup_edit.html
Normal file
49
netbox/templates/ipam/vlangroup_edit.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{% extends 'generic/object_edit.html' %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block form %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>VLAN Group</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_field form.name %}
|
||||||
|
{% render_field form.slug %}
|
||||||
|
{% render_field form.description %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Scope</strong>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% with virtual_tab_active=form.initial.cluster %}
|
||||||
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
|
<li role="presentation"{% if not virtual_tab_active %} class="active"{% endif %}><a href="#physical" role="tab" data-toggle="tab">Physical</a></li>
|
||||||
|
<li role="presentation"{% if virtual_tab_active %} class="active"{% endif %}><a href="#virtual" role="tab" data-toggle="tab">Virtual</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane{% if not virtual_tab_active %} active{% endif %}" id="physical">
|
||||||
|
{% render_field form.region %}
|
||||||
|
{% render_field form.site_group %}
|
||||||
|
{% render_field form.site %}
|
||||||
|
{% render_field form.location %}
|
||||||
|
{% render_field form.rack %}
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane{% if virtual_tab_active %} active{% endif %}" id="virtual">
|
||||||
|
{% render_field form.cluster_group %}
|
||||||
|
{% render_field form.cluster %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="help-block">The VLAN group will be limited in scope to the most-specific object selected above.</span>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if form.custom_fields %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading"><strong>Custom Fields</strong></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% render_custom_fields form %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user