Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug changing interface mode #2816

Merged
merged 7 commits into from Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/2816.fixed
@@ -0,0 +1 @@
Fixed issue where changing the interface mode first required removing tagged_vlans in a different request.
33 changes: 21 additions & 12 deletions nautobot/dcim/api/serializers.py
Expand Up @@ -975,10 +975,29 @@ class Meta:
]


class InterfaceCommonSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
def validate(self, data):

# Validate many-to-many VLAN assignments
mode = data.get("mode", getattr(self.instance, "mode", None))

if mode != InterfaceModeChoices.MODE_TAGGED:
if data.get("tagged_vlans"):
raise serializers.ValidationError(
{
"tagged_vlans": f"Mode must be set to {InterfaceModeChoices.MODE_TAGGED} when specifying tagged_vlans"
}
)

if data.get("tagged_vlans") != [] and self.instance and self.instance.tagged_vlans.exists():
raise serializers.ValidationError({"tagged_vlans": f"Clear tagged_vlans to set mode to {mode}"})

return super().validate(data)


# 2.0 TODO: This becomes non-default in 2.0, removed in 2.2.
class InterfaceSerializerVersion12(
NautobotModelSerializer,
TaggedModelSerializerMixin,
InterfaceCommonSerializer,
CableTerminationModelSerializerMixin,
PathEndpointModelSerializerMixin,
):
Expand Down Expand Up @@ -1045,16 +1064,6 @@ def validate(self, data):
)

# Validate many-to-many VLAN assignments
mode = data.get("mode", getattr(self.instance, "mode", None))
has_tagged_vlans = data.get("tagged_vlans", False)
if not has_tagged_vlans and self.instance and self.instance.tagged_vlans.exists():
has_tagged_vlans = True

if has_tagged_vlans and mode != InterfaceModeChoices.MODE_TAGGED:
raise serializers.ValidationError(
{"tagged_vlans": f"Mode must be set to {InterfaceModeChoices.MODE_TAGGED} when specifying tagged_vlans"}
)

device = self.instance.device if self.instance else data.get("device")
for vlan in data.get("tagged_vlans", []):
if vlan.site not in [device.site, None]:
Expand Down
12 changes: 8 additions & 4 deletions nautobot/dcim/forms.py
Expand Up @@ -183,17 +183,21 @@ def clean(self):

parent_field = "device" if "device" in self.cleaned_data else "virtual_machine"
tagged_vlans = self.cleaned_data["tagged_vlans"]
mode = self.cleaned_data["mode"]

# Untagged interfaces cannot be assigned tagged VLANs
if self.cleaned_data["mode"] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
if mode == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
raise forms.ValidationError({"mode": "An access interface cannot have tagged VLANs assigned."})
timizuoebideri1 marked this conversation as resolved.
Show resolved Hide resolved

if mode != InterfaceModeChoices.MODE_TAGGED and tagged_vlans:
raise forms.ValidationError({"tagged_vlans": f"Clear tagged_vlans to set mode to {self.mode}"})

# Remove all tagged VLAN assignments from "tagged all" interfaces
elif self.cleaned_data["mode"] == InterfaceModeChoices.MODE_TAGGED_ALL:
elif mode == InterfaceModeChoices.MODE_TAGGED_ALL:
self.cleaned_data["tagged_vlans"] = []

# Validate tagged VLANs; must be a global VLAN or in the same site
elif self.cleaned_data["mode"] == InterfaceModeChoices.MODE_TAGGED:
elif mode == InterfaceModeChoices.MODE_TAGGED:
valid_sites = [None, self.cleaned_data[parent_field].site]
invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites]

Expand Down Expand Up @@ -2689,7 +2693,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm, StatusModelFilterFormMixin)
tag = TagFilterField(model)


