Repository: netbox-community/netbox

Issue:
Patching front port to virtual chassis member interface is broken in UI
### NetBox version

v3.4.2

### Python version

3.8

### Steps to Reproduce

1. Create an empty rack (default settings with 42 HUs works).
2. Add a device with 1 front port to rack.
3. Create a virtual chassis with at least two members (pos. 1, 2,...) with at least one interface each, **no master chassis (!)**, add members to rack.
4. Attach a cable from front port to an interface of VC member at pos. 2 or higher.

### Expected Behavior

The cable should be attached between front port and VC member's interface. Only the interfaces of selected VC member are shown.

### Observed Behavior

The interface selection always show all interfaces of all VC members even if selected VC member is no master. This can lead to confusion because you don't expect to see other VC members' interfaces.

We've made the following observations.
- This issue only occurs for VC members, not for single devices in the same rack.
- If VC member's interface X is already patched, interface X is always marked as not available to connect for all other VC members.
- Patching from the VC member's interface view works which suggests that this is indeed a VC related problem.
- When in the cable creation UI and when selecting any VC member, scrolling through the interface selector will always list interfaces of all VC members, even if selected VC member isn't a master chassis (or, as in our case, no VC member is configured as master chassis).

I believe that this is an UI issue. My guess is that in `/dcim/cables/add`, the Interface selector always displays VC member 1's interfaces instead the actual selected VC member.


