Skip to content

Commit 1854a6b

Browse files
Fix #11478 - Add vc_interfaces flag to control selection of VC interfaces (#13296)
* Add `vc_interfaces` flag to control interface queryset * Fix test failure * Add new filters instead of using undocumented query params * Cleanup filterset, add test * Rename filter and re-introduce virtual_chassis filtering method (required) * Fix test * Adjust tests to more accurately provide coverage * Add breaking change note * Misc cleanup --------- Co-authored-by: Jeremy Stretch <[email protected]>
1 parent aebf328 commit 1854a6b

File tree

4 files changed

+96
-52
lines changed

4 files changed

+96
-52
lines changed

docs/release-notes/version-3.6.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
* The `device_role` field on the Device model has been renamed to `role`. The `device_role` field has been temporarily retained on the REST API serializer for devices for backward compatibility, but is read-only.
2424
* The `choices` array field has been removed from the CustomField model. Any defined choices are automatically migrated to CustomFieldChoiceSets, accessible via the new `choice_set` field on the CustomField model.
2525
* The `napalm_driver` and `napalm_args` fields (which were deprecated in v3.5) have been removed from the Platform model.
26+
* The `device` and `device_id` filter for interfaces will no longer include interfaces from virtual chassis peers. Two new filters, `virtual_chassis_member` and `virtual_chassis_member_id`, have been introduced to match all interfaces belonging to the specified device's virtual chassis (if any).
2627
* Reports and scripts are now returned within a `results` list when fetched via the REST API, consistent with other models.
2728
* Superusers can no longer retrieve API token keys via the web UI if [`ALLOW_TOKEN_RETRIEVAL`](https://docs.netbox.dev/en/stable/configuration/security/#allow_token_retrieval) is disabled. (The admin view has been removed per [#13044](https://github.com/netbox-community/netbox/issues/13044).)
2829

netbox/dcim/filtersets.py

+9-23
Original file line numberDiff line numberDiff line change
@@ -1462,17 +1462,15 @@ class InterfaceFilterSet(
14621462
PathEndpointFilterSet,
14631463
CommonInterfaceFilterSet
14641464
):
1465-
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
1466-
# members
1467-
device = MultiValueCharFilter(
1468-
method='filter_device',
1465+
virtual_chassis_member = MultiValueCharFilter(
1466+
method='filter_virtual_chassis_member',
14691467
field_name='name',
1470-
label=_('Device'),
1468+
label=_('Virtual Chassis Interfaces for Device')
14711469
)
1472-
device_id = MultiValueNumberFilter(
1473-
method='filter_device_id',
1470+
virtual_chassis_member_id = MultiValueNumberFilter(
1471+
method='filter_virtual_chassis_member',
14741472
field_name='pk',
1475-
label=_('Device (ID)'),
1473+
label=_('Virtual Chassis Interfaces for Device (ID)')
14761474
)
14771475
kind = django_filters.CharFilter(
14781476
method='filter_kind',
@@ -1540,23 +1538,11 @@ class Meta:
15401538
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'cable_end',
15411539
]
15421540

1543-
def filter_device(self, queryset, name, value):
1541+
def filter_virtual_chassis_member(self, queryset, name, value):
15441542
try:
1545-
devices = Device.objects.filter(**{'{}__in'.format(name): value})
15461543
vc_interface_ids = []
1547-
for device in devices:
1548-
vc_interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
1549-
return queryset.filter(pk__in=vc_interface_ids)
1550-
except Device.DoesNotExist:
1551-
return queryset.none()
1552-
1553-
def filter_device_id(self, queryset, name, id_list):
1554-
# Include interfaces belonging to peer virtual chassis members
1555-
vc_interface_ids = []
1556-
try:
1557-
devices = Device.objects.filter(pk__in=id_list)
1558-
for device in devices:
1559-
vc_interface_ids += device.vc_interfaces(if_master=False).values_list('id', flat=True)
1544+
for device in Device.objects.filter(**{f'{name}__in': value}):
1545+
vc_interface_ids.extend(device.vc_interfaces(if_master=False).values_list('id', flat=True))
15601546
return queryset.filter(pk__in=vc_interface_ids)
15611547
except Device.DoesNotExist:
15621548
return queryset.none()

netbox/dcim/forms/model_forms.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1111,23 +1111,23 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
11111111
required=False,
11121112
label=_('Parent interface'),
11131113
query_params={
1114-
'device_id': '$device',
1114+
'virtual_chassis_member_id': '$device',
11151115
}
11161116
)
11171117
bridge = DynamicModelChoiceField(
11181118
queryset=Interface.objects.all(),
11191119
required=False,
11201120
label=_('Bridged interface'),
11211121
query_params={
1122-
'device_id': '$device',
1122+
'virtual_chassis_member_id': '$device',
11231123
}
11241124
)
11251125
lag = DynamicModelChoiceField(
11261126
queryset=Interface.objects.all(),
11271127
required=False,
11281128
label=_('LAG interface'),
11291129
query_params={
1130-
'device_id': '$device',
1130+
'virtual_chassis_member_id': '$device',
11311131
'type': 'lag',
11321132
}
11331133
)

netbox/dcim/tests/test_filtersets.py

+83-26
Original file line numberDiff line numberDiff line change
@@ -2822,25 +2822,72 @@ def setUpTestData(cls):
28222822
)
28232823
Rack.objects.bulk_create(racks)
28242824

2825+
# VirtualChassis assignment for filtering
2826+
virtual_chassis = VirtualChassis(name='Virtual Chassis')
2827+
virtual_chassis.save()
2828+
28252829
devices = (
2826-
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
2827-
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
2828-
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
2829-
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
2830+
Device(
2831+
name='Device 1A',
2832+
device_type=device_types[0],
2833+
role=roles[0],
2834+
site=sites[0],
2835+
location=locations[0],
2836+
rack=racks[0],
2837+
virtual_chassis=virtual_chassis,
2838+
vc_position=1,
2839+
vc_priority=1
2840+
),
2841+
Device(
2842+
name='Device 1B',
2843+
device_type=device_types[2],
2844+
role=roles[2],
2845+
site=sites[2],
2846+
location=locations[2],
2847+
rack=racks[2],
2848+
virtual_chassis=virtual_chassis,
2849+
vc_position=2,
2850+
vc_priority=1
2851+
),
2852+
Device(
2853+
name='Device 2',
2854+
device_type=device_types[1],
2855+
role=roles[1],
2856+
site=sites[1],
2857+
location=locations[1],
2858+
rack=racks[1]
2859+
),
2860+
Device(
2861+
name='Device 3',
2862+
device_type=device_types[2],
2863+
role=roles[2],
2864+
site=sites[2],
2865+
location=locations[2],
2866+
rack=racks[2]
2867+
),
2868+
# For cable connections
2869+
Device(
2870+
name=None,
2871+
device_type=device_types[2],
2872+
role=roles[2],
2873+
site=sites[3]
2874+
),
28302875
)
28312876
Device.objects.bulk_create(devices)
28322877

28332878
module_bays = (
28342879
ModuleBay(device=devices[0], name='Module Bay 1'),
28352880
ModuleBay(device=devices[1], name='Module Bay 2'),
28362881
ModuleBay(device=devices[2], name='Module Bay 3'),
2882+
ModuleBay(device=devices[3], name='Module Bay 4'),
28372883
)
28382884
ModuleBay.objects.bulk_create(module_bays)
28392885

28402886
modules = (
28412887
Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
28422888
Module(device=devices[1], module_bay=module_bays[1], module_type=module_type),
28432889
Module(device=devices[2], module_bay=module_bays[2], module_type=module_type),
2890+
Module(device=devices[3], module_bay=module_bays[3], module_type=module_type),
28442891
)
28452892
Module.objects.bulk_create(modules)
28462893

@@ -2853,16 +2900,11 @@ def setUpTestData(cls):
28532900

28542901
# Virtual Device Context Creation
28552902
vdcs = (
2856-
VirtualDeviceContext(device=devices[3], name='VDC 1', identifier=1, status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
2857-
VirtualDeviceContext(device=devices[3], name='VDC 2', identifier=2, status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
2903+
VirtualDeviceContext(device=devices[4], name='VDC 1', identifier=1, status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
2904+
VirtualDeviceContext(device=devices[4], name='VDC 2', identifier=2, status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
28582905
)
28592906
VirtualDeviceContext.objects.bulk_create(vdcs)
28602907

2861-
# VirtualChassis assignment for filtering
2862-
virtual_chassis = VirtualChassis.objects.create(master=devices[0])
2863-
Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1)
2864-
Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
2865-
28662908
interfaces = (
28672909
Interface(
28682910
device=devices[0],
@@ -2885,6 +2927,13 @@ def setUpTestData(cls):
28852927
Interface(
28862928
device=devices[1],
28872929
module=modules[1],
2930+
name='VC Chassis Interface',
2931+
type=InterfaceTypeChoices.TYPE_1GE_SFP,
2932+
enabled=True
2933+
),
2934+
Interface(
2935+
device=devices[2],
2936+
module=modules[2],
28882937
name='Interface 2',
28892938
label='B',
28902939
type=InterfaceTypeChoices.TYPE_1GE_GBIC,
@@ -2901,8 +2950,8 @@ def setUpTestData(cls):
29012950
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
29022951
),
29032952
Interface(
2904-
device=devices[2],
2905-
module=modules[2],
2953+
device=devices[3],
2954+
module=modules[3],
29062955
name='Interface 3',
29072956
label='C',
29082957
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
@@ -2919,7 +2968,7 @@ def setUpTestData(cls):
29192968
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
29202969
),
29212970
Interface(
2922-
device=devices[3],
2971+
device=devices[4],
29232972
name='Interface 4',
29242973
label='D',
29252974
type=InterfaceTypeChoices.TYPE_OTHER,
@@ -2932,7 +2981,7 @@ def setUpTestData(cls):
29322981
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
29332982
),
29342983
Interface(
2935-
device=devices[3],
2984+
device=devices[4],
29362985
name='Interface 5',
29372986
label='E',
29382987
type=InterfaceTypeChoices.TYPE_OTHER,
@@ -2941,7 +2990,7 @@ def setUpTestData(cls):
29412990
tx_power=40
29422991
),
29432992
Interface(
2944-
device=devices[3],
2993+
device=devices[4],
29452994
name='Interface 6',
29462995
label='F',
29472996
type=InterfaceTypeChoices.TYPE_OTHER,
@@ -2950,7 +2999,7 @@ def setUpTestData(cls):
29502999
tx_power=40
29513000
),
29523001
Interface(
2953-
device=devices[3],
3002+
device=devices[4],
29543003
name='Interface 7',
29553004
type=InterfaceTypeChoices.TYPE_80211AC,
29563005
rf_role=WirelessRoleChoices.ROLE_AP,
@@ -2959,7 +3008,7 @@ def setUpTestData(cls):
29593008
rf_channel_width=22
29603009
),
29613010
Interface(
2962-
device=devices[3],
3011+
device=devices[4],
29633012
name='Interface 8',
29643013
type=InterfaceTypeChoices.TYPE_80211AC,
29653014
rf_role=WirelessRoleChoices.ROLE_STATION,
@@ -2977,8 +3026,8 @@ def setUpTestData(cls):
29773026
interfaces[7].vdcs.set([vdcs[1]])
29783027

29793028
# Cables
2980-
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[3]]).save()
2981-
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]]).save()
3029+
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[5]]).save()
3030+
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[6]]).save()
29823031
# Third pair is not connected
29833032

