mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
#7852: Extend VRF assignment to VM interfaces
This commit is contained in:
parent
5fea012eab
commit
3651ef53e3
@ -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.
|
||||
|
@ -134,3 +134,5 @@ A new REST API endpoint has been added at `/api/ipam/vlan-groups/<pk>/available-
|
||||
* ipam.VLANGroup
|
||||
* Added the `/availables-vlans/` endpoint
|
||||
* Added the `min_vid` and `max_vid` fields
|
||||
* virtualization.VMInterface
|
||||
* Added `vrf` field
|
||||
|
@ -59,6 +59,16 @@
|
||||
{% endif %}
|
||||
</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>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ object.description|placeholder }} </td>
|
||||
|
@ -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 %}
|
||||
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user