Browse Source

Merge pull request #8282 from netbox-community/7852-interface-vrf

Closes #7852: Interface VRF assignment
Jeremy Stretch 4 years ago
parent
commit
447a5f01a9

+ 1 - 1
docs/models/dcim/interface.md

@@ -1,6 +1,6 @@
 ## Interfaces
 
-Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management).
+Interfaces in NetBox represent network interfaces used to exchange data with connected devices. On modern networks, these are most commonly Ethernet, but other types are supported as well. Each interface must be assigned a type, and may optionally be assigned a MAC address, MTU, and IEEE 802.1Q mode (tagged or access). Each interface can also be enabled or disabled, and optionally designated as management-only (for out-of-band management). Additionally, each interface may optionally be assigned to a VRF.
 
 !!! note
     Although devices and virtual machines both can have interfaces, a separate model is used for each. Thus, device interfaces have some properties that are not present on virtual machine interfaces and vice versa.

+ 2 - 1
docs/release-notes/version-3.2.md

@@ -61,6 +61,7 @@ Inventory item templates can be arranged hierarchically within a device type, an
 * [#7759](https://github.com/netbox-community/netbox/issues/7759) - Improved the user preferences form
 * [#7784](https://github.com/netbox-community/netbox/issues/7784) - Support cluster type assignment for config contexts
 * [#7846](https://github.com/netbox-community/netbox/issues/7846) - Enable associating inventory items with device components
+* [#7852](https://github.com/netbox-community/netbox/issues/7852) - Enable assigning interfaces to VRFs
 * [#8168](https://github.com/netbox-community/netbox/issues/8168) - Add `min_vid` and `max_vid` fields to VLAN group
 
 ### Other Changes
@@ -88,7 +89,7 @@ Inventory item templates can be arranged hierarchically within a device type, an
 * dcim.FrontPort
     * Added `module` field
 * dcim.Interface
-    * Added `module` field
+    * Added `module` and `vrf` fields
 * dcim.InventoryItem
     * Added `component_type`, `component_id`, and `role` fields
     * Added read-only `component` field

+ 5 - 2
netbox/dcim/api/serializers.py

@@ -6,7 +6,9 @@ from timezone_field.rest_framework import TimeZoneSerializerField
 from dcim.choices import *
 from dcim.constants import *
 from dcim.models import *
-from ipam.api.nested_serializers import NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer
+from ipam.api.nested_serializers import (
+    NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
+)
 from ipam.models import ASN, VLAN
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
 from netbox.api.serializers import (
@@ -728,6 +730,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
         required=False,
         many=True
     )
+    vrf = NestedVRFSerializer(required=False, allow_null=True)
     cable = NestedCableSerializer(read_only=True)
     wireless_link = NestedWirelessLinkSerializer(read_only=True)
     wireless_lans = SerializedPKRelatedField(
@@ -745,7 +748,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
             'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
             'mtu', 'mac_address', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
             'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'mark_connected',
-            'cable', 'wireless_link', 'link_peer', 'link_peer_type', 'wireless_lans', 'connected_endpoint',
+            'cable', 'wireless_link', 'link_peer', 'link_peer_type', 'wireless_lans', 'vrf', 'connected_endpoint',
             'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
             'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
         ]

+ 1 - 1
netbox/dcim/api/views.py

@@ -583,7 +583,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
 class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
     queryset = Interface.objects.prefetch_related(
         'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
-        'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'ip_addresses', 'fhrp_group_assignments', 'tags'
+        'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
     )
     serializer_class = serializers.InterfaceSerializer
     filterset_class = filtersets.InterfaceFilterSet

+ 12 - 1
netbox/dcim/filtersets.py

@@ -3,7 +3,7 @@ from django.contrib.auth.models import User
 
 from extras.filters import TagFilter
 from extras.filtersets import LocalConfigContextFilterSet
-from ipam.models import ASN
+from ipam.models import ASN, VRF
 from netbox.filtersets import (
     BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
 )
@@ -1217,6 +1217,17 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
     rf_channel = django_filters.MultipleChoiceFilter(
         choices=WirelessChannelChoices
     )
+    vrf_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='vrf',
+        queryset=VRF.objects.all(),
+        label='VRF',
+    )
+    vrf = django_filters.ModelMultipleChoiceFilter(
+        field_name='vrf__rd',
+        queryset=VRF.objects.all(),
+        to_field_name='rd',
+        label='VRF (RD)',
+    )
 
     class Meta:
         model = Interface

+ 9 - 3
netbox/dcim/forms/bulk_edit.py

@@ -7,7 +7,7 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.models import *
 from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
-from ipam.models import VLAN, ASN
+from ipam.models import ASN, VLAN, VRF
 from tenancy.models import Tenant
 from utilities.forms import (
     add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
@@ -1061,7 +1061,8 @@ class InterfaceBulkEditForm(
         required=False,
         query_params={
             'type': 'lag',
-        }
+        },
+        label='LAG'
     )
     mgmt_only = forms.NullBooleanField(
         required=False,
@@ -1080,11 +1081,16 @@ class InterfaceBulkEditForm(
         queryset=VLAN.objects.all(),
         required=False
     )
+    vrf = DynamicModelChoiceField(
+        queryset=VRF.objects.all(),
+        required=False,
+        label='VRF'
+    )
 
     class Meta:
         nullable_fields = [
             'label', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel',
-            'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans',
+            'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'vrf',
         ]
 
     def __init__(self, *args, **kwargs):

+ 8 - 1
netbox/dcim/forms/bulk_import.py

@@ -8,6 +8,7 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.models import *
 from extras.forms import CustomFieldModelCSVForm
+from ipam.models import VRF
 from tenancy.models import Tenant
 from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
 from virtualization.models import Cluster
@@ -622,6 +623,12 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
         required=False,
         help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
     )
+    vrf = CSVModelChoiceField(
+        queryset=VRF.objects.all(),
+        required=False,
+        to_field_name='rd',
+        help_text='Assigned VRF'
+    )
     rf_role = CSVChoiceField(
         choices=WirelessRoleChoices,
         required=False,
@@ -632,7 +639,7 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
         model = Interface
         fields = (
             'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address',
-            'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
+            'wwn', 'mtu', 'mgmt_only', 'description', 'mode', 'vrf', 'rf_role', 'rf_channel', 'rf_channel_frequency',
             'rf_channel_width', 'tx_power',
         )
 

+ 8 - 2
netbox/dcim/forms/filtersets.py

@@ -6,7 +6,7 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.models import *
 from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
-from ipam.models import ASN
+from ipam.models import ASN, VRF
 from tenancy.forms import TenancyFilterForm
 from utilities.forms import (
     APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
@@ -920,7 +920,8 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
     model = Interface
     field_groups = [
         ['q', 'tag'],
-        ['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only', 'mac_address', 'wwn'],
+        ['name', 'label', 'kind', 'type', 'enabled', 'mgmt_only'],
+        ['vrf_id', 'mac_address', 'wwn'],
         ['rf_role', 'rf_channel', 'rf_channel_width', 'tx_power'],
         ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
     ]
@@ -980,6 +981,11 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
         min_value=0,
         max_value=127
     )
+    vrf_id = DynamicModelMultipleChoiceField(
+        queryset=VRF.objects.all(),
+        required=False,
+        label='VRF'
+    )
     tag = TagFilterField(model)
 
 

+ 8 - 3
netbox/dcim/forms/models.py

@@ -9,7 +9,7 @@ from dcim.constants import *
 from dcim.models import *
 from extras.forms import CustomFieldModelForm
 from extras.models import Tag
-from ipam.models import IPAddress, VLAN, VLANGroup, ASN
+from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
 from tenancy.forms import TenancyForm
 from utilities.forms import (
     APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
@@ -1261,6 +1261,11 @@ class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
             'available_on_device': '$device',
         }
     )
+    vrf = DynamicModelChoiceField(
+        queryset=VRF.objects.all(),
+        required=False,
+        label='VRF'
+    )
     tags = DynamicModelMultipleChoiceField(
         queryset=Tag.objects.all(),
         required=False
@@ -1271,11 +1276,11 @@ class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
         fields = [
             'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu',
             'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency',
-            'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'tags',
+            'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
         ]
         fieldsets = (
             ('Interface', ('device', 'name', 'type', 'label', 'description', 'tags')),
-            ('Addressing', ('mac_address', 'wwn')),
+            ('Addressing', ('vrf', 'mac_address', 'wwn')),
             ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
             ('Related Interfaces', ('parent', 'bridge', 'lag')),
             ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),

+ 20 - 0
netbox/dcim/migrations/0149_interface_vrf.py

@@ -0,0 +1,20 @@
+# Generated by Django 3.2.11 on 2022-01-07 18:34
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ipam', '0054_vlangroup_min_max_vids'),
+        ('dcim', '0148_inventoryitem_templates'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='interface',
+            name='vrf',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interfaces', to='ipam.vrf'),
+        ),
+    ]

+ 8 - 0
netbox/dcim/models/device_components.py

@@ -616,6 +616,14 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
         blank=True,
         verbose_name='Tagged VLANs'
     )
+    vrf = models.ForeignKey(
+        to='ipam.VRF',
+        on_delete=models.SET_NULL,
+        related_name='interfaces',
+        null=True,
+        blank=True,
+        verbose_name='VRF'
+    )
     ip_addresses = GenericRelation(
         to='ipam.IPAddress',
         content_type_field='assigned_object_type',

+ 4 - 1
netbox/dcim/tables/devices.py

@@ -521,6 +521,9 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
         orderable=False,
         verbose_name='Wireless LANs'
     )
+    vrf = tables.Column(
+        linkify=True
+    )
     tags = TagColumn(
         url_name='dcim:interface_list'
     )
@@ -531,7 +534,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
             'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', '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',
+            'link_peer', 'connection', 'tags', 'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans',
         )
         default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
 

+ 11 - 1
netbox/dcim/tests/test_api.py

@@ -6,7 +6,7 @@ from rest_framework import status
 from dcim.choices import *
 from dcim.constants import *
 from dcim.models import *
-from ipam.models import ASN, RIR, VLAN
+from ipam.models import ASN, RIR, VLAN, VRF
 from utilities.testing import APITestCase, APIViewTestCases, create_test_device
 from virtualization.models import Cluster, ClusterType
 from wireless.models import WirelessLAN
@@ -1424,6 +1424,13 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
         )
         WirelessLAN.objects.bulk_create(wireless_lans)
 
+        vrfs = (
+            VRF(name='VRF 1'),
+            VRF(name='VRF 2'),
+            VRF(name='VRF 3'),
+        )
+        VRF.objects.bulk_create(vrfs)
+
         cls.create_data = [
             {
                 'device': device.pk,
@@ -1431,6 +1438,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
                 'type': '1000base-t',
                 'mode': InterfaceModeChoices.MODE_TAGGED,
                 'tx_power': 10,
+                'vrf': vrfs[0].pk,
                 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
                 'untagged_vlan': vlans[2].pk,
                 'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
@@ -1442,6 +1450,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
                 'mode': InterfaceModeChoices.MODE_TAGGED,
                 'bridge': interfaces[0].pk,
                 'tx_power': 10,
+                'vrf': vrfs[1].pk,
                 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
                 'untagged_vlan': vlans[2].pk,
                 'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
@@ -1453,6 +1462,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
                 'mode': InterfaceModeChoices.MODE_TAGGED,
                 'parent': interfaces[1].pk,
                 'tx_power': 10,
+                'vrf': vrfs[2].pk,
                 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
                 'untagged_vlan': vlans[2].pk,
                 'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],

+ 18 - 4
netbox/dcim/tests/test_filtersets.py

@@ -4,7 +4,7 @@ from django.test import TestCase
 from dcim.choices import *
 from dcim.filtersets import *
 from dcim.models import *
-from ipam.models import ASN, IPAddress, RIR
+from ipam.models import ASN, IPAddress, RIR, VRF
 from tenancy.models import Tenant, TenantGroup
 from utilities.choices import ColorChoices
 from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
@@ -2370,15 +2370,22 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
         )
         Device.objects.bulk_create(devices)
 
+        vrfs = (
+            VRF(name='VRF 1', rd='65000:1'),
+            VRF(name='VRF 2', rd='65000:2'),
+            VRF(name='VRF 3', rd='65000:3'),
+        )
+        VRF.objects.bulk_create(vrfs)
+
         # 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], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First'),
-            Interface(device=devices[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second'),
-            Interface(device=devices[2], name='Interface 3', label='C', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third'),
+            Interface(device=devices[0], name='Interface 1', label='A', type=InterfaceTypeChoices.TYPE_1GE_SFP, enabled=True, mgmt_only=True, mtu=100, mode=InterfaceModeChoices.MODE_ACCESS, mac_address='00-00-00-00-00-01', description='First', vrf=vrfs[0]),
+            Interface(device=devices[1], name='Interface 2', label='B', type=InterfaceTypeChoices.TYPE_1GE_GBIC, enabled=True, mgmt_only=True, mtu=200, mode=InterfaceModeChoices.MODE_TAGGED, mac_address='00-00-00-00-00-02', description='Second', vrf=vrfs[1]),
+            Interface(device=devices[2], name='Interface 3', label='C', type=InterfaceTypeChoices.TYPE_1GE_FIXED, enabled=False, mgmt_only=False, mtu=300, mode=InterfaceModeChoices.MODE_TAGGED_ALL, mac_address='00-00-00-00-00-03', description='Third', vrf=vrfs[2]),
             Interface(device=devices[3], name='Interface 4', label='D', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, tx_power=40),
             Interface(device=devices[3], name='Interface 5', label='E', type=InterfaceTypeChoices.TYPE_OTHER, enabled=True, mgmt_only=True, tx_power=40),
             Interface(device=devices[3], name='Interface 6', label='F', type=InterfaceTypeChoices.TYPE_OTHER, enabled=False, mgmt_only=False, tx_power=40),
@@ -2550,6 +2557,13 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'tx_power': [40]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
 
+    def test_vrf(self):
+        vrfs = VRF.objects.all()[:2]
+        params = {'vrf_id': [vrfs[0].pk, vrfs[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        params = {'vrf': [vrfs[0].rd, vrfs[1].rd]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
 
 class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
     queryset = FrontPort.objects.all()

+ 15 - 5
netbox/dcim/tests/test_views.py

@@ -11,7 +11,7 @@ from netaddr import EUI
 from dcim.choices import *
 from dcim.constants import *
 from dcim.models import *
-from ipam.models import ASN, RIR, VLAN
+from ipam.models import ASN, RIR, VLAN, VRF
 from tenancy.models import Tenant
 from utilities.testing import ViewTestCases, create_tags, create_test_device
 from wireless.models import WirelessLAN
@@ -2105,6 +2105,13 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
         )
         WirelessLAN.objects.bulk_create(wireless_lans)
 
+        vrfs = (
+            VRF(name='VRF 1'),
+            VRF(name='VRF 2'),
+            VRF(name='VRF 3'),
+        )
+        VRF.objects.bulk_create(vrfs)
+
         tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
         cls.form_data = {
@@ -2124,6 +2131,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'untagged_vlan': vlans[0].pk,
             'tagged_vlans': [v.pk for v in vlans[1:4]],
             'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
+            'vrf': vrfs[0].pk,
             'tags': [t.pk for t in tags],
         }
 
@@ -2143,6 +2151,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'untagged_vlan': vlans[0].pk,
             'tagged_vlans': [v.pk for v in vlans[1:4]],
             'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
+            'vrf': vrfs[0].pk,
             'tags': [t.pk for t in tags],
         }
 
@@ -2159,13 +2168,14 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'tx_power': 10,
             'untagged_vlan': vlans[0].pk,
             'tagged_vlans': [v.pk for v in vlans[1:4]],
+            'vrf': vrfs[1].pk,
         }
 
         cls.csv_data = (
-            "device,name,type",
-            "Device 1,Interface 4,1000base-t",
-            "Device 1,Interface 5,1000base-t",
-            "Device 1,Interface 6,1000base-t",
+            f"device,name,type,vrf.pk",
+            f"Device 1,Interface 4,1000base-t,{vrfs[0].pk}",
+            f"Device 1,Interface 5,1000base-t,{vrfs[0].pk}",
+            f"Device 1,Interface 6,1000base-t,{vrfs[0].pk}",
         )
 
     @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])

+ 10 - 0
netbox/templates/dcim/interface.html

@@ -108,6 +108,16 @@
                             <th scope="row">802.1Q Mode</th>
                             <td>{{ object.get_mode_display|placeholder }}</td>
                         </tr>
+                        <tr>
+                            <th scope="row">VRF</th>
+                            <td>
+                                {% if object.vrf %}
+                                    <a href="{{ object.vrf.get_absolute_url }}">{{ object.vrf }}</a>
+                                {% else %}
+                                    <span class="text-muted">None</span>
+                                {% endif %}
+                            </td>
+                        </tr>
                     </table>
                 </div>
             </div>

+ 1 - 0
netbox/templates/dcim/interface_edit.html

@@ -25,6 +25,7 @@
         <div class="row mb-2">
           <h5 class="offset-sm-3">Addressing</h5>
         </div>
+        {% render_field form.vrf %}
         {% render_field form.mac_address %}
         {% render_field form.wwn %}
     </div>