Repository: netbox-community/netbox

Issue:
Allow filtering cables by tenant of connected device
### Environment
* Python version:  3.6.6
* NetBox version:  2.5.8

### Proposed Functionality
Currently, a tenant's device might be linked to another tenant or provider device via one ore more cables.  It is not possible to see easily identify all connections across tenants. Therefore Cables need to have a tenant ownership or else there needs to be a view that shows all cables related to a tenant.

### Use Case
As a data center and that uses Netbox we maintain a full list of equipment, customer and own.  Currently it is possible to assign an ID to all services provided to a customer (tenant).  Prior to the introduction of cables in Netbox, a Circuit (type: "Uplink") was used to indicate the physical link providing the customer connectivity.  The tenant of such an ID was the customer.  This allows for a comprehensive view of everything a customer(tenant) has, i.e. all devices, racks, vlans, IPs, physical uplinks, circuits etc.  

Having a cable that is assignable to a tenant or being able to filter all cables connecting to a tenant's device would restore this ability without having to resort to using Circuits (which is incorrect as it creates two cables when there would be one).

### Database Changes

### External Dependencies



Discussion:
There is no need to declare an explicit relationship to a tenant on the cable model, as the endpoints of a cable already have this association. (It also begs the question: who would own a cable connecting tenants A and B?)

It sounds like you want to be able to filter cables based on the tenant assigned to either end. If this is the case, please revise your FR to better capture the use case and remove the proposed relationship (or close this and open a new FR if you prefer).
Updated FR, thanks.

Resolving patch:
diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py
--- a/netbox/dcim/filters.py
+++ b/netbox/dcim/filters.py
@@ -1050,6 +1050,14 @@ class CableFilter(django_filters.FilterSet):
         method='filter_device',
         field_name='device__site__slug'
     )
+    tenant_id = MultiValueNumberFilter(
+        method='filter_device',
+        field_name='device__tenant_id'
+    )
+    tenant = MultiValueNumberFilter(
+        method='filter_device',
+        field_name='device__tenant__slug'
+    )
 
     class Meta:
         model = Cable
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -3119,6 +3119,17 @@ class CableFilterForm(BootstrapMixin, forms.Form):
             }
         )
     )
+    tenant = FilterChoiceField(
+        queryset=Tenant.objects.all(),
+        to_field_name='slug',
+        widget=APISelectMultiple(
+            api_url="/api/tenancy/tenants/",
+            value_field='slug',
+            filter_for={
+                'device_id': 'tenant',
+            }
+        )
+    )
     rack_id = FilterChoiceField(
         queryset=Rack.objects.all(),
         label='Rack',


Test patch:
diff --git a/netbox/dcim/tests/test_filters.py b/netbox/dcim/tests/test_filters.py
--- a/netbox/dcim/tests/test_filters.py
+++ b/netbox/dcim/tests/test_filters.py
@@ -11,6 +11,7 @@
     VirtualChassis,
 )
 from ipam.models import IPAddress
+from tenancy.models import Tenant
 from virtualization.models import Cluster, ClusterType
 
 
@@ -2121,6 +2122,12 @@ def setUpTestData(cls):
         )
         Site.objects.bulk_create(sites)
 
+        tenants = (
+            Tenant(name='Tenant 1', slug='tenant-1'),
+            Tenant(name='Tenant 2', slug='tenant-2'),
+        )
+        Tenant.objects.bulk_create(tenants)
+
         racks = (
             Rack(name='Rack 1', site=sites[0]),
             Rack(name='Rack 2', site=sites[1]),
@@ -2133,9 +2140,9 @@ def setUpTestData(cls):
         device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
 
         devices = (
-            Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1),
-            Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2),
-            Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1),
+            Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1, tenant=tenants[0]),
+            Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2, tenant=tenants[0]),
+            Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1, tenant=tenants[1]),
             Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=2),
             Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=1),
             Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=2),
@@ -2216,6 +2223,13 @@ def test_site(self):
         params = {'site': [site[0].slug, site[1].slug]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
 
+    def test_tenant(self):
+        tenant = Tenant.objects.all()[:2]
+        params = {'tenant_id': [tenant[0].pk, tenant[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+        params = {'tenant': [tenant[0].slug, tenant[1].slug]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+
 
 class PowerPanelTestCase(TestCase):
     queryset = PowerPanel.objects.all()