mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge release v2.4.7 into develop-2.5
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@ -54,12 +54,21 @@ NetBox now supports modeling physical cables for console, power, and interface c
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
v2.4.7 (FUTURE)
|
v2.4.7 (2018-11-06)
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
|
|
||||||
|
* [#2502](https://github.com/digitalocean/netbox/issues/2502) - Allow duplicate VIPs inside a uniqueness-enforced VRF
|
||||||
* [#2514](https://github.com/digitalocean/netbox/issues/2514) - Prevent new connections to already connected interfaces
|
* [#2514](https://github.com/digitalocean/netbox/issues/2514) - Prevent new connections to already connected interfaces
|
||||||
* [#2515](https://github.com/digitalocean/netbox/issues/2515) - Only use django-rq admin tmeplate if webhooks are enabled
|
* [#2515](https://github.com/digitalocean/netbox/issues/2515) - Only use django-rq admin tmeplate if webhooks are enabled
|
||||||
|
* [#2528](https://github.com/digitalocean/netbox/issues/2528) - Enable creating circuit terminations with interface assignment via API
|
||||||
|
* [#2549](https://github.com/digitalocean/netbox/issues/2549) - Changed naming of `peer_device` and `peer_interface` on API /dcim/connected-device/ endpoint to use underscores
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ The first step to documenting your IP space is to define its scope by creating a
|
|||||||
|
|
||||||
* 10.0.0.0/8 (RFC 1918)
|
* 10.0.0.0/8 (RFC 1918)
|
||||||
* 100.64.0.0/10 (RFC 6598)
|
* 100.64.0.0/10 (RFC 6598)
|
||||||
* 172.16.0.0/20 (RFC 1918)
|
* 172.16.0.0/12 (RFC 1918)
|
||||||
* 192.168.0.0/16 (RFC 1918)
|
* 192.168.0.0/16 (RFC 1918)
|
||||||
* One or more /48s within fd00::/8 (IPv6 unique local addressing)
|
* One or more /48s within fd00::/8 (IPv6 unique local addressing)
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from rest_framework import status
|
|||||||
|
|
||||||
from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
|
from circuits.constants import CIRCUIT_STATUS_ACTIVE, TERM_SIDE_A, TERM_SIDE_Z
|
||||||
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
|
||||||
from dcim.models import Site
|
from dcim.models import Device, DeviceRole, DeviceType, Interface, Manufacturer, Site
|
||||||
from extras.constants import GRAPH_TYPE_PROVIDER
|
from extras.constants import GRAPH_TYPE_PROVIDER
|
||||||
from extras.models import Graph
|
from extras.models import Graph
|
||||||
from utilities.testing import APITestCase
|
from utilities.testing import APITestCase
|
||||||
@ -328,21 +328,24 @@ class CircuitTerminationTest(APITestCase):
|
|||||||
|
|
||||||
super(CircuitTerminationTest, self).setUp()
|
super(CircuitTerminationTest, self).setUp()
|
||||||
|
|
||||||
|
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
||||||
|
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
||||||
provider = Provider.objects.create(name='Test Provider', slug='test-provider')
|
provider = Provider.objects.create(name='Test Provider', slug='test-provider')
|
||||||
circuittype = CircuitType.objects.create(name='Test Circuit Type', slug='test-circuit-type')
|
circuittype = CircuitType.objects.create(name='Test Circuit Type', slug='test-circuit-type')
|
||||||
self.circuit1 = Circuit.objects.create(cid='TEST0001', provider=provider, type=circuittype)
|
self.circuit1 = Circuit.objects.create(cid='TEST0001', provider=provider, type=circuittype)
|
||||||
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
|
self.circuit2 = Circuit.objects.create(cid='TEST0002', provider=provider, type=circuittype)
|
||||||
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
|
self.circuit3 = Circuit.objects.create(cid='TEST0003', provider=provider, type=circuittype)
|
||||||
self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
|
|
||||||
self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
|
|
||||||
self.circuittermination1 = CircuitTermination.objects.create(
|
self.circuittermination1 = CircuitTermination.objects.create(
|
||||||
circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
circuit=self.circuit1, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||||
)
|
)
|
||||||
self.circuittermination2 = CircuitTermination.objects.create(
|
self.circuittermination2 = CircuitTermination.objects.create(
|
||||||
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
circuit=self.circuit1, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
|
||||||
)
|
)
|
||||||
self.circuittermination3 = CircuitTermination.objects.create(
|
self.circuittermination3 = CircuitTermination.objects.create(
|
||||||
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
circuit=self.circuit2, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||||
|
)
|
||||||
|
self.circuittermination4 = CircuitTermination.objects.create(
|
||||||
|
circuit=self.circuit2, term_side=TERM_SIDE_Z, site=self.site2, port_speed=1000000
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_circuittermination(self):
|
def test_get_circuittermination(self):
|
||||||
@ -357,14 +360,14 @@ class CircuitTerminationTest(APITestCase):
|
|||||||
url = reverse('circuits-api:circuittermination-list')
|
url = reverse('circuits-api:circuittermination-list')
|
||||||
response = self.client.get(url, **self.header)
|
response = self.client.get(url, **self.header)
|
||||||
|
|
||||||
self.assertEqual(response.data['count'], 3)
|
self.assertEqual(response.data['count'], 4)
|
||||||
|
|
||||||
def test_create_circuittermination(self):
|
def test_create_circuittermination(self):
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'circuit': self.circuit1.pk,
|
'circuit': self.circuit3.pk,
|
||||||
'term_side': TERM_SIDE_Z,
|
'term_side': TERM_SIDE_A,
|
||||||
'site': self.site2.pk,
|
'site': self.site1.pk,
|
||||||
'port_speed': 1000000,
|
'port_speed': 1000000,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,7 +375,7 @@ class CircuitTerminationTest(APITestCase):
|
|||||||
response = self.client.post(url, data, format='json', **self.header)
|
response = self.client.post(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(CircuitTermination.objects.count(), 4)
|
self.assertEqual(CircuitTermination.objects.count(), 5)
|
||||||
circuittermination4 = CircuitTermination.objects.get(pk=response.data['id'])
|
circuittermination4 = CircuitTermination.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(circuittermination4.circuit_id, data['circuit'])
|
self.assertEqual(circuittermination4.circuit_id, data['circuit'])
|
||||||
self.assertEqual(circuittermination4.term_side, data['term_side'])
|
self.assertEqual(circuittermination4.term_side, data['term_side'])
|
||||||
@ -381,20 +384,23 @@ class CircuitTerminationTest(APITestCase):
|
|||||||
|
|
||||||
def test_update_circuittermination(self):
|
def test_update_circuittermination(self):
|
||||||
|
|
||||||
|
circuittermination5 = CircuitTermination.objects.create(
|
||||||
|
circuit=self.circuit3, term_side=TERM_SIDE_A, site=self.site1, port_speed=1000000
|
||||||
|
)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'circuit': self.circuit1.pk,
|
'circuit': self.circuit3.pk,
|
||||||
'term_side': TERM_SIDE_Z,
|
'term_side': TERM_SIDE_Z,
|
||||||
'site': self.site2.pk,
|
'site': self.site2.pk,
|
||||||
'port_speed': 1000000,
|
'port_speed': 1000000,
|
||||||
}
|
}
|
||||||
|
|
||||||
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': self.circuittermination1.pk})
|
url = reverse('circuits-api:circuittermination-detail', kwargs={'pk': circuittermination5.pk})
|
||||||
response = self.client.put(url, data, format='json', **self.header)
|
response = self.client.put(url, data, format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(CircuitTermination.objects.count(), 3)
|
self.assertEqual(CircuitTermination.objects.count(), 5)
|
||||||
circuittermination1 = CircuitTermination.objects.get(pk=response.data['id'])
|
circuittermination1 = CircuitTermination.objects.get(pk=response.data['id'])
|
||||||
self.assertEqual(circuittermination1.circuit_id, data['circuit'])
|
|
||||||
self.assertEqual(circuittermination1.term_side, data['term_side'])
|
self.assertEqual(circuittermination1.term_side, data['term_side'])
|
||||||
self.assertEqual(circuittermination1.site_id, data['site'])
|
self.assertEqual(circuittermination1.site_id, data['site'])
|
||||||
self.assertEqual(circuittermination1.port_speed, data['port_speed'])
|
self.assertEqual(circuittermination1.port_speed, data['port_speed'])
|
||||||
@ -405,4 +411,4 @@ class CircuitTerminationTest(APITestCase):
|
|||||||
response = self.client.delete(url, **self.header)
|
response = self.client.delete(url, **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
||||||
self.assertEqual(CircuitTermination.objects.count(), 2)
|
self.assertEqual(CircuitTermination.objects.count(), 3)
|
||||||
|
@ -524,13 +524,13 @@ class ConnectedDeviceViewSet(ViewSet):
|
|||||||
interface. This is useful in a situation where a device boots with no configuration, but can detect its neighbors
|
interface. This is useful in a situation where a device boots with no configuration, but can detect its neighbors
|
||||||
via a protocol such as LLDP. Two query parameters must be included in the request:
|
via a protocol such as LLDP. Two query parameters must be included in the request:
|
||||||
|
|
||||||
* `peer-device`: The name of the peer device
|
* `peer_device`: The name of the peer device
|
||||||
* `peer-interface`: The name of the peer interface
|
* `peer_interface`: The name of the peer interface
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||||
_device_param = Parameter('peer-device', 'query',
|
_device_param = Parameter('peer_device', 'query',
|
||||||
description='The name of the peer device', required=True, type=openapi.TYPE_STRING)
|
description='The name of the peer device', required=True, type=openapi.TYPE_STRING)
|
||||||
_interface_param = Parameter('peer-interface', 'query',
|
_interface_param = Parameter('peer_interface', 'query',
|
||||||
description='The name of the peer interface', required=True, type=openapi.TYPE_STRING)
|
description='The name of the peer interface', required=True, type=openapi.TYPE_STRING)
|
||||||
|
|
||||||
def get_view_name(self):
|
def get_view_name(self):
|
||||||
@ -541,9 +541,15 @@ class ConnectedDeviceViewSet(ViewSet):
|
|||||||
def list(self, request):
|
def list(self, request):
|
||||||
|
|
||||||
peer_device_name = request.query_params.get(self._device_param.name)
|
peer_device_name = request.query_params.get(self._device_param.name)
|
||||||
|
if not peer_device_name:
|
||||||
|
# TODO: remove this after 2.4 as the switch to using underscores is a breaking change
|
||||||
|
peer_device_name = request.query_params.get('peer-device')
|
||||||
peer_interface_name = request.query_params.get(self._interface_param.name)
|
peer_interface_name = request.query_params.get(self._interface_param.name)
|
||||||
|
if not peer_interface_name:
|
||||||
|
# TODO: remove this after 2.4 as the switch to using underscores is a breaking change
|
||||||
|
peer_interface_name = request.query_params.get('peer-interface')
|
||||||
if not peer_device_name or not peer_interface_name:
|
if not peer_device_name or not peer_interface_name:
|
||||||
raise MissingFilterException(detail='Request must include "peer-device" and "peer-interface" filters.')
|
raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
|
||||||
|
|
||||||
# Determine local interface from peer interface's connection
|
# Determine local interface from peer interface's connection
|
||||||
peer_interface = get_object_or_404(Interface, device__name=peer_device_name, name=peer_interface_name)
|
peer_interface = get_object_or_404(Interface, device__name=peer_device_name, name=peer_interface_name)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from netaddr import EUI
|
from netaddr import EUI
|
||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
@ -539,6 +540,16 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
name = NullableCharFieldFilter()
|
name = NullableCharFieldFilter()
|
||||||
asset_tag = NullableCharFieldFilter()
|
asset_tag = NullableCharFieldFilter()
|
||||||
|
region_id = django_filters.NumberFilter(
|
||||||
|
method='filter_region',
|
||||||
|
field_name='pk',
|
||||||
|
label='Region (ID)',
|
||||||
|
)
|
||||||
|
region = django_filters.CharFilter(
|
||||||
|
method='filter_region',
|
||||||
|
field_name='slug',
|
||||||
|
label='Region (slug)',
|
||||||
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label='Site (ID)',
|
||||||
@ -633,6 +644,16 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
).distinct()
|
).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):
|
def _mac_address(self, queryset, name, value):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
if not value:
|
if not value:
|
||||||
@ -757,6 +778,14 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
tag = django_filters.CharFilter(
|
tag = django_filters.CharFilter(
|
||||||
field_name='tags__slug',
|
field_name='tags__slug',
|
||||||
)
|
)
|
||||||
|
vlan_id = django_filters.CharFilter(
|
||||||
|
method='filter_vlan_id',
|
||||||
|
label='Assigned VLAN'
|
||||||
|
)
|
||||||
|
vlan = django_filters.CharFilter(
|
||||||
|
method='filter_vlan',
|
||||||
|
label='Assigned VID'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
@ -770,6 +799,24 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
except Device.DoesNotExist:
|
except Device.DoesNotExist:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
|
def filter_vlan_id(self, queryset, name, value):
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(untagged_vlan_id=value) |
|
||||||
|
Q(tagged_vlans=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_vlan(self, queryset, name, value):
|
||||||
|
value = value.strip()
|
||||||
|
if not value:
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(
|
||||||
|
Q(untagged_vlan_id__vid=value) |
|
||||||
|
Q(tagged_vlans__vid=value)
|
||||||
|
)
|
||||||
|
|
||||||
def filter_type(self, queryset, name, value):
|
def filter_type(self, queryset, name, value):
|
||||||
value = value.strip().lower()
|
value = value.strip().lower()
|
||||||
return {
|
return {
|
||||||
@ -816,6 +863,15 @@ class InventoryItemFilter(DeviceComponentFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
|
device_id = django_filters.ModelChoiceFilter(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
label='Device (ID)',
|
||||||
|
)
|
||||||
|
device = django_filters.ModelChoiceFilter(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
to_field_name='name',
|
||||||
|
label='Device (name)',
|
||||||
|
)
|
||||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=InventoryItem.objects.all(),
|
queryset=InventoryItem.objects.all(),
|
||||||
label='Parent inventory item (ID)',
|
label='Parent inventory item (ID)',
|
||||||
|
@ -1269,6 +1269,11 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditF
|
|||||||
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||||
model = Device
|
model = Device
|
||||||
q = forms.CharField(required=False, label='Search')
|
q = forms.CharField(required=False, label='Search')
|
||||||
|
region = FilterTreeNodeMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
site = FilterChoiceField(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.annotate(filter_count=Count('devices')),
|
queryset=Site.objects.annotate(filter_count=Count('devices')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
@ -2141,6 +2146,7 @@ class InventoryItemBulkEditForm(BootstrapMixin, BulkEditForm):
|
|||||||
class InventoryItemFilterForm(BootstrapMixin, forms.Form):
|
class InventoryItemFilterForm(BootstrapMixin, forms.Form):
|
||||||
model = InventoryItem
|
model = InventoryItem
|
||||||
q = forms.CharField(required=False, label='Search')
|
q = forms.CharField(required=False, label='Search')
|
||||||
|
device = forms.CharField(required=False, label='Device name')
|
||||||
manufacturer = FilterChoiceField(
|
manufacturer = FilterChoiceField(
|
||||||
queryset=Manufacturer.objects.annotate(filter_count=Count('inventory_items')),
|
queryset=Manufacturer.objects.annotate(filter_count=Count('inventory_items')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
@ -587,11 +587,11 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
|||||||
if self.address:
|
if self.address:
|
||||||
|
|
||||||
# Enforce unique IP space (if applicable)
|
# Enforce unique IP space (if applicable)
|
||||||
if self.role not in IPADDRESS_ROLES_NONUNIQUE and (
|
if self.role not in IPADDRESS_ROLES_NONUNIQUE and ((
|
||||||
self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE
|
self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE
|
||||||
) or (
|
) or (
|
||||||
self.vrf and self.vrf.enforce_unique
|
self.vrf and self.vrf.enforce_unique
|
||||||
):
|
)):
|
||||||
duplicate_ips = self.get_duplicates()
|
duplicate_ips = self.get_duplicates()
|
||||||
if duplicate_ips:
|
if duplicate_ips:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from netaddr import EUI
|
from netaddr import EUI
|
||||||
from netaddr.core import AddrFormatError
|
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 extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import NumericInFilter
|
from utilities.filters import NumericInFilter
|
||||||
@ -120,6 +121,16 @@ class VirtualMachineFilter(CustomFieldFilterSet):
|
|||||||
queryset=Cluster.objects.all(),
|
queryset=Cluster.objects.all(),
|
||||||
label='Cluster (ID)',
|
label='Cluster (ID)',
|
||||||
)
|
)
|
||||||
|
region_id = django_filters.NumberFilter(
|
||||||
|
method='filter_region',
|
||||||
|
field_name='pk',
|
||||||
|
label='Region (ID)',
|
||||||
|
)
|
||||||
|
region = django_filters.CharFilter(
|
||||||
|
method='filter_region',
|
||||||
|
field_name='slug',
|
||||||
|
label='Region (slug)',
|
||||||
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='cluster__site',
|
field_name='cluster__site',
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
@ -177,6 +188,16 @@ class VirtualMachineFilter(CustomFieldFilterSet):
|
|||||||
Q(comments__icontains=value)
|
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):
|
class InterfaceFilter(django_filters.FilterSet):
|
||||||
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
@ -14,8 +14,8 @@ from tenancy.models import Tenant
|
|||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
|
||||||
ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm,
|
ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm,
|
||||||
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField, SmallTextarea,
|
ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField,
|
||||||
add_blank_choice
|
JSONField, SlugField, SmallTextarea, add_blank_choice,
|
||||||
)
|
)
|
||||||
from .constants import VM_STATUS_CHOICES
|
from .constants import VM_STATUS_CHOICES
|
||||||
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||||
@ -384,6 +384,11 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
|||||||
queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')),
|
queryset=Cluster.objects.annotate(filter_count=Count('virtual_machines')),
|
||||||
label='Cluster'
|
label='Cluster'
|
||||||
)
|
)
|
||||||
|
region = FilterTreeNodeMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
site = FilterChoiceField(
|
site = FilterChoiceField(
|
||||||
queryset=Site.objects.annotate(filter_count=Count('clusters__virtual_machines')),
|
queryset=Site.objects.annotate(filter_count=Count('clusters__virtual_machines')),
|
||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
|
Reference in New Issue
Block a user