diff --git a/CHANGELOG.md b/CHANGELOG.md index bb24017bc..3303a71d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ v2.6.5 (FUTURE) * [#3531](https://github.com/netbox-community/netbox/issues/3531) - Fixed rack role foreground color * [#3534](https://github.com/netbox-community/netbox/issues/3534) - Added blank option for untagged VLANs * [#3540](https://github.com/netbox-community/netbox/issues/3540) - Fixed virtual machine interface edit with new inline vlan edit fields +* [#3543](https://github.com/netbox-community/netbox/issues/3543) - Added inline VLAN editing to virtual machine interfaces v2.6.4 (2019-09-19) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 55672514e..d3fc4b83b 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -2,11 +2,11 @@ from django import forms from django.core.exceptions import ValidationError from taggit.forms import TagField -from dcim.constants import IFACE_TYPE_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL +from dcim.constants import IFACE_TYPE_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL, IFACE_MODE_CHOICES from dcim.forms import INTERFACE_MODE_HELP_TEXT from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm -from ipam.models import IPAddress +from ipam.models import IPAddress, VLANGroup, VLAN from tenancy.forms import TenancyForm from tenancy.forms import TenancyFilterForm from tenancy.models import Tenant @@ -616,6 +616,24 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFil # class InterfaceForm(BootstrapMixin, forms.ModelForm): + untagged_vlan = forms.ModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + widget=APISelect( + api_url="/api/ipam/vlans/", + display_field='display_name', + full=True + ) + ) + tagged_vlans = forms.ModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False, + widget=APISelectMultiple( + api_url="/api/ipam/vlans/", + display_field='display_name', + full=True + ) + ) tags = TagField( required=False ) @@ -638,6 +656,39 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm): 'mode': INTERFACE_MODE_HELP_TEXT, } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # 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.device, '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 + def clean(self): super().clean() @@ -681,6 +732,29 @@ class InterfaceCreateForm(ComponentForm): max_length=100, required=False ) + mode = forms.ChoiceField( + choices=add_blank_choice(IFACE_MODE_CHOICES), + required=False, + widget=StaticSelect2(), + ) + untagged_vlan = forms.ModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + widget=APISelect( + api_url="/api/ipam/vlans/", + display_field='display_name', + full=True + ) + ) + tagged_vlans = forms.ModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False, + widget=APISelectMultiple( + api_url="/api/ipam/vlans/", + display_field='display_name', + full=True + ) + ) tags = TagField( required=False ) @@ -693,6 +767,36 @@ class InterfaceCreateForm(ComponentForm): super().__init__(*args, **kwargs) + # 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.cluster, '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(BootstrapMixin, BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -713,12 +817,68 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): max_length=100, required=False ) + mode = forms.ChoiceField( + choices=add_blank_choice(IFACE_MODE_CHOICES), + required=False, + widget=StaticSelect2() + ) + untagged_vlan = forms.ModelChoiceField( + queryset=VLAN.objects.all(), + required=False, + widget=APISelect( + api_url="/api/ipam/vlans/", + display_field='display_name', + full=True + ) + ) + tagged_vlans = forms.ModelMultipleChoiceField( + queryset=VLAN.objects.all(), + required=False, + widget=APISelectMultiple( + api_url="/api/ipam/vlans/", + display_field='display_name', + full=True + ) + ) class Meta: nullable_fields = [ 'mtu', 'description', ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # 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.cluster is not None: + site = getattr(self.parent_obj.cluster, '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 + # # Bulk VirtualMachine component creation