class InterfaceForm(NautobotModelForm, InterfaceCommonForm):
class InterfaceForm(InterfaceCommonForm, NautobotModelForm):
parent_interface = DynamicModelChoiceField(
queryset=Interface.objects.all(),
required=False,
Expand Down
3 changes: 0 additions & 3 deletions nautobot/dcim/models/device_components.py
Expand Up @@ -519,9 +519,6 @@ def clean(self):
if not self.mode and self.untagged_vlan is not None:
raise ValidationError({"untagged_vlan": "Mode must be set when specifying untagged_vlan"})

if self.tagged_vlans.exists() and self.mode != InterfaceModeChoices.MODE_TAGGED:
raise ValidationError({"tagged_vlans": f"Clear tagged_vlans to set mode to {self.mode}"})

def save(self, *args, **kwargs):

if not self.status:
Expand Down
20 changes: 19 additions & 1 deletion nautobot/dcim/tests/test_api.py
Expand Up @@ -1839,13 +1839,31 @@ def test_tagged_vlan_raise_error_if_mode_not_set_to_tagged(self):
type=InterfaceTypeChoices.TYPE_VIRTUAL,
)
interface.tagged_vlans.add(self.vlans[0])
payload = {"mode": None}
payload = {"mode": None, "tagged_vlans": [self.vlans[2].pk]}
response = self.client.patch(self._get_detail_url(interface), data=payload, format="json", **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data["tagged_vlans"][0], "Mode must be set to tagged when specifying tagged_vlans"
)

def test_change_mode_from_tagged_to_others(self):
self.add_permissions("dcim.change_interface")
interface = Interface.objects.first()
interface.mode = InterfaceModeChoices.MODE_TAGGED
interface.validated_save()
interface.tagged_vlans.add(self.vlans[0])

with self.subTest("Update Fail"):
payload = {"mode": InterfaceModeChoices.MODE_ACCESS}
response = self.client.patch(self._get_detail_url(interface), data=payload, format="json", **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["tagged_vlans"][0], "Clear tagged_vlans to set mode to access")

with self.subTest("Update Successful"):
payload = {"mode": InterfaceModeChoices.MODE_ACCESS, "tagged_vlans": []}
response = self.client.patch(self._get_detail_url(interface), data=payload, format="json", **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)


class InterfaceTestVersion14(InterfaceTestVersion12):
api_version = "1.4"
Expand Down
15 changes: 0 additions & 15 deletions nautobot/dcim/tests/test_models.py
Expand Up @@ -9,7 +9,6 @@
CableStatusChoices,
CableTypeChoices,
DeviceFaceChoices,
InterfaceModeChoices,
InterfaceTypeChoices,
PortTypeChoices,
PowerOutletFeedLegChoices,
Expand Down Expand Up @@ -1334,20 +1333,6 @@ def test_tagged_vlan_raise_error_if_mode_not_set_to_tagged(self):
err.exception.message_dict["tagged_vlans"][0], "Mode must be set to tagged when specifying tagged_vlans"
)

def test_tagged_vlan_raise_error_if_mode_is_changed_without_clearing_tagged_vlans(self):
interface = Interface.objects.create(
name="Int2",
type=InterfaceTypeChoices.TYPE_VIRTUAL,
device=self.device,
mode=InterfaceModeChoices.MODE_TAGGED,
)
interface.tagged_vlans.add(self.vlan)

interface.mode = InterfaceModeChoices.MODE_ACCESS
with self.assertRaises(ValidationError) as err:
interface.validated_save()
self.assertEqual(err.exception.message_dict["tagged_vlans"][0], "Clear tagged_vlans to set mode to access")


class SiteTestCase(TestCase):
def test_latitude_or_longitude(self):
Expand Down
14 changes: 2 additions & 12 deletions nautobot/virtualization/api/serializers.py
Expand Up @@ -11,6 +11,7 @@
NestedPlatformSerializer,
NestedSiteSerializer,
)
from nautobot.dcim.api.serializers import InterfaceCommonSerializer
from nautobot.dcim.choices import InterfaceModeChoices
from nautobot.extras.api.serializers import (
NautobotModelSerializer,
Expand Down Expand Up @@ -167,7 +168,7 @@ def get_config_context(self, obj):


# 2.0 TODO: This becomes non-default in 2.0, removed in 2.2.
class VMInterfaceSerializerVersion12(NautobotModelSerializer, TaggedModelSerializerMixin):
class VMInterfaceSerializerVersion12(InterfaceCommonSerializer):
url = serializers.HyperlinkedIdentityField(view_name="virtualization-api:vminterface-detail")
virtual_machine = NestedVirtualMachineSerializer()
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
Expand Down Expand Up @@ -217,17 +218,6 @@ def validate(self, data):
)

