From b91e5763e20cbb5bdc4801fad648f46ca5ee2439 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 30 Mar 2021 21:32:48 -0400 Subject: [PATCH] Add 'available_on' VLAN filters for devices & VMs --- netbox/dcim/forms.py | 58 +++++++---------------- netbox/ipam/filters.py | 14 ++++++ netbox/ipam/models/vlans.py | 3 +- netbox/ipam/querysets.py | 84 ++++++++++++++++++++++++++++++++++ netbox/virtualization/forms.py | 75 ++++++++---------------------- 5 files changed, 134 insertions(+), 100 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index dca1b86c3..667605b5c 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -3077,20 +3077,12 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, - label='Untagged VLAN', - brief_mode=False, - query_params={ - 'site_id': 'null', - } + label='Untagged VLAN' ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, - label='Tagged VLANs', - brief_mode=False, - query_params={ - 'site_id': 'null', - } + label='Tagged VLANs' ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -3124,9 +3116,9 @@ class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): self.fields['parent'].widget.add_query_param('device_id', device.pk) self.fields['lag'].widget.add_query_param('device_id', device.pk) - # Add current site to VLANs query params - self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk) - self.fields['tagged_vlans'].widget.add_query_param('site_id', device.site.pk) + # Limit VLAN choices by device + self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk) + self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk) class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): @@ -3177,19 +3169,11 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): ) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), - required=False, - brief_mode=False, - query_params={ - 'site_id': 'null', - } + required=False ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), - required=False, - brief_mode=False, - query_params={ - 'site_id': 'null', - } + required=False ) field_order = ( 'device', 'name_pattern', 'label_pattern', 'type', 'enabled', 'parent', 'lag', 'mtu', 'mac_address', @@ -3199,12 +3183,10 @@ class InterfaceCreateForm(ComponentCreateForm, InterfaceCommonForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Add current site to VLANs query params - device = Device.objects.get( - pk=self.initial.get('device') or self.data.get('device') - ) - self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk) - self.fields['tagged_vlans'].widget.add_query_param('site_id', device.site.pk) + # Limit VLAN choices by device + device_id = self.initial.get('device') or self.data.get('device') + self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device_id) + self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device_id) class InterfaceBulkCreateForm( @@ -3264,19 +3246,11 @@ class InterfaceBulkEditForm( ) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), - required=False, - brief_mode=False, - query_params={ - 'site_id': 'null', - } + required=False ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), - required=False, - brief_mode=False, - query_params={ - 'site_id': 'null', - } + required=False ) class Meta: @@ -3293,9 +3267,9 @@ class InterfaceBulkEditForm( self.fields['parent'].widget.add_query_param('device_id', device.pk) self.fields['lag'].widget.add_query_param('device_id', device.pk) - # Add current site to VLANs query params - self.fields['untagged_vlan'].widget.add_query_param('site_id', device.site.pk) - self.fields['tagged_vlans'].widget.add_query_param('site_id', device.site.pk) + # Limit VLAN choices by device + self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk) + self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk) else: # See #4523 diff --git a/netbox/ipam/filters.py b/netbox/ipam/filters.py index 5c5b9e8d3..141d50139 100644 --- a/netbox/ipam/filters.py +++ b/netbox/ipam/filters.py @@ -635,6 +635,14 @@ class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, choices=VLANStatusChoices, null_value=None ) + available_on_device = django_filters.ModelChoiceFilter( + queryset=Device.objects.all(), + method='get_for_device' + ) + available_on_virtualmachine = django_filters.ModelChoiceFilter( + queryset=VirtualMachine.objects.all(), + method='get_for_virtualmachine' + ) tag = TagFilter() class Meta: @@ -651,6 +659,12 @@ class VLANFilterSet(BaseFilterSet, TenancyFilterSet, CustomFieldModelFilterSet, pass return queryset.filter(qs_filter) + def get_for_device(self, queryset, name, value): + return queryset.get_for_device(value) + + def get_for_virtualmachine(self, queryset, name, value): + return queryset.get_for_virtualmachine(value) + class ServiceFilterSet(BaseFilterSet, CreatedUpdatedFilterSet): q = django_filters.CharFilter( diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index 0c184adff..040d23746 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -9,6 +9,7 @@ from dcim.models import Interface from extras.utils import extras_features from ipam.choices import * from ipam.constants import * +from ipam.querysets import VLANQuerySet from netbox.models import OrganizationalModel, PrimaryModel from utilities.querysets import RestrictedQuerySet from virtualization.models import VMInterface @@ -156,7 +157,7 @@ class VLAN(PrimaryModel): blank=True ) - objects = RestrictedQuerySet.as_manager() + objects = VLANQuerySet.as_manager() csv_headers = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description'] clone_fields = [ diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index 102954f13..a205dca32 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -1,3 +1,6 @@ +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q + from utilities.querysets import RestrictedQuerySet @@ -20,3 +23,84 @@ class PrefixQuerySet(RestrictedQuerySet): 'AND COALESCE(U1."vrf_id", 0) = COALESCE("ipam_prefix"."vrf_id", 0))', } ) + + +class VLANQuerySet(RestrictedQuerySet): + + def get_for_device(self, device): + """ + Return all VLANs available to the specified Device. + """ + from .models import VLANGroup + + # Find all relevant VLANGroups + q = Q() + if device.site.region: + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'), + scope_id__in=device.site.region.get_ancestors(include_self=True) + ) + if device.site.group: + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'), + scope_id__in=device.site.group.get_ancestors(include_self=True) + ) + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'), + scope_id=device.site_id + ) + if device.location: + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'location'), + scope_id__in=device.location.get_ancestors(include_self=True) + ) + if device.rack: + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'rack'), + scope_id=device.rack_id + ) + + return self.filter( + Q(group__in=VLANGroup.objects.filter(q)) | + Q(site=device.site) | + Q(group__isnull=True, site__isnull=True) # Global VLANs + ) + + def get_for_virtualmachine(self, vm): + """ + Return all VLANs available to the specified VirtualMachine. + """ + from .models import VLANGroup + + # Find all relevant VLANGroups + q = Q() + if vm.cluster.site: + if vm.cluster.site.region: + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'), + scope_id__in=vm.cluster.site.region.get_ancestors(include_self=True) + ) + if vm.cluster.site.group: + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'), + scope_id__in=vm.cluster.site.group.get_ancestors(include_self=True) + ) + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'), + scope_id=vm.cluster.site_id + ) + if vm.cluster.group: + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'), + scope_id=vm.cluster.group_id + ) + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'), + scope_id=vm.cluster_id + ) + + return self.filter( + Q(group__in=VLANGroup.objects.filter(q)) | + Q(site=vm.cluster.site) | + Q(group__isnull=True, site__isnull=True) # Global VLANs + ) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 95e1d2dc1..4764e9a8e 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -606,20 +606,12 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, - label='Untagged VLAN', - brief_mode=False, - query_params={ - 'site_id': 'null', - } + label='Untagged VLAN' ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), required=False, - label='Tagged VLANs', - brief_mode=False, - query_params={ - 'site_id': 'null', - } + label='Tagged VLANs' ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -646,15 +638,10 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - virtual_machine = VirtualMachine.objects.get( - pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine') - ) - - # Add current site to VLANs query params - site = virtual_machine.site - if site: - self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk) - self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk) + # Limit VLAN choices by virtual machine + vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') + self.fields['untagged_vlan'].widget.add_query_param('available_on_vm', vm_id) + self.fields['tagged_vlans'].widget.add_query_param('available_on_vm', vm_id) class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm): @@ -689,19 +676,11 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm): ) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), - required=False, - brief_mode=False, - query_params={ - 'site_id': 'null', - } + required=False ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), - required=False, - brief_mode=False, - query_params={ - 'site_id': 'null', - } + required=False ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -711,15 +690,10 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - virtual_machine = VirtualMachine.objects.get( - pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine') - ) - - # Add current site to VLANs query params - site = virtual_machine.site - if site: - self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk) - self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk) + # Limit VLAN choices by virtual machine + vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') + self.fields['untagged_vlan'].widget.add_query_param('available_on_vm', vm_id) + self.fields['tagged_vlans'].widget.add_query_param('available_on_vm', vm_id) class VMInterfaceCSVForm(CSVModelForm): @@ -777,19 +751,11 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): ) untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), - required=False, - brief_mode=False, - query_params={ - 'site_id': 'null', - } + required=False ) tagged_vlans = DynamicModelMultipleChoiceField( queryset=VLAN.objects.all(), - required=False, - brief_mode=False, - query_params={ - 'site_id': 'null', - } + required=False ) class Meta: @@ -800,15 +766,10 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Limit available VLANs based on the parent VirtualMachine - if 'virtual_machine' in self.initial: - parent_obj = VirtualMachine.objects.filter(pk=self.initial['virtual_machine']).first() - - site = getattr(parent_obj.cluster, 'site', None) - if site is not None: - # Add current site to VLANs query params - self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk) - self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk) + # Limit VLAN choices by virtual machine + vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine') + self.fields['untagged_vlan'].widget.add_query_param('available_on_vm', vm_id) + self.fields['tagged_vlans'].widget.add_query_param('available_on_vm', vm_id) class VMInterfaceBulkRenameForm(BulkRenameForm):