diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 57ce1d57a..9664e9218 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -4,12 +4,12 @@ from rest_framework import serializers from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField from dcim.api.serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer -from dcim.constants import IFACE_MODE_CHOICES +from dcim.constants import IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_MODE_CHOICES from dcim.models import Interface from extras.api.customfields import CustomFieldModelSerializer from ipam.models import IPAddress, VLAN from tenancy.api.serializers import NestedTenantSerializer -from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer +from utilities.api import ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer from virtualization.constants import VM_STATUS_CHOICES from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -135,7 +135,7 @@ class NestedVirtualMachineSerializer(WritableNestedSerializer): # # Cannot import ipam.api.serializers.NestedVLANSerializer due to circular dependency -class InterfaceVLANSerializer(serializers.ModelSerializer): +class InterfaceVLANSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') class Meta: @@ -143,17 +143,24 @@ class InterfaceVLANSerializer(serializers.ModelSerializer): fields = ['id', 'url', 'vid', 'name', 'display_name'] -class InterfaceSerializer(serializers.ModelSerializer): +class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer): virtual_machine = NestedVirtualMachineSerializer() - mode = ChoiceField(choices=IFACE_MODE_CHOICES) - untagged_vlan = InterfaceVLANSerializer() - tagged_vlans = InterfaceVLANSerializer(many=True) + form_factor = ChoiceField(choices=IFACE_FF_CHOICES, default=IFACE_FF_VIRTUAL, required=False) + mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False) + untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True) + tagged_vlans = SerializedPKRelatedField( + queryset=VLAN.objects.all(), + serializer=InterfaceVLANSerializer, + required=False, + many=True + ) + tags = TagListSerializerField(required=False) class Meta: model = Interface fields = [ - 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'untagged_vlan', 'tagged_vlans', - 'description', + 'id', 'virtual_machine', 'name', 'form_factor', 'enabled', 'mtu', 'mac_address', 'description', 'mode', + 'untagged_vlan', 'tagged_vlans', 'tags', ] diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index 6ecf6ec22..b397e9dff 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -3,6 +3,9 @@ from __future__ import unicode_literals from django.urls import reverse from rest_framework import status +from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_TAGGED +from dcim.models import Interface +from ipam.models import VLAN from utilities.testing import APITestCase from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -390,3 +393,168 @@ class VirtualMachineTest(APITestCase): self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) self.assertEqual(VirtualMachine.objects.count(), 2) + + +class InterfaceTest(APITestCase): + + def setUp(self): + + super(InterfaceTest, self).setUp() + + clustertype = ClusterType.objects.create(name='Test Cluster Type 1', slug='test-cluster-type-1') + cluster = Cluster.objects.create(name='Test Cluster 1', type=clustertype) + self.virtualmachine = VirtualMachine.objects.create(cluster=cluster, name='Test VM 1') + self.interface1 = Interface.objects.create( + virtual_machine=self.virtualmachine, + name='Test Interface 1', + form_factor=IFACE_FF_VIRTUAL + ) + self.interface2 = Interface.objects.create( + virtual_machine=self.virtualmachine, + name='Test Interface 2', + form_factor=IFACE_FF_VIRTUAL + ) + self.interface3 = Interface.objects.create( + virtual_machine=self.virtualmachine, + name='Test Interface 3', + form_factor=IFACE_FF_VIRTUAL + ) + + self.vlan1 = VLAN.objects.create(name="Test VLAN 1", vid=1) + self.vlan2 = VLAN.objects.create(name="Test VLAN 2", vid=2) + self.vlan3 = VLAN.objects.create(name="Test VLAN 3", vid=3) + + def test_get_interface(self): + + url = reverse('virtualization-api:interface-detail', kwargs={'pk': self.interface1.pk}) + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['name'], self.interface1.name) + + def test_list_interfaces(self): + + url = reverse('virtualization-api:interface-list') + response = self.client.get(url, **self.header) + + self.assertEqual(response.data['count'], 3) + + def test_create_interface(self): + + data = { + 'virtual_machine': self.virtualmachine.pk, + 'name': 'Test Interface 4', + } + + url = reverse('virtualization-api:interface-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(Interface.objects.count(), 4) + interface4 = Interface.objects.get(pk=response.data['id']) + self.assertEqual(interface4.virtual_machine_id, data['virtual_machine']) + self.assertEqual(interface4.name, data['name']) + + def test_create_interface_with_802_1q(self): + + data = { + 'virtual_machine': self.virtualmachine.pk, + 'name': 'Test Interface 4', + 'mode': IFACE_MODE_TAGGED, + 'untagged_vlan': self.vlan3.id, + 'tagged_vlans': [self.vlan1.id, self.vlan2.id], + } + + url = reverse('virtualization-api:interface-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(Interface.objects.count(), 4) + self.assertEqual(response.data['virtual_machine']['id'], data['virtual_machine']) + self.assertEqual(response.data['name'], data['name']) + self.assertEqual(response.data['untagged_vlan']['id'], data['untagged_vlan']) + self.assertEqual([v['id'] for v in response.data['tagged_vlans']], data['tagged_vlans']) + + def test_create_interface_bulk(self): + + data = [ + { + 'virtual_machine': self.virtualmachine.pk, + 'name': 'Test Interface 4', + }, + { + 'virtual_machine': self.virtualmachine.pk, + 'name': 'Test Interface 5', + }, + { + 'virtual_machine': self.virtualmachine.pk, + 'name': 'Test Interface 6', + }, + ] + + url = reverse('virtualization-api:interface-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(Interface.objects.count(), 6) + self.assertEqual(response.data[0]['name'], data[0]['name']) + self.assertEqual(response.data[1]['name'], data[1]['name']) + self.assertEqual(response.data[2]['name'], data[2]['name']) + + def test_create_interface_802_1q_bulk(self): + + data = [ + { + 'virtual_machine': self.virtualmachine.pk, + 'name': 'Test Interface 4', + 'mode': IFACE_MODE_TAGGED, + 'untagged_vlan': self.vlan2.id, + 'tagged_vlans': [self.vlan1.id], + }, + { + 'virtual_machine': self.virtualmachine.pk, + 'name': 'Test Interface 5', + 'mode': IFACE_MODE_TAGGED, + 'untagged_vlan': self.vlan2.id, + 'tagged_vlans': [self.vlan1.id], + }, + { + 'virtual_machine': self.virtualmachine.pk, + 'name': 'Test Interface 6', + 'mode': IFACE_MODE_TAGGED, + 'untagged_vlan': self.vlan2.id, + 'tagged_vlans': [self.vlan1.id], + }, + ] + + url = reverse('virtualization-api:interface-list') + response = self.client.post(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(Interface.objects.count(), 6) + for i in range(0, 3): + self.assertEqual(response.data[i]['name'], data[i]['name']) + self.assertEqual([v['id'] for v in response.data[i]['tagged_vlans']], data[i]['tagged_vlans']) + self.assertEqual(response.data[i]['untagged_vlan']['id'], data[i]['untagged_vlan']) + + def test_update_interface(self): + + data = { + 'virtual_machine': self.virtualmachine.pk, + 'name': 'Test Interface X', + } + + url = reverse('virtualization-api:interface-detail', kwargs={'pk': self.interface1.pk}) + response = self.client.put(url, data, format='json', **self.header) + + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(Interface.objects.count(), 3) + interface1 = Interface.objects.get(pk=response.data['id']) + self.assertEqual(interface1.name, data['name']) + + def test_delete_interface(self): + + url = reverse('dcim-api:interface-detail', kwargs={'pk': self.interface1.pk}) + response = self.client.delete(url, **self.header) + + self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) + self.assertEqual(Interface.objects.count(), 2)