From 4d2c75a824cdd56bf55657ecaba725fbb2d8525a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 24 Jun 2020 11:30:28 -0400 Subject: [PATCH] Restore ability to assign interface when editing an IPAddress --- netbox/ipam/forms.py | 111 +++++++++++++--------- netbox/ipam/models.py | 31 +++--- netbox/templates/ipam/ipaddress_edit.html | 48 +++++----- 3 files changed, 105 insertions(+), 85 deletions(-) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 83e7aa470..596d353bd 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -522,11 +522,33 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm) # class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModelForm): - # TODO: Restore ability to select assigned object when editing IPAddress - # interface = forms.ModelChoiceField( - # queryset=Interface.objects.all(), - # required=False - # ) + device = DynamicModelChoiceField( + queryset=Device.objects.all(), + required=False, + widget=APISelect( + filter_for={ + 'interface': 'device_id' + } + ) + ) + interface = DynamicModelChoiceField( + queryset=Interface.objects.all(), + required=False + ) + virtual_machine = DynamicModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + widget=APISelect( + filter_for={ + 'vminterface': 'virtual_machine_id' + } + ) + ) + vminterface = DynamicModelChoiceField( + queryset=VMInterface.objects.all(), + required=False, + label='Interface' + ) vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -611,67 +633,68 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel # Initialize helper selectors instance = kwargs.get('instance') initial = kwargs.get('initial', {}).copy() - if instance and instance.nat_inside and instance.nat_inside.device is not None: - initial['nat_site'] = instance.nat_inside.device.site - initial['nat_rack'] = instance.nat_inside.device.rack - initial['nat_device'] = instance.nat_inside.device + if instance: + if type(instance.assigned_object) is Interface: + initial['device'] = instance.assigned_object.device + initial['interface'] = instance.assigned_object + elif type(instance.assigned_object) is VMInterface: + initial['virtual_machine'] = instance.assigned_object.virtual_machine + initial['vminterface'] = instance.assigned_object + if instance.nat_inside and instance.nat_inside.device is not None: + initial['nat_site'] = instance.nat_inside.device.site + initial['nat_rack'] = instance.nat_inside.device.rack + initial['nat_device'] = instance.nat_inside.device kwargs['initial'] = initial super().__init__(*args, **kwargs) self.fields['vrf'].empty_label = 'Global' - # # Limit interface selections to those belonging to the parent device/VM - # if self.instance and self.instance.interface: - # self.fields['interface'].queryset = Interface.objects.filter( - # device=self.instance.interface.device, virtual_machine=self.instance.interface.virtual_machine - # ).prefetch_related( - # 'device__primary_ip4', - # 'device__primary_ip6', - # 'virtual_machine__primary_ip4', - # 'virtual_machine__primary_ip6', - # ) # We prefetch the primary address fields to ensure cache invalidation does not balk on the save() - # else: - # self.fields['interface'].choices = [] - # - # # Initialize primary_for_parent if IP address is already assigned - # if self.instance.pk and self.instance.interface is not None: - # parent = self.instance.interface.parent - # if ( - # self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or - # self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk - # ): - # self.initial['primary_for_parent'] = True + # Initialize primary_for_parent if IP address is already assigned + if self.instance.pk and self.instance.assigned_object: + parent = self.instance.assigned_object.parent + if ( + self.instance.address.version == 4 and parent.primary_ip4_id == self.instance.pk or + self.instance.address.version == 6 and parent.primary_ip6_id == self.instance.pk + ): + self.initial['primary_for_parent'] = True def clean(self): super().clean() + # Cannot select both a device interface and a VM interface + if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'): + raise forms.ValidationError("Cannot select both a device interface and a virtual machine interface") + # Primary IP assignment is only available if an interface has been assigned. - if self.cleaned_data.get('primary_for_parent') and not self.cleaned_data.get('interface'): + interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface') + if self.cleaned_data.get('primary_for_parent') and not interface: self.add_error( 'primary_for_parent', "Only IP addresses assigned to an interface can be designated as primary IPs." ) def save(self, *args, **kwargs): + # Set assigned object + interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface') + if interface: + self.instance.assigned_object = interface + ipaddress = super().save(*args, **kwargs) # Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine. - if self.cleaned_data['primary_for_parent']: - parent = self.cleaned_data['interface'].parent + if interface and self.cleaned_data['primary_for_parent']: if ipaddress.address.version == 4: - parent.primary_ip4 = ipaddress + interface.parent.primary_ip4 = ipaddress else: - parent.primary_ip6 = ipaddress - parent.save() - # elif self.cleaned_data['interface']: - # parent = self.cleaned_data['interface'].parent - # if ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress: - # parent.primary_ip4 = None - # parent.save() - # elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress: - # parent.primary_ip6 = None - # parent.save() + interface.primary_ip6 = ipaddress + interface.parent.save() + elif interface and ipaddress.address.version == 4 and interface.parent.primary_ip4 == ipaddress: + interface.parent.primary_ip4 = None + interface.parent.save() + elif interface and ipaddress.address.version == 6 and interface.parent.primary_ip6 == ipaddress: + interface.parent.primary_ip4 = None + interface.parent.save() return ipaddress diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index d2df6261f..ff564de6e 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -15,7 +15,7 @@ from extras.utils import extras_features from utilities.models import ChangeLoggedModel from utilities.querysets import RestrictedQuerySet from utilities.utils import serialize_object -from virtualization.models import VirtualMachine +from virtualization.models import VirtualMachine, VMInterface from .choices import * from .constants import * from .fields import IPNetworkField, IPAddressField @@ -717,32 +717,31 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): ) }) - if self.pk: - - # Check for primary IP assignment that doesn't match the assigned device/VM + # Check for primary IP assignment that doesn't match the assigned device/VM + if self.pk and type(self.assigned_object) is Interface: device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first() if device: - if self.interface is None: + if self.assigned_object is None: raise ValidationError({ - 'interface': "IP address is primary for device {} but not assigned".format(device) + 'interface': f"IP address is primary for device {device} but not assigned to an interface" }) - elif (device.primary_ip4 == self or device.primary_ip6 == self) and self.interface.device != device: + elif self.assigned_object.device != device: raise ValidationError({ - 'interface': "IP address is primary for device {} but assigned to {} ({})".format( - device, self.interface.device, self.interface - ) + 'interface': f"IP address is primary for device {device} but assigned to " + f"{self.assigned_object.device} ({self.assigned_object})" }) + elif self.pk and type(self.assigned_object) is VMInterface: vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first() if vm: - if self.interface is None: + if self.assigned_object is None: raise ValidationError({ - 'interface': "IP address is primary for virtual machine {} but not assigned".format(vm) + 'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to an " + f"interface" }) - elif (vm.primary_ip4 == self or vm.primary_ip6 == self) and self.interface.virtual_machine != vm: + elif self.interface.virtual_machine != vm: raise ValidationError({ - 'interface': "IP address is primary for virtual machine {} but assigned to {} ({})".format( - vm, self.interface.virtual_machine, self.interface - ) + 'vminterface': f"IP address is primary for virtual machine {vm} but assigned to " + f"{self.assigned_object.virtual_machine} ({self.assigned_object})" }) def save(self, *args, **kwargs): diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html index 8583ec160..4e2706daf 100644 --- a/netbox/templates/ipam/ipaddress_edit.html +++ b/netbox/templates/ipam/ipaddress_edit.html @@ -28,32 +28,30 @@ {% render_field form.tenant %} - {% if obj.assigned_object %} -
-
- Interface Assignment -
-
-
- - -
-
- - -
- {% render_field form.primary_for_parent %} -
+
+
+ Interface Assignment
- {% endif %} +
+ {% with vm_tab_active=obj.vminterface.exists %} + +
+
+ {% render_field form.device %} + {% render_field form.interface %} +
+
+ {% render_field form.virtual_machine %} + {% render_field form.vminterface %} +
+
+ {% endwith %} + {% render_field form.primary_for_parent %} +
+
NAT IP (Inside)