29843033
def test_name(self):
@@ -2991,7 +3040,7 @@ def test_label(self):
29913040

29923041
def test_enabled(self):
29933042
params = {'enabled': 'true'}
2994-
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
3043+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
29953044
params = {'enabled': 'false'}
29963045
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
29973046

@@ -3011,7 +3060,7 @@ def test_mgmt_only(self):
30113060
params = {'mgmt_only': 'true'}
30123061
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
30133062
params = {'mgmt_only': 'false'}
3014-
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
3063+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
30153064

30163065
def test_poe_mode(self):
30173066
params = {'poe_mode': [InterfacePoEModeChoices.MODE_PD, InterfacePoEModeChoices.MODE_PSE]}
@@ -3116,6 +3165,14 @@ def test_device(self):
31163165
params = {'device': [devices[0].name, devices[1].name]}
31173166
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
31183167

3168+
def test_virtual_chassis_member(self):
3169+
# Device 1A & 3 have 1 management interface, Device 1B has 1 interfaces
3170+
devices = Device.objects.filter(name__in=['Device 1A', 'Device 3'])
3171+
params = {'virtual_chassis_member_id': [devices[0].pk, devices[1].pk]}
3172+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
3173+
params = {'virtual_chassis_member': [devices[0].name, devices[1].name]}
3174+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
3175+
31193176
def test_module(self):
31203177
modules = Module.objects.all()[:2]
31213178
params = {'module_id': [modules[0].pk, modules[1].pk]}
@@ -3125,23 +3182,23 @@ def test_cabled(self):
31253182
params = {'cabled': True}
31263183
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
31273184
params = {'cabled': False}
3128-
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
3185+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
31293186

31303187
def test_occupied(self):
31313188
params = {'occupied': True}
31323189
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
31333190
params = {'occupied': False}
3134-
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
3191+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
31353192

31363193
def test_connected(self):
31373194
params = {'connected': True}
31383195
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
31393196
params = {'connected': False}
3140-
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
3197+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
31413198

31423199
def test_kind(self):
31433200
params = {'kind': 'physical'}
3144-
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
3201+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
31453202
params = {'kind': 'virtual'}
31463203
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
31473204

0 commit comments

Comments
 (0)