# Validate many-to-many VLAN assignments
# if self.instance and self.instance.tagged_vlans.exists()
mode = data.get("mode", getattr(self.instance, "mode", None))
has_tagged_vlans = data.get("tagged_vlans", False)
if not has_tagged_vlans and self.instance and self.instance.tagged_vlans.exists():
has_tagged_vlans = True

if has_tagged_vlans and mode != InterfaceModeChoices.MODE_TAGGED:
raise serializers.ValidationError(
{"tagged_vlans": f"Mode must be set to {InterfaceModeChoices.MODE_TAGGED} when specifying tagged_vlans"}
)

virtual_machine = self.instance.virtual_machine if self.instance else data.get("virtual_machine")
for vlan in data.get("tagged_vlans", []):
if vlan.site not in [virtual_machine.site, None]:
Expand Down
23 changes: 21 additions & 2 deletions nautobot/virtualization/tests/test_api.py
Expand Up @@ -383,7 +383,7 @@ def test_untagged_vlan_requires_mode(self):

def test_tagged_vlan_raise_error_if_mode_not_set_to_tagged(self):
self.add_permissions("virtualization.add_vminterface", "virtualization.change_vminterface")
vlan = VLAN.objects.first()
vlan = VLAN.objects.get(name="VLAN 1")
virtualmachine = VirtualMachine.objects.first()
with self.subTest("On create, assert 400 status."):
payload = {
Expand All @@ -406,13 +406,32 @@ def test_tagged_vlan_raise_error_if_mode_not_set_to_tagged(self):
mode=InterfaceModeChoices.MODE_TAGGED,
)
interface.tagged_vlans.add(vlan)
payload = {"mode": None}
payload = {"mode": None, "tagged_vlans": [vlan.pk]}
response = self.client.patch(self._get_detail_url(interface), data=payload, format="json", **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data["tagged_vlans"][0], "Mode must be set to tagged when specifying tagged_vlans"
)

def test_change_mode_from_tagged_to_others(self):
self.add_permissions("virtualization.change_vminterface")
vlan = VLAN.objects.get(name="VLAN 1")
interface = VMInterface.objects.first()
interface.mode = InterfaceModeChoices.MODE_TAGGED
interface.validated_save()
interface.tagged_vlans.add(vlan)

with self.subTest("Update Fail"):
payload = {"mode": InterfaceModeChoices.MODE_ACCESS}
response = self.client.patch(self._get_detail_url(interface), data=payload, format="json", **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["tagged_vlans"][0], "Clear tagged_vlans to set mode to access")

with self.subTest("Update Successful"):
payload = {"mode": InterfaceModeChoices.MODE_ACCESS, "tagged_vlans": []}
response = self.client.patch(self._get_detail_url(interface), data=payload, format="json", **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)


class VMInterfaceTestVersion14(VMInterfaceTestVersion12):
api_version = "1.4"
Expand Down
12 changes: 0 additions & 12 deletions nautobot/virtualization/tests/test_models.py
Expand Up @@ -2,7 +2,6 @@
from django.core.exceptions import ValidationError
from django.test import TestCase

from nautobot.dcim.choices import InterfaceModeChoices
from nautobot.dcim.models import Location, LocationType, Site
from nautobot.extras.models import Status
from nautobot.ipam.models import VLAN
Expand Down Expand Up @@ -88,14 +87,3 @@ def test_tagged_vlan_raise_error_if_mode_not_set_to_tagged(self):
self.assertEqual(
err.exception.message_dict["tagged_vlans"][0], "Mode must be set to tagged when specifying tagged_vlans"
)

def test_tagged_vlan_raise_error_if_mode_is_changed_without_clearing_tagged_vlans(self):
interface = VMInterface.objects.create(
virtual_machine=self.virtualmachine, name="Interface 1", mode=InterfaceModeChoices.MODE_TAGGED
)
interface.tagged_vlans.add(self.vlan)

interface.mode = InterfaceModeChoices.MODE_ACCESS
with self.assertRaises(ValidationError) as err:
interface.validated_save()
self.assertEqual(err.exception.message_dict["tagged_vlans"][0], "Clear tagged_vlans to set mode to access")