Discussion:
So what is happening is we are actually getting all interfaces from the VC when querying this method (if you search for chassis specific interfaces, you will see them).  I will need to confirm that this is the intended result, I don't believe so though.
Hi Dan! The behaviour was different in earlier versions. Regarding the intended result: the observed behaviour would be correct for a master chassis where all interfaces of members of the same VC should be listed. For non-master chassis it should only list the interfaces of that particular chassis. (We don't use the master chassis functionality in our scenario.)
Quick P.S.: the problem also exists in v3.4.3.
hi @DanSheps 
Could you please tell me the exact path in the source code where the problem is located? I would like to try to fix it.

Thanks in advance
Seems to be caused by: https://github.com/netbox-community/netbox/pull/10743

Changing the API to always return all VC interfaces when filtering on device id seems like an error, though I get why it was needed for LAG assignment. Not sure what the correct solution is here. Reverting the change would break LAG assignment again I assume.
The change definitely breaks what used to work in the UI. Does it make sense to discuss this with @jeremystretch ? @lp-2 is currently trying to figure out a solution but we probably don't have enough insight into the design of NetBox to tell whether our proposed solution is acceptable (if we find one, that is).
@cs-1 - A couple of things:

> Cable is actually always attached to VC member 1's interface with the same number.

This is not the case for me. When attaching the cable, all interfaces show up but it's still possible to attach them to the right device, it's just impossible to know which device is which if the interfaces are named the same.

> but we probably don't have enough insight into the design of NetBox to tell whether our proposed solution is acceptable (if we find one, that is).

I guess one option is to: 
- Revert the change in https://github.com/netbox-community/netbox/pull/10743
- Add an option to the rest API for interfaces that controls wether vc interfaces are included in situations where the device is not a master.
- Refix the LAG select field issue in https://github.com/netbox-community/netbox/issues/10610 with the new API option.

Wether this makes sense in a modelling sense I can't tell you, I know next to nothing about VC's.
> This is not the case for me. When attaching the cable, all interfaces show up but it's still possible to attach them to the right device, it's just impossible to know which device is which if the interfaces are named the same.

Yes, same here. But that's exactly the problem. In VC scenarios, interface numbers "y" can be the same on each individual VC member. Only the VC chassis position "x" gives your an identification "x/y" of an interface. There's typically no need to add "x" to each chassis member's interface name, it's redundant information and it also makes using "Device Type" templating difficult to use because in these templates, interfaces are numerated as if not part of a VC (y = 1..48).

> * Add an option to the rest API for interfaces that controls wether vc interfaces are included in situations where the device is not a master.
> * Refix the LAG select field issue in [Can't assign interface to LAG from slave device of virtual chassis #10610](https://github.com/netbox-community/netbox/issues/10610) with the new API option.

These two options sound reasonable. I tend to think that refixing the LAG select field issue is the most plausible solution. It's not logical to get all interfaces of all VC members when querying only a single, non-master VC member.
> Yes, same here. But that's exactly the problem. In VC scenarios, interface numbers "y" can be the same on each individual VC member. Only the VC chassis position "x" gives your an identification "x/y" of an interface. There's typically no need to add "x" to each chassis member's interface name, it's redundant information and it also makes using "Device Type" templating difficult to use because in these templates, interfaces are numerated as if not part of a VC (y = 1..48).

I understand the issue, just wanted to clarify that what I quoted from the original issue wasn't correct, as it makes it seem like regardless of which interface you pick it is always connected to the lowest index VC member. If you don't mind, please update the issue text for posterity.

> It's not logical to get all interfaces of all VC members when querying only a single, non-master VC member.

I agree, and I'm sure it was unintended, and thus a bug. Let me know if you want to try to contribute a fix yourself :)
> I understand the issue, just wanted to clarify that what I quoted from the original issue wasn't correct, as it makes it seem like regardless of which interface you pick it is always connected to the lowest index VC member. If you don't mind, please update the issue text for posterity.

Oops! Yes, I'll fix the description. Sorry for the confusion.

> I agree, and I'm sure it was unintended, and thus a bug. Let me know if you want to try to contribute a fix yourself :)

Yes, that's what we try to figure out whether we can help to fix it. @lp-2 is currently looking into the code.
> Seems to be caused by: #10743
> 
> Changing the API to always return all VC interfaces when filtering on device id seems like an error, though I get why it was needed for LAG assignment. Not sure what the correct solution is here. Reverting the change would break LAG assignment again I assume.

I looked at the code in the _/dcim/filtersets.py_ file. In the filter_device_id function, you could add an optional passing parameter that sets the filter **if_master** to **true** for the virtual chassis and **false** for the LAG setting,
but I couldn't find a call to this function, as it seems to be abstracted away by **MultiValueNumberFilter**.


The filterset doesn't really know if it's being used by the cable form or the interface form, as the fields are DynamicModelChoiceFields which just call the API to get the data. As I said the solution is probably to add a toggle to the interface API signifying if LAG members should be included for non-master members and then the new toggle could be added to query_params for the lag field of the InterfaceForm.
We'd like to help solve this but our Django knowledge is still a little limited. Could you be so kind to point us to the InterfaceForm that you speak of? The idea you mention is what we have in mind but we simply don't seem to find the right "entry point" in the code where to implement it. :) Thanks for your patience, we're slowly but surely getting the hang of it. :)
This is the problematic lag field on the InterfaceForm:

https://github.com/netbox-community/netbox/blob/develop/netbox/dcim/forms/model_forms.py#L1353-L1361

You can pass API parameters to the query_params dict. Not sure of the best way to modify the filter/api though.
Thanks for the pointer. We looked at the code and we can say for sure that the patch in https://github.com/netbox-community/netbox/pull/10743 needs to be reverted because of its side-effects. We're currently trying to figure out how to modify the queryset / query_params to get all cross-VC interfaces in case a device is part of a VC. But we're pretty sure that modifying https://github.com/netbox-community/netbox/blob/3150c1f8b3c38f11dc182d22071da3b9bd62b15e/netbox/dcim/filtersets.py#L1452 with `if_master=False` is not the correct way to do this.
> Yes, same here. But that's exactly the problem. In VC scenarios, interface numbers "y" can be the same on each individual VC member. Only the VC chassis position "x" gives your an identification "x/y" of an interface. There's typically no need to add "x" to each chassis member's interface name, it's redundant information and it also makes using "Device Type" templating difficult to use because in these templates, interfaces are numerated as if not part of a VC (y = 1..48).

Not sure what platform you are using, but on Cisco, switch(or fex or whatever)/slot/interface are all critical to proper interface naming and you shouldn't be skipping on them.  Yes, you have to rename them after, but you could easily handle that with a plugin that functions on a trigger or something and a bulk rename is not difficult.  Not sure what other platforms do, TBH, but I imagine they are similar in that it is a integral part of the name.

Now, if you are using VC for it's non-intended purpose (MC-LAG, VPC, etc), then that is, unfortunately not something we can account for.
We don't use VC in a non-intended purpose, see below. What I'm saying is that the patch in https://github.com/netbox-community/netbox/pull/10743 renders the purpose of a master chassis in a VC obsolete because it will turn **every** chassis into a master chassis when using the `filter_device_id()` function. That clearly doesn't make sense and it has side effects for every part of NetBox where `filter_device_id()` is called. In `model_forms.py` for example, it makes no sense: when attaching a cable, NetBox lets you select a specific device so that NetBox's UI only shows you interfaces on that particular device. With the current patch for `filter_device_id()`, **every** interface of a VC is listed instead of only the interface of the selected device (which may be part of a VC). So https://github.com/netbox-community/netbox/pull/10743 breaks the selection of a single device in the UI. This was **not** the behaviour of the UI before the patch was introduced. This has nothing to do with interface naming in VCs. It's only about breaking the UI when adding a cable (and maybe other places where `filter_device_id()` is used which we haven't found yet). Note that @arthanson himself says in in https://github.com/netbox-community/netbox/pull/10743 that he doesn't know if the patch may have side effects.

RE: VCs (though this is independent of what's stated above): we do use the VC function as intended. We chose not to make a chassis a master chassis in NetBox because our configuration management will look at every member of a VC to determine the interface configuration to use. The "x/y" notation of an interface is derived from x = position number of VC member in NetBox / y = interface number (as it is done by most switch vendors; x will change when changing the position of a chassis in a stack which is analogue to changing the VC position of a chassis in NetBox). I don't see how that is a problem. We don't use MC-LAG or VPC in that context.  That way we don't need to do any interface renaming and can simply use Device Types without changes. I can't find NetBox documentation that states that this is an unintended approach. Using a master chassis and the functionality that comes with it is clearly optional in the docs.
@kkthxbye-code sorry for the confusion in https://github.com/netbox-community/netbox/issues/11838
My main concern was that changing the filterset may have unintended effects elsewhere, not only in this cable adding UI issue. I'll patiently wait for the UI issue to be fixed. We tried to fix the cable UI issue ourself but found no other way than to revert the filterset.py patch and to patch the LAG issue in model_forms.py rather than changing the UI for adding cables. However, we're not familiar enough with the code yet to find a good fix. Sorry for the fuss, wasn't my intention.
This bug is leading to unexpected outcomes in the API as well as GUI. Compare results of
https://demo.netbox.dev/dcim/interfaces/?device=mysampledevice2 and
https://demo.netbox.dev/dcim/interfaces/?device_id=110

The proposal from [kkthxbye-code](https://github.com/kkthxbye-code) appears very reasonable. Allow an additional parameter to the query where the caller can specify whether all interfaces of the virtual-chassis or of the member only should be returned. Defaulting to True on the master and False on the rest. Challenge comes when not specified and devices from different VCs are requested but then it can return 400 error for "ambiguous query".
LAG selection or other GUI/API use cases can select what is appropriate for them.
I understand that one day it was decided to go a different path, but to me, all this would have been easier to deal with if the complete listing of interfaces would happen on the VC instead of the master member. 

In our use case, I have to model a device without any physical interfaces (including power) to represent the stack/VC. This non-racked "device" contains only the virtual interfaces (VLAN interfaces, loopbacks, lags, etc) of the stack. Then I add the physical members to the stack and rack them. This way, each member only holds their physical interfaces.  Ideally, the information I put in the device I create first to represent the stack would lie under the VC. 

Putting all interfaces under the master member of VC is like mixing the concept of virtual and physical device. 

Not sure if it would be too much of a breaking change to consider this again... 
@thefreakquency Totally agreed, here is my argument for having interface listings on virtual chassis ...

https://github.com/netbox-community/netbox/discussions/10953#discussioncomment-4935174

@thefreakquency Yes, that's the reason why we completely avoid using the "master chassis" option in NetBox because it's no logical that a physical chassis seems to have more interfaces than it actually has. Not using the master chassis option may create other problems but for us that approach works. @troeger I agree what you write in the comment that you linked to. That being said, I think it would be a good idea to start a discussion on the [Discussions page](https://github.com/netbox-community/netbox/discussions). My hope still is that the patch that causes unwanted side effects (on top of the UI issues described here) will be reverted.
>Totally agreed, here is my argument for having interface listings on virtual chassis ...

Put in a FR for this, this somewhat makes sense but we still need to include master interfaces for lags and other devices in the API queries so this likely won't solve the problem you are seeking to resolve.

> In our use case

I understand you have a specific use case, however from your description you are likely, for lack of a better word, abusing how the virtual chassis and devices are suppose to be used.

> My hope still is that the patch that causes unwanted side effects (on top of the UI issues described here) will be reverted.

This isn't going to happen.  As mentioned, it needs to be properly resolved and not just reverted to the previous bad state.

**Personally, my view is this:**

All members queried should return all members of the stack no matter which member you are quering as this is how it behaves in real life with a shared management and control plane, which is what VirtualChassis is for.

In an instance where there is separate management and control, you should not be using VirtualChassis.
@DanSheps Having all interfaces under each members would be a really good idea, as long we are able to have a management IP on the VC.  Stacked switches or firewalls can have both separate & shared management. 

In an ideal world full of unicorns, the VC should represent the virtual instance of that switch, which has management.  A few more items would be also needed to represent the non-physical characteristics of a stack/cluster, like:
- Status, 
- Role, 
- Platform
- Tags,
- Services

This would replicate real-life scenario where you document & manage both the physical device (Rack, serial number, interfaces and anything tangible), and the virtual stack (Configuration, management, etc...). Each physical device could have it's own primary management IP, as it is sometime possible to manage each member individually, while having a Stack management IP). 
This is not technically a separate management and control per se. They share the control plane as they are aggregated in one logical instance (VC), but have separate physical characteristics. 

As an example, in Juniper world, each physical management IP would likely be on me0 or fxp0 of each device, and the shared management would be on vme0. In Fortinet, you would assign an IP to mgmt port of each device, while having a cluster management IP. 
> Stacked switches or firewalls can have both separate & shared management.

Can you give me an example of this?  I have never met a stacked switch that has a separate MP.  I have also never met a stacked firewall.
If we changed the serializer to provide the device name as part of the interface lookup, would that suffice for most people?
> If we changed the serializer to provide the device name as part of the interface lookup, would that suffice for most people?

Yes, that would be a very useful and a good solution.
After discussion in the maintainers meeting, we are going to shift the direction:

1. Restore the existing functionality
2. Add a new "vc" attribute to return all VC interfaces on all devices if selected.

Resolving patch:
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -1462,17 +1462,15 @@ class InterfaceFilterSet(
     PathEndpointFilterSet,
     CommonInterfaceFilterSet
 ):
-    # Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
-    # members
-    device = MultiValueCharFilter(
-        method='filter_device',
+    virtual_chassis_member = MultiValueCharFilter(
+        method='filter_virtual_chassis_member',
         field_name='name',
-        label=_('Device'),
+        label=_('Virtual Chassis Interfaces for Device')
     )
-    device_id = MultiValueNumberFilter(
-        method='filter_device_id',
+    virtual_chassis_member_id = MultiValueNumberFilter(
+        method='filter_virtual_chassis_member',
         field_name='pk',
-        label=_('Device (ID)'),
+        label=_('Virtual Chassis Interfaces for Device (ID)')
     )
     kind = django_filters.CharFilter(
         method='filter_kind',
@@ -1540,23 +1538,11 @@ class Meta:
             'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'cable_end',
         ]
 
-    def filter_device(self, queryset, name, value):
+    def filter_virtual_chassis_member(self, queryset, name, value):
         try:
-            devices = Device.objects.filter(**{'{}__in'.format(name): value})
             vc_interface_ids = []
-            for device in devices:
-                vc_interface_ids.extend(device.vc_interfaces().values_list('id', flat=True))
-            return queryset.filter(pk__in=vc_interface_ids)
-        except Device.DoesNotExist:
-            return queryset.none()
-
-    def filter_device_id(self, queryset, name, id_list):
-        # Include interfaces belonging to peer virtual chassis members
-        vc_interface_ids = []
-        try:
-            devices = Device.objects.filter(pk__in=id_list)
-            for device in devices:
-                vc_interface_ids += device.vc_interfaces(if_master=False).values_list('id', flat=True)
+            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))
             return queryset.filter(pk__in=vc_interface_ids)
         except Device.DoesNotExist:
             return queryset.none()
diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py
--- a/netbox/dcim/forms/model_forms.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -1110,7 +1110,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
         required=False,
         label=_('Parent interface'),
         query_params={
-            'device_id': '$device',
+            'virtual_chassis_member_id': '$device',
         }
     )
     bridge = DynamicModelChoiceField(
@@ -1118,7 +1118,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
         required=False,
         label=_('Bridged interface'),
         query_params={
-            'device_id': '$device',
+            'virtual_chassis_member_id': '$device',
         }
     )
     lag = DynamicModelChoiceField(
@@ -1126,7 +1126,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
         required=False,
         label=_('LAG interface'),
         query_params={
-            'device_id': '$device',
+            'virtual_chassis_member_id': '$device',
             'type': 'lag',
         }
     )


Test patch:
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -2822,11 +2822,56 @@ def setUpTestData(cls):
         )
         Rack.objects.bulk_create(racks)
 
+        # VirtualChassis assignment for filtering
+        virtual_chassis = VirtualChassis(name='Virtual Chassis')
+        virtual_chassis.save()
+
         devices = (
-            Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
-            Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
-            Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
-            Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]),  # For cable connections
+            Device(
+                name='Device 1A',
+                device_type=device_types[0],
+                role=roles[0],
+                site=sites[0],
+                location=locations[0],
+                rack=racks[0],
+                virtual_chassis=virtual_chassis,
+                vc_position=1,
+                vc_priority=1
+            ),
+            Device(
+                name='Device 1B',
+                device_type=device_types[2],
+                role=roles[2],
+                site=sites[2],
+                location=locations[2],
+                rack=racks[2],
+                virtual_chassis=virtual_chassis,
+                vc_position=2,
+                vc_priority=1
+            ),
+            Device(
+                name='Device 2',
+                device_type=device_types[1],
+                role=roles[1],
+                site=sites[1],
+                location=locations[1],
+                rack=racks[1]
+            ),
+            Device(
+                name='Device 3',
+                device_type=device_types[2],
+                role=roles[2],
+                site=sites[2],
+                location=locations[2],
+                rack=racks[2]
+            ),
+            # For cable connections
+            Device(
+                name=None,
+                device_type=device_types[2],
+                role=roles[2],
+                site=sites[3]
+            ),
         )
         Device.objects.bulk_create(devices)
 
