Repository: netbox-community/netbox

Issue:
CSV upload doesn't respect chassis master as accessor for all interfaces in stack
### NetBox version

v3.3.5

### Python version

3.10

### Steps to Reproduce

1. Create a virtual chassis and two stack members
2. Ensure 1st stack member has interfaces 1/0/X
3. Ensure 2nd stack member has interfaces 2/0/X
4. Set 1st stack member to be primary for the virtual chassis
5. Use CSV import for Connections, and ensure one of the terminations goes to the 2/0/X, with the device selected as the 1st (primary) stack member
6. Don't profit

### Expected Behavior

In the UI, all interfaces in a stack are exposed by the primary member. We can manually create a connection from one of the other member's interfaces to a device, while viewing these interfaces as the primary.

### Observed Behavior

Import states that termination doesn't exist, which is sort of true since it "doesn't" exist on that switch.


Discussion:
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. NetBox is governed by a small group of core maintainers which means not all opened issues may receive direct feedback. **Do not** attempt to circumvent this process by "bumping" the issue; doing so will result in its immediate closure and you may be barred from participating in any future discussions. Please see our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).

Resolving patch:
diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py
--- a/netbox/dcim/forms/bulk_import.py
+++ b/netbox/dcim/forms/bulk_import.py
@@ -1078,7 +1078,11 @@ def _clean_side(self, side):
 
         model = content_type.model_class()
         try:
-            termination_object = model.objects.get(device=device, name=name)
+            if device.virtual_chassis and device.virtual_chassis.master == device and \
+                    model.objects.filter(device=device, name=name).count() == 0:
+                termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name)
+            else:
+                termination_object = model.objects.get(device=device, name=name)
             if termination_object.cable is not None:
                 raise forms.ValidationError(f"Side {side.upper()}: {device} {termination_object} is already connected")
         except ObjectDoesNotExist:


Test patch:
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -2907,6 +2907,7 @@ def setUpTestData(cls):
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
         devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
+        vc = VirtualChassis.objects.create(name='Virtual Chassis')
 
         devices = (
             Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole),
@@ -2916,6 +2917,10 @@ def setUpTestData(cls):
         )
         Device.objects.bulk_create(devices)
 
+        vc.members.set((devices[0], devices[1], devices[2]))
+        vc.master = devices[0]
+        vc.save()
+
         interfaces = (
             Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
             Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
@@ -2929,6 +2934,10 @@ def setUpTestData(cls):
             Interface(device=devices[3], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
             Interface(device=devices[3], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
             Interface(device=devices[3], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[1], name='Device 2 Interface', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[2], name='Device 3 Interface', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[3], name='Interface 4', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(device=devices[3], name='Interface 5', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
         )
         Interface.objects.bulk_create(interfaces)
 
@@ -2961,6 +2970,8 @@ def setUpTestData(cls):
             "Device 3,dcim.interface,Interface 1,Device 4,dcim.interface,Interface 1",
             "Device 3,dcim.interface,Interface 2,Device 4,dcim.interface,Interface 2",
             "Device 3,dcim.interface,Interface 3,Device 4,dcim.interface,Interface 3",
+            "Device 1,dcim.interface,Device 2 Interface,Device 4,dcim.interface,Interface 4",
+            "Device 1,dcim.interface,Device 3 Interface,Device 4,dcim.interface,Interface 5",
         )
 
         cls.csv_update_data = (