Skip to content

Fixes: #19645 - Correct Interface selection for Cable add when VC master is the selected device #20041

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions netbox/dcim/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1885,6 +1885,16 @@ class InterfaceFilterSet(
PathEndpointFilterSet,
CommonInterfaceFilterSet
):
virtual_chassis_member_or_master = MultiValueCharFilter(
method='filter_virtual_chassis_member_or_master',
field_name='name',
label=_('Virtual Chassis Interfaces for Device when device is master')
)
virtual_chassis_member_or_master_id = MultiValueNumberFilter(
method='filter_virtual_chassis_member_or_master',
field_name='pk',
label=_('Virtual Chassis Interfaces for Device when device is master (ID)')
)
virtual_chassis_member = MultiValueCharFilter(
method='filter_virtual_chassis_member',
field_name='name',
Expand Down Expand Up @@ -1995,11 +2005,14 @@ class Meta:
'cable_id', 'cable_end',
)

def filter_virtual_chassis_member(self, queryset, name, value):
def filter_virtual_chassis_member_or_master(self, queryset, name, value):
return self.filter_virtual_chassis_member(queryset, name, value, if_master=True)

def filter_virtual_chassis_member(self, queryset, name, value, if_master=False):
try:
vc_interface_ids = []
for device in Device.objects.filter(**{f'{name}__in': value}):
vc_interface_ids.extend(device.vc_interfaces(if_master=False).values_list('id', flat=True))
vc_interface_ids.extend(device.vc_interfaces(if_master=if_master).values_list('id', flat=True))
return queryset.filter(pk__in=vc_interface_ids)
except Device.DoesNotExist:
return queryset.none()
Expand Down
7 changes: 6 additions & 1 deletion netbox/dcim/forms/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ def __new__(mcs, name, bases, attrs):
# Device component
if hasattr(term_cls, 'device'):

# Dynamically change the param field for interfaces to use virtual_chassis filter
query_param_device_field = 'device_id'
if term_cls == Interface:
query_param_device_field = 'virtual_chassis_member_or_master_id'

attrs[f'termination_{cable_end}_device'] = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
label=_('Device'),
Expand All @@ -36,7 +41,7 @@ def __new__(mcs, name, bases, attrs):
'parent': 'device',
},
query_params={
'device_id': f'$termination_{cable_end}_device',
query_param_device_field: f'$termination_{cable_end}_device',
'kind': 'physical', # Exclude virtual interfaces
}
)
Expand Down
16 changes: 16 additions & 0 deletions netbox/dcim/tests/test_filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4373,6 +4373,9 @@ def setUpTestData(cls):
)
Device.objects.bulk_create(devices)

virtual_chassis.master = devices[0]
virtual_chassis.save()

module_bays = (
ModuleBay(device=devices[0], name='Module Bay 1'),
ModuleBay(device=devices[1], name='Module Bay 2'),
Expand Down Expand Up @@ -4759,6 +4762,19 @@ def test_device(self):
params = {'device': [devices[0].name, devices[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

def test_virtual_chassis_member_or_master(self):
vc = VirtualChassis.objects.first()
master = vc.master
member = vc.members.exclude(pk=master.pk).first()
params = {'virtual_chassis_member_or_master_id': [master.pk,]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'virtual_chassis_member_or_master_id': [member.pk,]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'virtual_chassis_member_or_master': [master.name,]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'virtual_chassis_member_or_master': [member.name,]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)

def test_virtual_chassis_member(self):
# Device 1A & 3 have 1 management interface, Device 1B has 1 interfaces
devices = Device.objects.filter(name__in=['Device 1A', 'Device 3'])
Expand Down