@@ -2834,6 +2879,7 @@ def setUpTestData(cls):
             ModuleBay(device=devices[0], name='Module Bay 1'),
             ModuleBay(device=devices[1], name='Module Bay 2'),
             ModuleBay(device=devices[2], name='Module Bay 3'),
+            ModuleBay(device=devices[3], name='Module Bay 4'),
         )
         ModuleBay.objects.bulk_create(module_bays)
 
@@ -2841,6 +2887,7 @@ def setUpTestData(cls):
             Module(device=devices[0], module_bay=module_bays[0], module_type=module_type),
             Module(device=devices[1], module_bay=module_bays[1], module_type=module_type),
             Module(device=devices[2], module_bay=module_bays[2], module_type=module_type),
+            Module(device=devices[3], module_bay=module_bays[3], module_type=module_type),
         )
         Module.objects.bulk_create(modules)
 
@@ -2853,16 +2900,11 @@ def setUpTestData(cls):
 
         # Virtual Device Context Creation
         vdcs = (
-            VirtualDeviceContext(device=devices[3], name='VDC 1', identifier=1, status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
-            VirtualDeviceContext(device=devices[3], name='VDC 2', identifier=2, status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
+            VirtualDeviceContext(device=devices[4], name='VDC 1', identifier=1, status=VirtualDeviceContextStatusChoices.STATUS_ACTIVE),
+            VirtualDeviceContext(device=devices[4], name='VDC 2', identifier=2, status=VirtualDeviceContextStatusChoices.STATUS_PLANNED),
         )
         VirtualDeviceContext.objects.bulk_create(vdcs)
 
-        # VirtualChassis assignment for filtering
-        virtual_chassis = VirtualChassis.objects.create(master=devices[0])
-        Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=virtual_chassis, vc_position=1, vc_priority=1)
-        Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
-
         interfaces = (
             Interface(
                 device=devices[0],
@@ -2885,6 +2927,13 @@ def setUpTestData(cls):
             Interface(
                 device=devices[1],
                 module=modules[1],
+                name='VC Chassis Interface',
+                type=InterfaceTypeChoices.TYPE_1GE_SFP,
+                enabled=True
+            ),
+            Interface(
+                device=devices[2],
+                module=modules[2],
                 name='Interface 2',
                 label='B',
                 type=InterfaceTypeChoices.TYPE_1GE_GBIC,
@@ -2901,8 +2950,8 @@ def setUpTestData(cls):
                 poe_type=InterfacePoETypeChoices.TYPE_1_8023AF
             ),
             Interface(
-                device=devices[2],
-                module=modules[2],
+                device=devices[3],
+                module=modules[3],
                 name='Interface 3',
                 label='C',
                 type=InterfaceTypeChoices.TYPE_1GE_FIXED,
@@ -2919,7 +2968,7 @@ def setUpTestData(cls):
                 poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
             ),
             Interface(
-                device=devices[3],
+                device=devices[4],
                 name='Interface 4',
                 label='D',
                 type=InterfaceTypeChoices.TYPE_OTHER,
@@ -2932,7 +2981,7 @@ def setUpTestData(cls):
                 poe_type=InterfacePoETypeChoices.TYPE_2_8023AT
             ),
             Interface(
-                device=devices[3],
+                device=devices[4],
                 name='Interface 5',
                 label='E',
                 type=InterfaceTypeChoices.TYPE_OTHER,
@@ -2941,7 +2990,7 @@ def setUpTestData(cls):
                 tx_power=40
             ),
             Interface(
-                device=devices[3],
+                device=devices[4],
                 name='Interface 6',
                 label='F',
                 type=InterfaceTypeChoices.TYPE_OTHER,
@@ -2950,7 +2999,7 @@ def setUpTestData(cls):
                 tx_power=40
             ),
             Interface(
-                device=devices[3],
+                device=devices[4],
                 name='Interface 7',
                 type=InterfaceTypeChoices.TYPE_80211AC,
                 rf_role=WirelessRoleChoices.ROLE_AP,
@@ -2959,7 +3008,7 @@ def setUpTestData(cls):
                 rf_channel_width=22
             ),
             Interface(
-                device=devices[3],
+                device=devices[4],
                 name='Interface 8',
                 type=InterfaceTypeChoices.TYPE_80211AC,
                 rf_role=WirelessRoleChoices.ROLE_STATION,
@@ -2977,8 +3026,8 @@ def setUpTestData(cls):
         interfaces[7].vdcs.set([vdcs[1]])
 
         # Cables
-        Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[3]]).save()
-        Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]]).save()
+        Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[5]]).save()
+        Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[6]]).save()
         # Third pair is not connected
 
     def test_name(self):
