ソースを参照

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

Closes #7852: Interface VRF assignment
Jeremy Stretch 4 年 前
コミット
447a5f01a9

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

@@ -1,6 +1,6 @@
 ## Interfaces
 ## 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
 !!! 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.
     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
 * [#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
 * [#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
 * [#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
 * [#8168](https://github.com/netbox-community/netbox/issues/8168) - Add `min_vid` and `max_vid` fields to VLAN group
 
 
 ### Other Changes
 ### Other Changes
@@ -88,7 +89,7 @@ Inventory item templates can be arranged hierarchically within a device type, an
 * dcim.FrontPort
 * dcim.FrontPort
     * Added `module` field
     * Added `module` field
 * dcim.Interface
 * dcim.Interface
-    * Added `module` field
+    * Added `module` and `vrf` fields
 * dcim.InventoryItem
 * dcim.InventoryItem
     * Added `component_type`, `component_id`, and `role` fields
     * Added `component_type`, `component_id`, and `role` fields
     * Added read-only `component` field
     * 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.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models 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 ipam.models import ASN, VLAN
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
 from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
 from netbox.api.serializers import (
 from netbox.api.serializers import (
@@ -728,6 +730,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
         required=False,
         required=False,
         many=True
         many=True
     )
     )
+    vrf = NestedVRFSerializer(required=False, allow_null=True)
     cable = NestedCableSerializer(read_only=True)
     cable = NestedCableSerializer(read_only=True)
     wireless_link = NestedWirelessLinkSerializer(read_only=True)
     wireless_link = NestedWirelessLinkSerializer(read_only=True)
     wireless_lans = SerializedPKRelatedField(
     wireless_lans = SerializedPKRelatedField(
@@ -745,7 +748,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
             'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
             'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
             'mtu', 'mac_address', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
             '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',
             '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',
             'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
             'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
             '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):
 class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
     queryset = Interface.objects.prefetch_related(
     queryset = Interface.objects.prefetch_related(
         'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
         '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
     serializer_class = serializers.InterfaceSerializer
     filterset_class = filtersets.InterfaceFilterSet
     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.filters import TagFilter
 from extras.filtersets import LocalConfigContextFilterSet
 from extras.filtersets import LocalConfigContextFilterSet
-from ipam.models import ASN
+from ipam.models import ASN, VRF
 from netbox.filtersets import (
 from netbox.filtersets import (
     BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
     BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
 )
 )
@@ -1217,6 +1217,17 @@ class InterfaceFilterSet(PrimaryModelFilterSet, DeviceComponentFilterSet, CableT
     rf_channel = django_filters.MultipleChoiceFilter(
     rf_channel = django_filters.MultipleChoiceFilter(
         choices=WirelessChannelChoices
         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:
     class Meta:
         model = Interface
         model = Interface

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

@@ -7,7 +7,7 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models import *
 from dcim.models import *
 from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
 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 tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
     add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
     add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField,
@@ -1061,7 +1061,8 @@ class InterfaceBulkEditForm(
         required=False,
         required=False,
         query_params={
         query_params={
             'type': 'lag',
             'type': 'lag',
-        }
+        },
+        label='LAG'
     )
     )
     mgmt_only = forms.NullBooleanField(
     mgmt_only = forms.NullBooleanField(
         required=False,
         required=False,
@@ -1080,11 +1081,16 @@ class InterfaceBulkEditForm(
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         required=False
         required=False
     )
     )
+    vrf = DynamicModelChoiceField(
+        queryset=VRF.objects.all(),
+        required=False,
+        label='VRF'
+    )
 
 
     class Meta:
     class Meta:
         nullable_fields = [
         nullable_fields = [
             'label', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel',
             '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):
     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.constants import *
 from dcim.models import *
 from dcim.models import *
 from extras.forms import CustomFieldModelCSVForm
 from extras.forms import CustomFieldModelCSVForm
+from ipam.models import VRF
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
 from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField
 from virtualization.models import Cluster
 from virtualization.models import Cluster
@@ -622,6 +623,12 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
         required=False,
         required=False,
         help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
         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(
     rf_role = CSVChoiceField(
         choices=WirelessRoleChoices,
         choices=WirelessRoleChoices,
         required=False,
         required=False,
@@ -632,7 +639,7 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
         model = Interface
         model = Interface
         fields = (
         fields = (
             'device', 'name', 'label', 'parent', 'bridge', 'lag', 'type', 'enabled', 'mark_connected', 'mac_address',
             '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',
             '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.constants import *
 from dcim.models import *
 from dcim.models import *
 from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
 from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
-from ipam.models import ASN
+from ipam.models import ASN, VRF
 from tenancy.forms import TenancyFilterForm
 from tenancy.forms import TenancyFilterForm
 from utilities.forms import (
 from utilities.forms import (
     APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
     APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect,
@@ -920,7 +920,8 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
     model = Interface
     model = Interface
     field_groups = [
     field_groups = [
         ['q', 'tag'],
         ['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'],
         ['rf_role', 'rf_channel', 'rf_channel_width', 'tx_power'],
         ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
         ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'],
     ]
     ]
@@ -980,6 +981,11 @@ class InterfaceFilterForm(DeviceComponentFilterForm):
         min_value=0,
         min_value=0,
         max_value=127
         max_value=127
     )
     )
+    vrf_id = DynamicModelMultipleChoiceField(
+        queryset=VRF.objects.all(),
+        required=False,
+        label='VRF'
+    )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 

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

@@ -9,7 +9,7 @@ from dcim.constants import *
 from dcim.models import *
 from dcim.models import *
 from extras.forms import CustomFieldModelForm
 from extras.forms import CustomFieldModelForm
 from extras.models import Tag
 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 tenancy.forms import TenancyForm
 from utilities.forms import (
 from utilities.forms import (
     APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
     APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
@@ -1261,6 +1261,11 @@ class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
             'available_on_device': '$device',
             'available_on_device': '$device',
         }
         }
     )
     )
+    vrf = DynamicModelChoiceField(
+        queryset=VRF.objects.all(),
+        required=False,
+        label='VRF'
+    )
     tags = DynamicModelMultipleChoiceField(
     tags = DynamicModelMultipleChoiceField(
         queryset=Tag.objects.all(),
         queryset=Tag.objects.all(),
         required=False
         required=False
@@ -1271,11 +1276,11 @@ class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm):
         fields = [
         fields = [
             'device', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag', 'mac_address', 'wwn', 'mtu',
             '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',
             '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 = (
         fieldsets = (
             ('Interface', ('device', 'name', 'type', 'label', 'description', 'tags')),
             ('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')),
             ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
             ('Related Interfaces', ('parent', 'bridge', 'lag')),
             ('Related Interfaces', ('parent', 'bridge', 'lag')),
             ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
             ('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,
         blank=True,
         verbose_name='Tagged VLANs'
         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(
     ip_addresses = GenericRelation(
         to='ipam.IPAddress',
         to='ipam.IPAddress',
         content_type_field='assigned_object_type',
         content_type_field='assigned_object_type',

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

@@ -521,6 +521,9 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
         orderable=False,
         orderable=False,
         verbose_name='Wireless LANs'
         verbose_name='Wireless LANs'
     )
     )
+    vrf = tables.Column(
+        linkify=True
+    )
     tags = TagColumn(
     tags = TagColumn(
         url_name='dcim:interface_list'
         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',
             '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',
             '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',
             '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')
         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.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models 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 utilities.testing import APITestCase, APIViewTestCases, create_test_device
 from virtualization.models import Cluster, ClusterType
 from virtualization.models import Cluster, ClusterType
 from wireless.models import WirelessLAN
 from wireless.models import WirelessLAN
@@ -1424,6 +1424,13 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
         )
         )
         WirelessLAN.objects.bulk_create(wireless_lans)
         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 = [
         cls.create_data = [
             {
             {
                 'device': device.pk,
                 'device': device.pk,
@@ -1431,6 +1438,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
                 'type': '1000base-t',
                 'type': '1000base-t',
                 'mode': InterfaceModeChoices.MODE_TAGGED,
                 'mode': InterfaceModeChoices.MODE_TAGGED,
                 'tx_power': 10,
                 'tx_power': 10,
+                'vrf': vrfs[0].pk,
                 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
                 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
                 'untagged_vlan': vlans[2].pk,
                 'untagged_vlan': vlans[2].pk,
                 'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].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,
                 'mode': InterfaceModeChoices.MODE_TAGGED,
                 'bridge': interfaces[0].pk,
                 'bridge': interfaces[0].pk,
                 'tx_power': 10,
                 'tx_power': 10,
+                'vrf': vrfs[1].pk,
                 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
                 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
                 'untagged_vlan': vlans[2].pk,
                 'untagged_vlan': vlans[2].pk,
                 'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].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,
                 'mode': InterfaceModeChoices.MODE_TAGGED,
                 'parent': interfaces[1].pk,
                 'parent': interfaces[1].pk,
                 'tx_power': 10,
                 'tx_power': 10,
+                'vrf': vrfs[2].pk,
                 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
                 'tagged_vlans': [vlans[0].pk, vlans[1].pk],
                 'untagged_vlan': vlans[2].pk,
                 'untagged_vlan': vlans[2].pk,
                 'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].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.choices import *
 from dcim.filtersets import *
 from dcim.filtersets import *
 from dcim.models 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 tenancy.models import Tenant, TenantGroup
 from utilities.choices import ColorChoices
 from utilities.choices import ColorChoices
 from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
 from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
@@ -2370,15 +2370,22 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
         )
         )
         Device.objects.bulk_create(devices)
         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
         # VirtualChassis assignment for filtering
         virtual_chassis = VirtualChassis.objects.create(master=devices[0])
         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[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)
         Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=virtual_chassis, vc_position=2, vc_priority=2)
 
 
         interfaces = (
         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 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 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),
             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]}
         params = {'tx_power': [40]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
         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):
 class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
     queryset = FrontPort.objects.all()
     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.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models 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 tenancy.models import Tenant
 from utilities.testing import ViewTestCases, create_tags, create_test_device
 from utilities.testing import ViewTestCases, create_tags, create_test_device
 from wireless.models import WirelessLAN
 from wireless.models import WirelessLAN
@@ -2105,6 +2105,13 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
         )
         )
         WirelessLAN.objects.bulk_create(wireless_lans)
         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')
         tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
 
         cls.form_data = {
         cls.form_data = {
@@ -2124,6 +2131,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'untagged_vlan': vlans[0].pk,
             'untagged_vlan': vlans[0].pk,
             'tagged_vlans': [v.pk for v in vlans[1:4]],
             'tagged_vlans': [v.pk for v in vlans[1:4]],
             'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
             'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
+            'vrf': vrfs[0].pk,
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
         }
         }
 
 
@@ -2143,6 +2151,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'untagged_vlan': vlans[0].pk,
             'untagged_vlan': vlans[0].pk,
             'tagged_vlans': [v.pk for v in vlans[1:4]],
             'tagged_vlans': [v.pk for v in vlans[1:4]],
             'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
             'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
+            'vrf': vrfs[0].pk,
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
         }
         }
 
 
@@ -2159,13 +2168,14 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'tx_power': 10,
             'tx_power': 10,
             'untagged_vlan': vlans[0].pk,
             'untagged_vlan': vlans[0].pk,
             'tagged_vlans': [v.pk for v in vlans[1:4]],
             'tagged_vlans': [v.pk for v in vlans[1:4]],
+            'vrf': vrfs[1].pk,
         }
         }
 
 
         cls.csv_data = (
         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=['*'])
     @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])

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

@@ -108,6 +108,16 @@
                             <th scope="row">802.1Q Mode</th>
                             <th scope="row">802.1Q Mode</th>
                             <td>{{ object.get_mode_display|placeholder }}</td>
                             <td>{{ object.get_mode_display|placeholder }}</td>
                         </tr>
                         </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>
                     </table>
                 </div>
                 </div>
             </div>
             </div>

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

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