mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-18 19:32:24 -06:00
#7852: Extend VRF assignment to VM interfaces
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
20
netbox/virtualization/migrations/0028_vminterface_vrf.py
Normal file
20
netbox/virtualization/migrations/0028_vminterface_vrf.py
Normal file
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user