Repository: netbox-community/netbox

Issue:
Add 'L2VPN' column to device/VM interfaces table view
### NetBox version

v3.3-beta2

### Feature type

Change to existing functionality

### Proposed functionality

When viewing a table of interfaces on a device or VM, and configuring the columns to view, be able to select a column for "L2VPN" (analogous to FHRP Group, Parent etc).

### Use case

When browsing a router with many interfaces/subinterfaces and L2VPN terminations, you want to see which L2VPN each interface connects to.

Since you can already do this for many other objects which link to interface, I suspect this was just an oversight.

### Database changes

None

### External dependencies

None


Discussion:
Ref discussion: https://github.com/netbox-community/netbox/discussions/9727#discussioncomment-3409332
> Since you can already do this for many other objects which link to interface, I suspect this was just an oversight.

Something unique about this relationship is that we're following a reverse generic relation that should only ever return a _single_ object. Unfortunately there's no built-in relation type for this in Django, so we might want to experiment a bit with the ideal solution.
Is the problem specifically to do with table views?

The field is already present in the interfaces detail view page (e.g. `/dcim/interfaces/666/`)

![image](https://user-images.githubusercontent.com/44789/185207006-f05c795d-a6bb-4490-a402-8a4902b644bf.png)

```
            <tr>
              <th scope="row">L2VPN</th>
              <td>{{ object.l2vpn_termination.l2vpn|linkify|placeholder }}</td>
            </tr>
```

and it just comes from:

```
    @property
    def l2vpn_termination(self):
        return self.l2vpn_terminations.first()
```

That's certainly a valid approach. I was just thinking we should try to identify something slightly more elegant, like a custom GenericRelation that returns a single object. Granted, that's likely going to involve a lot of work, and probably shouldn't block this FR.
Looks like what you have already is pretty standard:
https://stackoverflow.com/questions/7837330/generic-one-to-one-relation-in-django

I presume you could also enforce it by putting a unique constraint on (object_type, object_id)
I think what Jeremy means is instead to have a custom relation class to make things a little cleaner to try and DRY up our code, otherwise we are repeating the same pattern all over the place but with different names.

Ideally, a GenericOneToOne() would be better but I don't know if that is possible or not.
just adding an additional note from https://github.com/netbox-community/netbox/issues/10194. 
The same is also needed when viewing a vlan list to show that a vlan is part of an l2vpn

Resolving patch:
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -483,6 +483,12 @@ class BaseInterfaceTable(NetBoxTable):
         orderable=False,
         verbose_name='FHRP Groups'
     )
+    l2vpn = tables.Column(
+        accessor=tables.A('l2vpn_termination__l2vpn'),
+        linkify=True,
+        orderable=False,
+        verbose_name='L2VPN'
+    )
     untagged_vlan = tables.Column(linkify=True)
     tagged_vlans = columns.TemplateColumn(
         template_code=INTERFACE_TAGGED_VLANS,
@@ -520,8 +526,8 @@ class Meta(DeviceComponentTable.Meta):
             'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
             'speed', 'duplex', 'mode', 'mac_address', 'wwn', 'poe_mode', 'poe_type', 'rf_role', 'rf_channel',
             'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
-            'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'ip_addresses',
-            'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
+            'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'l2vpn',
+            'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
         )
         default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
 
@@ -554,8 +560,8 @@ class Meta(DeviceComponentTable.Meta):
             'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
             'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
             'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
-            'wireless_lans', 'link_peer', 'connection', 'tags', 'ip_addresses', 'fhrp_groups', 'untagged_vlan',
-            'tagged_vlans', 'actions',
+            'wireless_lans', 'link_peer', 'connection', 'tags', 'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups',
+            'untagged_vlan', 'tagged_vlans', 'actions',
         )
         order_by = ('name',)
         default_columns = (
diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py
--- a/netbox/ipam/tables/vlans.py
+++ b/netbox/ipam/tables/vlans.py
@@ -110,6 +110,12 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable):
     role = tables.Column(
         linkify=True
     )
+    l2vpn = tables.Column(
+        accessor=tables.A('l2vpn_termination__l2vpn'),
+        linkify=True,
+        orderable=False,
+        verbose_name='L2VPN'
+    )
     prefixes = columns.TemplateColumn(
         template_code=VLAN_PREFIXES,
         orderable=False,
@@ -122,8 +128,8 @@ class VLANTable(TenancyColumnsMixin, NetBoxTable):
     class Meta(NetBoxTable.Meta):
         model = VLAN
         fields = (
-            'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role', 'description', 'tags',
-            'created', 'last_updated',
+            'pk', 'id', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'tenant_group', 'status', 'role',
+            'description', 'tags', 'l2vpn', 'created', 'last_updated',
         )
         default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description')
         row_attrs = {
diff --git a/netbox/virtualization/tables/virtualmachines.py b/netbox/virtualization/tables/virtualmachines.py
--- a/netbox/virtualization/tables/virtualmachines.py
+++ b/netbox/virtualization/tables/virtualmachines.py
@@ -108,7 +108,7 @@ class Meta(NetBoxTable.Meta):
         model = VMInterface
         fields = (
             'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
-            'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
+            'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated',
         )
         default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')
 
@@ -129,7 +129,7 @@ class Meta(NetBoxTable.Meta):
         model = VMInterface
         fields = (
             'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags',
-            'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
+            'vrf', 'l2vpn', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'actions',
         )
         default_columns = ('pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses')
         row_attrs = {


Test patch: