diff --git a/docs/models/virtualization/vminterface.md b/docs/models/virtualization/vminterface.md index 6fac7ce3682..7f1a5082def 100644 --- a/docs/models/virtualization/vminterface.md +++ b/docs/models/virtualization/vminterface.md @@ -1,3 +1,3 @@ ## Interfaces -Virtual machine interfaces behave similarly to device interfaces, and can be assigned IP addresses, VLANs, and services. However, given their virtual nature, they lack properties pertaining to physical attributes. For example, VM interfaces do not have a physical type and cannot have cables attached to them. +Virtual machine interfaces behave similarly to device interfaces, and can be assigned to VRFs, and may have IP addresses, VLANs, and services attached to them. However, given their virtual nature, they lack properties pertaining to physical attributes. For example, VM interfaces do not have a physical type and cannot have cables attached to them. diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 7168a13d00b..f60f6b81e76 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -134,3 +134,5 @@ A new REST API endpoint has been added at `/api/ipam/vlan-groups//available- * ipam.VLANGroup * Added the `/availables-vlans/` endpoint * Added the `min_vid` and `max_vid` fields +* virtualization.VMInterface + * Added `vrf` field diff --git a/netbox/templates/virtualization/vminterface.html b/netbox/templates/virtualization/vminterface.html index 6fcaf2a3e83..6d10ce91e61 100644 --- a/netbox/templates/virtualization/vminterface.html +++ b/netbox/templates/virtualization/vminterface.html @@ -59,6 +59,16 @@ {% endif %} + + VRF + + {% if object.vrf %} + {{ object.vrf }} + {% else %} + None + {% endif %} + + Description {{ object.description|placeholder }} diff --git a/netbox/templates/virtualization/vminterface_edit.html b/netbox/templates/virtualization/vminterface_edit.html index de882557432..bc479e9d970 100644 --- a/netbox/templates/virtualization/vminterface_edit.html +++ b/netbox/templates/virtualization/vminterface_edit.html @@ -22,6 +22,7 @@ {% render_field form.name %} {% render_field form.description %} {% render_field form.mac_address %} + {% render_field form.vrf %} {% render_field form.mtu %} {% render_field form.tags %} {% render_field form.enabled %} diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 866b8f9bb9d..3d345106213 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer from dcim.choices import InterfaceModeChoices -from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer +from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer from ipam.models import VLAN from netbox.api import ChoiceField, SerializedPKRelatedField from netbox.api.serializers import PrimaryModelSerializer @@ -116,6 +116,7 @@ class VMInterfaceSerializer(PrimaryModelSerializer): required=False, many=True ) + vrf = NestedVRFSerializer(required=False, allow_null=True) count_ipaddresses = serializers.IntegerField(read_only=True) count_fhrp_groups = serializers.IntegerField(read_only=True) @@ -123,8 +124,8 @@ class VMInterfaceSerializer(PrimaryModelSerializer): model = VMInterface fields = [ 'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address', - 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'custom_fields', 'created', 'last_updated', - 'count_ipaddresses', 'count_fhrp_groups', + 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', 'custom_fields', 'created', + 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', ] def validate(self, data): diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index 894045c1a95..471589ba540 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -80,7 +80,8 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet) class VMInterfaceViewSet(ModelViewSet): queryset = VMInterface.objects.prefetch_related( - 'virtual_machine', 'parent', 'tags', 'untagged_vlan', 'tagged_vlans', 'ip_addresses', 'fhrp_group_assignments', + 'virtual_machine', 'parent', 'tags', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', + 'fhrp_group_assignments', ) serializer_class = serializers.VMInterfaceSerializer filterset_class = filtersets.VMInterfaceFilterSet diff --git a/netbox/virtualization/filtersets.py b/netbox/virtualization/filtersets.py index 28b23e8a80e..6eae56c1306 100644 --- a/netbox/virtualization/filtersets.py +++ b/netbox/virtualization/filtersets.py @@ -3,6 +3,7 @@ from django.db.models import Q from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup from extras.filtersets import LocalConfigContextFilterSet +from ipam.models import VRF from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet from tenancy.filtersets import TenancyFilterSet from utilities.filters import MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter @@ -273,6 +274,17 @@ class VMInterfaceFilterSet(NetBoxModelFilterSet): mac_address = MultiValueMACAddressFilter( label='MAC address', ) + 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 = VMInterface diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index 6bd2f2d4ef3..d5d33df2aef 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -3,7 +3,7 @@ from django import forms from dcim.choices import InterfaceModeChoices from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup -from ipam.models import VLAN +from ipam.models import VLAN, VRF from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( @@ -190,15 +190,20 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm): queryset=VLAN.objects.all(), required=False ) + vrf = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) model = VMInterface fieldsets = ( - (None, ('mtu', 'enabled', 'description')), + (None, ('mtu', 'enabled', 'vrf', 'description')), ('Related Interfaces', ('parent', 'bridge')), ('802.1Q Switching', ('mode', 'untagged_vlan', 'tagged_vlans')), ) nullable_fields = ( - 'parent', 'bridge', 'mtu', 'description', + 'parent', 'bridge', 'mtu', 'vrf', 'description', ) def __init__(self, *args, **kwargs): diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py index cefc2219dd7..aa1b203e398 100644 --- a/netbox/virtualization/forms/bulk_import.py +++ b/netbox/virtualization/forms/bulk_import.py @@ -1,5 +1,6 @@ from dcim.choices import InterfaceModeChoices from dcim.models import DeviceRole, Platform, Site +from ipam.models import VRF from netbox.forms import NetBoxModelCSVForm from tenancy.models import Tenant from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField @@ -121,11 +122,18 @@ class VMInterfaceCSVForm(NetBoxModelCSVForm): 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' + ) class Meta: model = VMInterface fields = ( 'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode', + 'vrf', ) def clean_enabled(self): diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index 8e3dcd14305..7702a23ae5c 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -3,6 +3,7 @@ from django.utils.translation import gettext as _ from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup from extras.forms import LocalConfigContextFilterForm +from ipam.models import VRF from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import ( @@ -157,7 +158,7 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm): fieldsets = ( (None, ('q', 'tag')), ('Virtual Machine', ('cluster_id', 'virtual_machine_id')), - ('Attributes', ('enabled', 'mac_address')), + ('Attributes', ('enabled', 'mac_address', 'vrf_id')), ) cluster_id = DynamicModelMultipleChoiceField( queryset=Cluster.objects.all(), @@ -182,4 +183,9 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm): required=False, label='MAC address' ) + vrf_id = DynamicModelMultipleChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) tag = TagFilterField(model) diff --git a/netbox/virtualization/forms/models.py b/netbox/virtualization/forms/models.py index ecd909ec21a..e488ac23a01 100644 --- a/netbox/virtualization/forms/models.py +++ b/netbox/virtualization/forms/models.py @@ -6,7 +6,7 @@ from dcim.forms.common import InterfaceCommonForm from dcim.forms.models import INTERFACE_MODE_HELP_TEXT from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup from extras.models import Tag -from ipam.models import IPAddress, VLAN, VLANGroup +from ipam.models import IPAddress, VLAN, VLANGroup, VRF from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms import ( @@ -313,6 +313,11 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm): 'available_on_virtualmachine': '$virtual_machine', } ) + vrf = DynamicModelChoiceField( + queryset=VRF.objects.all(), + required=False, + label='VRF' + ) tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -322,7 +327,7 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm): model = VMInterface fields = [ 'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode', - 'tags', 'untagged_vlan', 'tagged_vlans', + 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', ] widgets = { 'virtual_machine': forms.HiddenInput(), diff --git a/netbox/virtualization/migrations/0028_vminterface_vrf.py b/netbox/virtualization/migrations/0028_vminterface_vrf.py new file mode 100644 index 00000000000..a188e1c609b --- /dev/null +++ b/netbox/virtualization/migrations/0028_vminterface_vrf.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.12 on 2022-02-07 14:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0056_standardize_id_fields'), + ('virtualization', '0027_standardize_id_fields'), + ] + + operations = [ + migrations.AddField( + model_name='vminterface', + name='vrf', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vminterfaces', to='ipam.vrf'), + ), + ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index dda1d0bee04..42d333d550e 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -384,6 +384,14 @@ class VMInterface(NetBoxModel, BaseInterface): object_id_field='assigned_object_id', related_query_name='vminterface' ) + vrf = models.ForeignKey( + to='ipam.VRF', + on_delete=models.SET_NULL, + related_name='vminterfaces', + null=True, + blank=True, + verbose_name='VRF' + ) fhrp_group_assignments = GenericRelation( to='ipam.FHRPGroupAssignment', content_type_field='interface_type', diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index e1156627ae0..4dc6bb9177f 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -168,6 +168,9 @@ class VMInterfaceTable(BaseInterfaceTable): name = tables.Column( linkify=True ) + vrf = tables.Column( + linkify=True + ) tags = columns.TagColumn( url_name='virtualization:vminterface_list' ) @@ -176,7 +179,7 @@ class VMInterfaceTable(BaseInterfaceTable): model = VMInterface fields = ( 'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags', - 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated', + 'vrf', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description') diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 4a9b67bf064..f6c07fa549d 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -2,7 +2,7 @@ from django.urls import reverse from rest_framework import status from dcim.choices import InterfaceModeChoices -from ipam.models import VLAN +from ipam.models import VLAN, VRF from utilities.testing import APITestCase, APIViewTestCases from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface @@ -234,6 +234,13 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase): ) VLAN.objects.bulk_create(vlans) + vrfs = ( + VRF(name='VRF 1'), + VRF(name='VRF 2'), + VRF(name='VRF 3'), + ) + VRF.objects.bulk_create(vrfs) + cls.create_data = [ { 'virtual_machine': virtualmachine.pk, @@ -241,6 +248,7 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase): 'mode': InterfaceModeChoices.MODE_TAGGED, 'tagged_vlans': [vlans[0].pk, vlans[1].pk], 'untagged_vlan': vlans[2].pk, + 'vrf': vrfs[0].pk, }, { 'virtual_machine': virtualmachine.pk, @@ -249,6 +257,7 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase): 'bridge': interfaces[0].pk, 'tagged_vlans': [vlans[0].pk, vlans[1].pk], 'untagged_vlan': vlans[2].pk, + 'vrf': vrfs[1].pk, }, { 'virtual_machine': virtualmachine.pk, @@ -257,5 +266,6 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase): 'parent': interfaces[1].pk, 'tagged_vlans': [vlans[0].pk, vlans[1].pk], 'untagged_vlan': vlans[2].pk, + 'vrf': vrfs[2].pk, }, ] diff --git a/netbox/virtualization/tests/test_filtersets.py b/netbox/virtualization/tests/test_filtersets.py index 8c8f6671f58..bcd2c469968 100644 --- a/netbox/virtualization/tests/test_filtersets.py +++ b/netbox/virtualization/tests/test_filtersets.py @@ -1,7 +1,7 @@ from django.test import TestCase from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup -from ipam.models import IPAddress +from ipam.models import IPAddress, VRF from tenancy.models import Tenant, TenantGroup from utilities.testing import ChangeLoggedFilterSetTests from virtualization.choices import * @@ -414,6 +414,13 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): ) Cluster.objects.bulk_create(clusters) + 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) + vms = ( VirtualMachine(name='Virtual Machine 1', cluster=clusters[0]), VirtualMachine(name='Virtual Machine 2', cluster=clusters[1]), @@ -422,9 +429,9 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): VirtualMachine.objects.bulk_create(vms) interfaces = ( - VMInterface(virtual_machine=vms[0], name='Interface 1', enabled=True, mtu=100, mac_address='00-00-00-00-00-01'), - VMInterface(virtual_machine=vms[1], name='Interface 2', enabled=True, mtu=200, mac_address='00-00-00-00-00-02'), - VMInterface(virtual_machine=vms[2], name='Interface 3', enabled=False, mtu=300, mac_address='00-00-00-00-00-03'), + VMInterface(virtual_machine=vms[0], name='Interface 1', enabled=True, mtu=100, mac_address='00-00-00-00-00-01', vrf=vrfs[0]), + VMInterface(virtual_machine=vms[1], name='Interface 2', enabled=True, mtu=200, mac_address='00-00-00-00-00-02', vrf=vrfs[1]), + VMInterface(virtual_machine=vms[2], name='Interface 3', enabled=False, mtu=300, mac_address='00-00-00-00-00-03', vrf=vrfs[2]), ) VMInterface.objects.bulk_create(interfaces) @@ -478,3 +485,10 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests): def test_mac_address(self): params = {'mac_address': ['00-00-00-00-00-01', '00-00-00-00-00-02']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + 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) diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 7dc5660fd49..8edc14f00df 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/netbox/virtualization/tests/test_views.py @@ -4,7 +4,7 @@ from netaddr import EUI from dcim.choices import InterfaceModeChoices from dcim.models import DeviceRole, Platform, Site -from ipam.models import VLAN +from ipam.models import VLAN, VRF from utilities.testing import ViewTestCases, create_tags from virtualization.choices import * from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface @@ -263,6 +263,13 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): ) VLAN.objects.bulk_create(vlans) + 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 = { @@ -276,6 +283,7 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': vlans[0].pk, 'tagged_vlans': [v.pk for v in vlans[1:4]], + 'vrf': vrfs[0].pk, 'tags': [t.pk for t in tags], } @@ -290,14 +298,15 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): 'mode': InterfaceModeChoices.MODE_TAGGED, 'untagged_vlan': vlans[0].pk, 'tagged_vlans': [v.pk for v in vlans[1:4]], + 'vrf': vrfs[0].pk, 'tags': [t.pk for t in tags], } cls.csv_data = ( - "virtual_machine,name", - "Virtual Machine 2,Interface 4", - "Virtual Machine 2,Interface 5", - "Virtual Machine 2,Interface 6", + f"virtual_machine,name,vrf.pk", + f"Virtual Machine 2,Interface 4,{vrfs[0].pk}", + f"Virtual Machine 2,Interface 5,{vrfs[0].pk}", + f"Virtual Machine 2,Interface 6,{vrfs[0].pk}", ) cls.bulk_edit_data = {