Repository: netbox-community/netbox

Issue:
Missing error message when Tagged VLAN with incompatible Site is set
<!--
    NOTE: This form is only for reproducible bugs. If you need assistance with
    NetBox installation, or if you have a general question, DO NOT open an
    issue. Instead, post to our mailing list:

        https://groups.google.com/forum/#!forum/netbox-discuss

    Please describe the environment in which you are running NetBox. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report.
-->
### Environment
* Python version: Python version: 3.6.8
* NetBox version: NetBox version: 2.6.5

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of NetBox (or the current beta
    release where applicable). Begin with the creation of any necessary
    database objects and call out every operation being performed explicitly.
    If reporting a bug in the REST API, be sure to reconstruct the raw HTTP
    request(s) being made: Don't rely on a wrapper like pynetbox.
-->
### Steps to Reproduce
1. Create VLAN 123 associated with Site AAA
2. Create Device at Site BBB and Interface
3. Edit interface and set 802.1Q Mode to Tagged
4. Select VLAN 123 as Tagged VLAN
5. Apply

<!-- What did you expect to happen? -->
### Expected Behavior
An Error message.

<!-- What happened instead? -->
### Observed Behavior
No error message, and the VLAN does not get set.

### Comment
A error message is correctly produced if the Mode is Access and the VLAN is selected in the Untagged input field instead. Then we get _"The untagged VLAN (123) must belong to the same site as the interface's parent device/VM, or it must be global"_. The same message should be show there too.


Discussion:
One issue is that the list of VLANs is not currently filtered by site/group assignment. This would actually be very difficult to implement fully given the current state of logic around API-based select fields. It would probably be best handled by introducing a new filter that returns all available VLANs for a given site (e.g. `available_for_site=foo`).

The other issue is that when an invalid VLAN is selected, we do not raise a validation error. I'm going to say that the scope of this particular bug report should be limited to this second issue only.
> The other issue is that when an invalid VLAN is selected, we do not raise a validation error. I'm going to say that the scope of this particular bug report should be limited to this second issue only.

I thought we did this in the clean() logic, did we not?
If we do, it's not working. I was able to confirm the reported bug.
Just checked, it only checks for untagged vlans.
I am going to give this a stab and see if there is a way to have it perform more then one query for the API as well.

Resolving patch:
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -74,6 +74,17 @@ def clean(self):
         elif self.cleaned_data['mode'] == IFACE_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'] == IFACE_MODE_TAGGED:
+            valid_sites = [None, self.cleaned_data['device'].site]
+            invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites]
+
+            if invalid_vlans:
+                raise forms.ValidationError({
+                    'tagged_vlans': "The tagged VLANs ({}) must belong to the same site as the interface's parent "
+                                    "device/VM, or they must be global".format(', '.join(invalid_vlans))
+                })
+
 
 class BulkRenameForm(forms.Form):
     """
@@ -2278,36 +2289,6 @@ def __init__(self, *args, **kwargs):
                 device__in=[self.instance.device, self.instance.device.get_vc_master()], type=IFACE_TYPE_LAG
             )
 
-        # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
-        vlan_choices = []
-        global_vlans = VLAN.objects.filter(site=None, group=None)
-        vlan_choices.append(
-            ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
-        )
-        for group in VLANGroup.objects.filter(site=None):
-            global_group_vlans = VLAN.objects.filter(group=group)
-            vlan_choices.append(
-                (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
-            )
-
-        site = getattr(self.instance.parent, 'site', None)
-        if site is not None:
-
-            # Add non-grouped site VLANs
-            site_vlans = VLAN.objects.filter(site=site, group=None)
-            vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
-
-            # Add grouped site VLANs
-            for group in VLANGroup.objects.filter(site=site):
-                site_group_vlans = VLAN.objects.filter(group=group)
-                vlan_choices.append((
-                    '{} / {}'.format(group.site.name, group.name),
-                    [(vlan.pk, vlan) for vlan in site_group_vlans]
-                ))
-
-        self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
-        self.fields['tagged_vlans'].choices = vlan_choices
-
 
 class InterfaceCreateForm(InterfaceCommonForm, ComponentForm, forms.Form):
     name_pattern = ExpandableNameField(
@@ -2388,36 +2369,6 @@ def __init__(self, *args, **kwargs):
         else:
             self.fields['lag'].queryset = Interface.objects.none()
 
-        # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
-        vlan_choices = []
-        global_vlans = VLAN.objects.filter(site=None, group=None)
-        vlan_choices.append(
-            ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
-        )
-        for group in VLANGroup.objects.filter(site=None):
-            global_group_vlans = VLAN.objects.filter(group=group)
-            vlan_choices.append(
-                (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
-            )
-
-        site = getattr(self.parent, 'site', None)
-        if site is not None:
-
-            # Add non-grouped site VLANs
-            site_vlans = VLAN.objects.filter(site=site, group=None)
-            vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
-
-            # Add grouped site VLANs
-            for group in VLANGroup.objects.filter(site=site):
-                site_group_vlans = VLAN.objects.filter(group=group)
-                vlan_choices.append((
-                    '{} / {}'.format(group.site.name, group.name),
-                    [(vlan.pk, vlan) for vlan in site_group_vlans]
-                ))
-
-        self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
-        self.fields['tagged_vlans'].choices = vlan_choices
-
 
 class InterfaceBulkEditForm(InterfaceCommonForm, BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
     pk = forms.ModelMultipleChoiceField(
@@ -2500,36 +2451,6 @@ def __init__(self, *args, **kwargs):
         else:
             self.fields['lag'].choices = []
 
-        # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
-        vlan_choices = []
-        global_vlans = VLAN.objects.filter(site=None, group=None)
-        vlan_choices.append(
-            ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
-        )
-        for group in VLANGroup.objects.filter(site=None):
-            global_group_vlans = VLAN.objects.filter(group=group)
-            vlan_choices.append(
-                (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
-            )
-        if self.parent_obj is not None:
-            site = getattr(self.parent_obj, 'site', None)
-            if site is not None:
-
-                # Add non-grouped site VLANs
-                site_vlans = VLAN.objects.filter(site=site, group=None)
-                vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
-
-                # Add grouped site VLANs
-                for group in VLANGroup.objects.filter(site=site):
-                    site_group_vlans = VLAN.objects.filter(group=group)
-                    vlan_choices.append((
-                        '{} / {}'.format(group.site.name, group.name),
-                        [(vlan.pk, vlan) for vlan in site_group_vlans]
-                    ))
-
-        self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
-        self.fields['tagged_vlans'].choices = vlan_choices
-
 
 class InterfaceBulkRenameForm(BulkRenameForm):
     pk = forms.ModelMultipleChoiceField(


Test patch: