Closes #6121: Extend parent interface assignment to VM interfaces

This commit is contained in:
jeremystretch
2021-04-09 10:53:05 -04:00
parent 7439faad34
commit a3721a94ce
13 changed files with 128 additions and 18 deletions

View File

@@ -106,6 +106,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
class VMInterfaceSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
virtual_machine = NestedVirtualMachineSerializer()
parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
tagged_vlans = SerializedPKRelatedField(
@@ -118,8 +119,8 @@ class VMInterfaceSerializer(PrimaryModelSerializer):
class Meta:
model = VMInterface
fields = [
'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'mtu', 'mac_address', 'description', 'mode',
'untagged_vlan', 'tagged_vlans', 'tags', 'custom_fields', 'created', 'last_updated',
'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'mtu', 'mac_address', 'description',
'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'custom_fields', 'created', 'last_updated',
]
def validate(self, data):

View File

@@ -80,7 +80,7 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet)
class VMInterfaceViewSet(ModelViewSet):
queryset = VMInterface.objects.prefetch_related(
'virtual_machine', 'tags', 'tagged_vlans'
'virtual_machine', 'parent', 'tags', 'tagged_vlans'
)
serializer_class = serializers.VMInterfaceSerializer
filterset_class = filters.VMInterfaceFilterSet

View File

@@ -264,6 +264,11 @@ class VMInterfaceFilterSet(BaseFilterSet, CustomFieldModelFilterSet, CreatedUpda
to_field_name='name',
label='Virtual machine',
)
parent_id = django_filters.ModelMultipleChoiceFilter(
field_name='parent',
queryset=VMInterface.objects.all(),
label='Parent interface (ID)',
)
mac_address = MultiValueMACAddressFilter(
label='MAC address',
)

View File

@@ -603,6 +603,11 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil
#
class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm):
parent = DynamicModelChoiceField(
queryset=VMInterface.objects.all(),
required=False,
label='Parent interface'
)
untagged_vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
required=False,
@@ -621,8 +626,8 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm)
class Meta:
model = VMInterface
fields = [
'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'tags', 'untagged_vlan',
'tagged_vlans',
'virtual_machine', 'name', 'enabled', 'parent', 'mac_address', 'mtu', 'description', 'mode', 'tags',
'untagged_vlan', 'tagged_vlans',
]
widgets = {
'virtual_machine': forms.HiddenInput(),
@@ -637,9 +642,12 @@ class VMInterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine')
# Restrict parent interface assignment by VM
self.fields['parent'].widget.add_query_param('virtualmachine_id', vm_id)
# Limit VLAN choices by virtual machine
vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine')
self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)
@@ -655,6 +663,14 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm):
required=False,
initial=True
)
parent = DynamicModelChoiceField(
queryset=VMInterface.objects.all(),
required=False,
display_field='display_name',
query_params={
'virtualmachine_id': 'virtual_machine',
}
)
mtu = forms.IntegerField(
required=False,
min_value=INTERFACE_MTU_MIN,
@@ -689,9 +705,12 @@ class VMInterfaceCreateForm(BootstrapMixin, InterfaceCommonForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine')
# Restrict parent interface assignment by VM
self.fields['parent'].widget.add_query_param('virtualmachine_id', vm_id)
# Limit VLAN choices by virtual machine
vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine')
self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)
@@ -730,6 +749,11 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
disabled=True,
widget=forms.HiddenInput()
)
parent = DynamicModelChoiceField(
queryset=VMInterface.objects.all(),
required=False,
display_field='display_name'
)
enabled = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
@@ -760,14 +784,17 @@ class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
class Meta:
nullable_fields = [
'mtu', 'description',
'parent', 'mtu', 'description',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine')
# Restrict parent interface assignment by VM
self.fields['parent'].widget.add_query_param('virtualmachine_id', vm_id)
# Limit VLAN choices by virtual machine
vm_id = self.initial.get('virtual_machine') or self.data.get('virtual_machine')
self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)

View File

@@ -0,0 +1,17 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0021_virtualmachine_vcpus_decimal'),
]
operations = [
migrations.AddField(
model_name='vminterface',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_interfaces', to='virtualization.vminterface'),
),
]

View File

@@ -395,6 +395,14 @@ class VMInterface(PrimaryModel, BaseInterface):
max_length=200,
blank=True
)
parent = models.ForeignKey(
to='self',
on_delete=models.SET_NULL,
related_name='child_interfaces',
null=True,
blank=True,
verbose_name='Parent interface'
)
untagged_vlan = models.ForeignKey(
to='ipam.VLAN',
on_delete=models.SET_NULL,
@@ -438,6 +446,7 @@ class VMInterface(PrimaryModel, BaseInterface):
self.virtual_machine.name,
self.name,
self.enabled,
self.parent.name if self.parent else None,
self.mac_address,
self.mtu,
self.description,
@@ -447,6 +456,13 @@ class VMInterface(PrimaryModel, BaseInterface):
def clean(self):
super().clean()
# An interface's parent must belong to the same virtual machine
if self.parent and self.parent.virtual_machine != self.virtual_machine:
raise ValidationError({
'parent': f"The selected parent interface ({self.parent}) belongs to a different virtual machine "
f"({self.parent.virtual_machine})."
})
# Validate untagged VLAN
if self.untagged_vlan and self.untagged_vlan.site not in [self.virtual_machine.site, None]:
raise ValidationError({

View File

@@ -170,6 +170,9 @@ class VMInterfaceTable(BaseInterfaceTable):
name = tables.Column(
linkify=True
)
parent = tables.Column(
linkify=True
)
tags = TagColumn(
url_name='virtualization:vminterface_list'
)
@@ -177,10 +180,10 @@ class VMInterfaceTable(BaseInterfaceTable):
class Meta(BaseTable.Meta):
model = VMInterface
fields = (
'pk', 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags',
'pk', 'virtual_machine', 'name', 'enabled', 'parent', 'mac_address', 'mtu', 'mode', 'description', 'tags',
'ip_addresses', 'untagged_vlan', 'tagged_vlans',
)
default_columns = ('pk', 'virtual_machine', 'name', 'enabled', 'description')
default_columns = ('pk', 'virtual_machine', 'name', 'enabled', 'parent', 'description')
class VirtualMachineVMInterfaceTable(VMInterfaceTable):

View File

@@ -453,12 +453,25 @@ class VMInterfaceTestCase(TestCase):
params = {'name': ['Interface 1', 'Interface 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_assigned_to_interface(self):
def test_enabled(self):
params = {'enabled': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'enabled': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_parent(self):
# Create child interfaces
parent_interface = VMInterface.objects.first()
child_interfaces = (
VMInterface(virtual_machine=parent_interface.virtual_machine, name='Child 1', parent=parent_interface),
VMInterface(virtual_machine=parent_interface.virtual_machine, name='Child 2', parent=parent_interface),
VMInterface(virtual_machine=parent_interface.virtual_machine, name='Child 3', parent=parent_interface),
)
VMInterface.objects.bulk_create(child_interfaces)
params = {'parent_id': [parent_interface.pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_mtu(self):
params = {'mtu': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@@ -421,6 +421,14 @@ class VMInterfaceView(generic.ObjectView):
orderable=False
)
# Get child interfaces
child_interfaces = VMInterface.objects.restrict(request.user, 'view').filter(parent=instance)
child_interfaces_tables = tables.VMInterfaceTable(
child_interfaces,
orderable=False
)
child_interfaces_tables.columns.hide('virtual_machine')
# Get assigned VLANs and annotate whether each is tagged or untagged
vlans = []
if instance.untagged_vlan is not None:
@@ -437,6 +445,7 @@ class VMInterfaceView(generic.ObjectView):
return {
'ipaddress_table': ipaddress_table,
'child_interfaces_table': child_interfaces_tables,
'vlan_table': vlan_table,
}