diff --git a/CHANGELOG.md b/CHANGELOG.md index 877a45a1e..7647a4152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ v2.4.7 (FUTURE) ## Enhancements +* [#2388](https://github.com/digitalocean/netbox/issues/2388) - Enable filtering of devices/VMs by region * [#2427](https://github.com/digitalocean/netbox/issues/2427) - Allow filtering of interfaces by assigned VLAN or VLAN ID * [#2512](https://github.com/digitalocean/netbox/issues/2512) - Add device field to inventory item filter form diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 1870df762..689e88a5d 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import django_filters from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError @@ -456,6 +457,16 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): ) name = NullableCharFieldFilter() asset_tag = NullableCharFieldFilter() + region_id = django_filters.NumberFilter( + method='filter_region', + name='pk', + label='Region (ID)', + ) + region = django_filters.CharFilter( + method='filter_region', + name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', @@ -538,6 +549,16 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): Q(comments__icontains=value) ).distinct() + def filter_region(self, queryset, name, value): + try: + region = Region.objects.get(**{name: value}) + except ObjectDoesNotExist: + return queryset.none() + return queryset.filter( + Q(site__region=region) | + Q(site__region__in=region.get_descendants()) + ) + def _mac_address(self, queryset, name, value): value = value.strip() if not value: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index b24c979c5..46e039211 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1108,6 +1108,11 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm): model = Device q = forms.CharField(required=False, label='Search') + region = FilterTreeNodeMultipleChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + ) site = FilterChoiceField( queryset=Site.objects.annotate(filter_count=Count('devices')), to_field_name='slug', diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 6af4e4a22..99df19aee 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -1,11 +1,12 @@ from __future__ import unicode_literals import django_filters +from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError -from dcim.models import DeviceRole, Interface, Platform, Site +from dcim.models import DeviceRole, Interface, Platform, Region, Site from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant from utilities.filters import NumericInFilter @@ -116,6 +117,16 @@ class VirtualMachineFilter(CustomFieldFilterSet): queryset=Cluster.objects.all(), label='Cluster (ID)', ) + region_id = django_filters.NumberFilter( + method='filter_region', + name='pk', + label='Region (ID)', + ) + region = django_filters.CharFilter( + method='filter_region', + name='slug', + label='Region (slug)', + ) site_id = django_filters.ModelMultipleChoiceFilter( name='cluster__site', queryset=Site.objects.all(), @@ -173,6 +184,16 @@ class VirtualMachineFilter(CustomFieldFilterSet): Q(comments__icontains=value) ) + def filter_region(self, queryset, name, value): + try: + region = Region.objects.get(**{name: value}) + except ObjectDoesNotExist: + return queryset.none() + return queryset.filter( + Q(cluster__site__region=region) | + Q(cluster__site__region__in=region.get_descendants()) + ) + class InterfaceFilter(django_filters.FilterSet): virtual_machine_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 8f973955c..6d11ed78a 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -16,8 +16,8 @@ from tenancy.models import Tenant from utilities.forms import ( AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm, - ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField, SmallTextarea, - add_blank_choice + ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField, + JSONField, SlugField, SmallTextarea, add_blank_choice, ) from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -386,6 +386,11 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')), label='Cluster' ) + region = FilterTreeNodeMultipleChoiceField( + queryset=Region.objects.all(), + to_field_name='slug', + required=False, + ) site = FilterChoiceField( queryset=Site.objects.annotate(filter_count=Count('clusters__virtual_machines')), to_field_name='slug',