diff --git a/docs/models/virtualization/vminterface.md b/docs/models/virtualization/vminterface.md index 6fac7ce36..7f1a5082d 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 7168a13d0..f60f6b81e 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 6fcaf2a3e..6d10ce91e 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 de8825574..bc479e9d9 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 866b8f9bb..3d3451062 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 894045c1a..471589ba5 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 28b23e8a8..6eae56c13 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 6bd2f2d4e..d5d33df2a 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 cefc2219d..aa1b203e3 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 8e3dcd143..7702a23ae 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 ecd909ec2..e488ac23a 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 000000000..a188e1c60 --- /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 dda1d0bee..42d333d55 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 e1156627a..4dc6bb917 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 4a9b67bf0..f6c07fa54 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 8c8f6671f..bcd2c4699 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 7dc5660fd..8edc14f00 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 = {