@@ -2991,7 +3040,7 @@ def test_label(self):
 
     def test_enabled(self):
         params = {'enabled': 'true'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
         params = {'enabled': 'false'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
@@ -3011,7 +3060,7 @@ def test_mgmt_only(self):
         params = {'mgmt_only': 'true'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         params = {'mgmt_only': 'false'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
 
     def test_poe_mode(self):
         params = {'poe_mode': [InterfacePoEModeChoices.MODE_PD, InterfacePoEModeChoices.MODE_PSE]}
@@ -3116,6 +3165,14 @@ 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(self):
+        # Device 1A & 3 have 1 management interface, Device 1B has 1 interfaces
+        devices = Device.objects.filter(name__in=['Device 1A', 'Device 3'])
+        params = {'virtual_chassis_member_id': [devices[0].pk, devices[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+        params = {'virtual_chassis_member': [devices[0].name, devices[1].name]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+
     def test_module(self):
         modules = Module.objects.all()[:2]
         params = {'module_id': [modules[0].pk, modules[1].pk]}
@@ -3125,23 +3182,23 @@ def test_cabled(self):
         params = {'cabled': True}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         params = {'cabled': False}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
 
     def test_occupied(self):
         params = {'occupied': True}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         params = {'occupied': False}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
 
     def test_connected(self):
         params = {'connected': True}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         params = {'connected': False}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
 
     def test_kind(self):
         params = {'kind': 'physical'}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
         params = {'kind': 'virtual'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)