#7852: Extend VRF assignment to VM interfaces

This commit is contained in:
jeremystretch 2022-02-07 09:46:38 -05:00
parent 5fea012eab
commit 3651ef53e3
17 changed files with 137 additions and 22 deletions

View File

@ -1,3 +1,3 @@
## Interfaces ## 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.

View File

@ -134,3 +134,5 @@ A new REST API endpoint has been added at `/api/ipam/vlan-groups/<pk>/available-
* ipam.VLANGroup * ipam.VLANGroup
* Added the `/availables-vlans/` endpoint * Added the `/availables-vlans/` endpoint
* Added the `min_vid` and `max_vid` fields * Added the `min_vid` and `max_vid` fields
* virtualization.VMInterface
* Added `vrf` field

View File

@ -59,6 +59,16 @@
{% endif %} {% endif %}
</td> </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>
<tr> <tr>
<th scope="row">Description</th> <th scope="row">Description</th>
<td>{{ object.description|placeholder }} </td> <td>{{ object.description|placeholder }} </td>

View File

@ -22,6 +22,7 @@
{% render_field form.name %} {% render_field form.name %}
{% render_field form.description %} {% render_field form.description %}
{% render_field form.mac_address %} {% render_field form.mac_address %}
{% render_field form.vrf %}
{% render_field form.mtu %} {% render_field form.mtu %}
{% render_field form.tags %} {% render_field form.tags %}
{% render_field form.enabled %} {% render_field form.enabled %}

View File

@ -3,7 +3,7 @@ from rest_framework import serializers
from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
from dcim.choices import InterfaceModeChoices 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 ipam.models import VLAN
from netbox.api import ChoiceField, SerializedPKRelatedField from netbox.api import ChoiceField, SerializedPKRelatedField
from netbox.api.serializers import PrimaryModelSerializer from netbox.api.serializers import PrimaryModelSerializer
@ -116,6 +116,7 @@ class VMInterfaceSerializer(PrimaryModelSerializer):
required=False, required=False,
many=True many=True
) )
vrf = NestedVRFSerializer(required=False, allow_null=True)
count_ipaddresses = serializers.IntegerField(read_only=True) count_ipaddresses = serializers.IntegerField(read_only=True)
count_fhrp_groups = serializers.IntegerField(read_only=True) count_fhrp_groups = serializers.IntegerField(read_only=True)
@ -123,8 +124,8 @@ class VMInterfaceSerializer(PrimaryModelSerializer):
model = VMInterface model = VMInterface
fields = [ fields = [
'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address', 'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address',
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'custom_fields', 'created', 'last_updated', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', 'custom_fields', 'created',
'count_ipaddresses', 'count_fhrp_groups', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups',
] ]
def validate(self, data): def validate(self, data):

View File

@ -80,7 +80,8 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet)
class VMInterfaceViewSet(ModelViewSet): class VMInterfaceViewSet(ModelViewSet):
queryset = VMInterface.objects.prefetch_related( 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 serializer_class = serializers.VMInterfaceSerializer
filterset_class = filtersets.VMInterfaceFilterSet filterset_class = filtersets.VMInterfaceFilterSet

View File

@ -3,6 +3,7 @@ from django.db.models import Q
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
from extras.filtersets import LocalConfigContextFilterSet from extras.filtersets import LocalConfigContextFilterSet
from ipam.models import VRF
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from utilities.filters import MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter from utilities.filters import MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
@ -273,6 +274,17 @@ class VMInterfaceFilterSet(NetBoxModelFilterSet):
mac_address = MultiValueMACAddressFilter( mac_address = MultiValueMACAddressFilter(
label='MAC address', 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: class Meta:
model = VMInterface model = VMInterface

View File

@ -3,7 +3,7 @@ from django import forms
from dcim.choices import InterfaceModeChoices from dcim.choices import InterfaceModeChoices
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup 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 netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
@ -190,15 +190,20 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm):
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
required=False required=False
) )
vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
label='VRF'
)
model = VMInterface model = VMInterface
fieldsets = ( fieldsets = (
(None, ('mtu', 'enabled', 'description')), (None, ('mtu', 'enabled', 'vrf', 'description')),
('Related Interfaces', ('parent', 'bridge')), ('Related Interfaces', ('parent', 'bridge')),
('802.1Q Switching', ('mode', 'untagged_vlan', 'tagged_vlans')), ('802.1Q Switching', ('mode', 'untagged_vlan', 'tagged_vlans')),
) )
nullable_fields = ( nullable_fields = (
'parent', 'bridge', 'mtu', 'description', 'parent', 'bridge', 'mtu', 'vrf', 'description',
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -1,5 +1,6 @@
from dcim.choices import InterfaceModeChoices from dcim.choices import InterfaceModeChoices
from dcim.models import DeviceRole, Platform, Site from dcim.models import DeviceRole, Platform, Site
from ipam.models import VRF
from netbox.forms import NetBoxModelCSVForm from netbox.forms import NetBoxModelCSVForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField
@ -121,11 +122,18 @@ class VMInterfaceCSVForm(NetBoxModelCSVForm):
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'
)
class Meta: class Meta:
model = VMInterface model = VMInterface
fields = ( fields = (
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
'vrf',
) )
def clean_enabled(self): def clean_enabled(self):

View File

@ -3,6 +3,7 @@ from django.utils.translation import gettext as _
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
from extras.forms import LocalConfigContextFilterForm from extras.forms import LocalConfigContextFilterForm
from ipam.models import VRF
from netbox.forms import NetBoxModelFilterSetForm from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm from tenancy.forms import TenancyFilterForm
from utilities.forms import ( from utilities.forms import (
@ -157,7 +158,7 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'tag')),
('Virtual Machine', ('cluster_id', 'virtual_machine_id')), ('Virtual Machine', ('cluster_id', 'virtual_machine_id')),
('Attributes', ('enabled', 'mac_address')), ('Attributes', ('enabled', 'mac_address', 'vrf_id')),
) )
cluster_id = DynamicModelMultipleChoiceField( cluster_id = DynamicModelMultipleChoiceField(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
@ -182,4 +183,9 @@ class VMInterfaceFilterForm(NetBoxModelFilterSetForm):
required=False, required=False,
label='MAC address' label='MAC address'
) )
vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
label='VRF'
)
tag = TagFilterField(model) tag = TagFilterField(model)

View File

@ -6,7 +6,7 @@ from dcim.forms.common import InterfaceCommonForm
from dcim.forms.models import INTERFACE_MODE_HELP_TEXT from dcim.forms.models import INTERFACE_MODE_HELP_TEXT
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
from extras.models import Tag 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 netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from utilities.forms import ( from utilities.forms import (
@ -313,6 +313,11 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
'available_on_virtualmachine': '$virtual_machine', 'available_on_virtualmachine': '$virtual_machine',
} }
) )
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
@ -322,7 +327,7 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
model = VMInterface model = VMInterface
fields = [ fields = [
'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
'tags', 'untagged_vlan', 'tagged_vlans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags',
] ]
widgets = { widgets = {
'virtual_machine': forms.HiddenInput(), 'virtual_machine': forms.HiddenInput(),

View 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'),
),
]

View File

@ -384,6 +384,14 @@ class VMInterface(NetBoxModel, BaseInterface):
object_id_field='assigned_object_id', object_id_field='assigned_object_id',
related_query_name='vminterface' 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( fhrp_group_assignments = GenericRelation(
to='ipam.FHRPGroupAssignment', to='ipam.FHRPGroupAssignment',
content_type_field='interface_type', content_type_field='interface_type',

View File

@ -168,6 +168,9 @@ class VMInterfaceTable(BaseInterfaceTable):
name = tables.Column( name = tables.Column(
linkify=True linkify=True
) )
vrf = tables.Column(
linkify=True
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='virtualization:vminterface_list' url_name='virtualization:vminterface_list'
) )
@ -176,7 +179,7 @@ class VMInterfaceTable(BaseInterfaceTable):
model = VMInterface model = VMInterface
fields = ( fields = (
'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags', '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') default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description')

View File

@ -2,7 +2,7 @@ from django.urls import reverse
from rest_framework import status from rest_framework import status
from dcim.choices import InterfaceModeChoices from dcim.choices import InterfaceModeChoices
from ipam.models import VLAN from ipam.models import VLAN, VRF
from utilities.testing import APITestCase, APIViewTestCases from utilities.testing import APITestCase, APIViewTestCases
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -234,6 +234,13 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
) )
VLAN.objects.bulk_create(vlans) 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 = [ cls.create_data = [
{ {
'virtual_machine': virtualmachine.pk, 'virtual_machine': virtualmachine.pk,
@ -241,6 +248,7 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
'mode': InterfaceModeChoices.MODE_TAGGED, 'mode': InterfaceModeChoices.MODE_TAGGED,
'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,
'vrf': vrfs[0].pk,
}, },
{ {
'virtual_machine': virtualmachine.pk, 'virtual_machine': virtualmachine.pk,
@ -249,6 +257,7 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
'bridge': interfaces[0].pk, 'bridge': interfaces[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,
'vrf': vrfs[1].pk,
}, },
{ {
'virtual_machine': virtualmachine.pk, 'virtual_machine': virtualmachine.pk,
@ -257,5 +266,6 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
'parent': interfaces[1].pk, 'parent': interfaces[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,
'vrf': vrfs[2].pk,
}, },
] ]

View File

@ -1,7 +1,7 @@
from django.test import TestCase from django.test import TestCase
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup 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 tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests from utilities.testing import ChangeLoggedFilterSetTests
from virtualization.choices import * from virtualization.choices import *
@ -414,6 +414,13 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
) )
Cluster.objects.bulk_create(clusters) 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 = ( vms = (
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0]), VirtualMachine(name='Virtual Machine 1', cluster=clusters[0]),
VirtualMachine(name='Virtual Machine 2', cluster=clusters[1]), VirtualMachine(name='Virtual Machine 2', cluster=clusters[1]),
@ -422,9 +429,9 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
VirtualMachine.objects.bulk_create(vms) VirtualMachine.objects.bulk_create(vms)
interfaces = ( 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[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'), 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'), 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) VMInterface.objects.bulk_create(interfaces)
@ -478,3 +485,10 @@ class VMInterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_mac_address(self): def test_mac_address(self):
params = {'mac_address': ['00-00-00-00-00-01', '00-00-00-00-00-02']} 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) 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)

View File

@ -4,7 +4,7 @@ from netaddr import EUI
from dcim.choices import InterfaceModeChoices from dcim.choices import InterfaceModeChoices
from dcim.models import DeviceRole, Platform, Site 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 utilities.testing import ViewTestCases, create_tags
from virtualization.choices import * from virtualization.choices import *
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -263,6 +263,13 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
) )
VLAN.objects.bulk_create(vlans) 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') tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = { cls.form_data = {
@ -276,6 +283,7 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
'mode': InterfaceModeChoices.MODE_TAGGED, 'mode': InterfaceModeChoices.MODE_TAGGED,
'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[0].pk,
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
@ -290,14 +298,15 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
'mode': InterfaceModeChoices.MODE_TAGGED, 'mode': InterfaceModeChoices.MODE_TAGGED,
'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[0].pk,
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
} }
cls.csv_data = ( cls.csv_data = (
"virtual_machine,name", f"virtual_machine,name,vrf.pk",
"Virtual Machine 2,Interface 4", f"Virtual Machine 2,Interface 4,{vrfs[0].pk}",
"Virtual Machine 2,Interface 5", f"Virtual Machine 2,Interface 5,{vrfs[0].pk}",
"Virtual Machine 2,Interface 6", f"Virtual Machine 2,Interface 6,{vrfs[0].pk}",
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {