Closes #1001: Merged IP interface assignment into ipam.IPAddressForm

This commit is contained in:
Jeremy Stretch 2017-04-13 14:54:17 -04:00
parent f70f0f8d62
commit 09000ad9b3
10 changed files with 81 additions and 196 deletions

View File

@ -1691,36 +1691,6 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
device = forms.CharField(required=False, label='Device name') device = forms.CharField(required=False, label='Device name')
#
# IP addresses
#
class IPAddressForm(BootstrapMixin, CustomFieldForm):
set_as_primary = forms.BooleanField(label='Set as primary IP for device', required=False)
class Meta:
model = IPAddress
fields = ['address', 'vrf', 'tenant', 'status', 'interface', 'description']
def __init__(self, device, *args, **kwargs):
super(IPAddressForm, self).__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global'
interfaces = device.interfaces.order_naturally(method=device.device_type.interface_ordering)
self.fields['interface'].queryset = interfaces
self.fields['interface'].required = True
# If this device has only one interface, select it by default.
if 'interface' not in self.initial and len(interfaces) == 1:
self.fields['interface'].initial = interfaces[0]
# If this device does not have any IP addresses assigned, default to setting the first IP as its primary.
if not IPAddress.objects.filter(interface__device=device).count():
self.fields['set_as_primary'].initial = True
# #
# Modules # Modules
# #

View File

@ -116,7 +116,6 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'), url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
url(r'^devices/(?P<pk>\d+)/inventory/$', views.device_inventory, name='device_inventory'), url(r'^devices/(?P<pk>\d+)/inventory/$', views.device_inventory, name='device_inventory'),
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'), url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'),
url(r'^devices/(?P<pk>\d+)/ip-addresses/assign/$', views.ipaddress_assign, name='ipaddress_assign'),
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'), url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'), url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'),

View File

@ -1561,47 +1561,6 @@ class InterfaceConnectionsListView(ObjectListView):
template_name = 'dcim/interface_connections_list.html' template_name = 'dcim/interface_connections_list.html'
#
# IP addresses
#
@permission_required(['dcim.change_device', 'ipam.add_ipaddress'])
def ipaddress_assign(request, pk):
device = get_object_or_404(Device, pk=pk)
if request.method == 'POST':
form = forms.IPAddressForm(device, request.POST)
if form.is_valid():
ipaddress = form.save(commit=False)
ipaddress.interface = form.cleaned_data['interface']
ipaddress.save()
form.save_custom_fields()
messages.success(request, u"Added new IP address {} to interface {}.".format(ipaddress, ipaddress.interface))
if form.cleaned_data['set_as_primary']:
if ipaddress.family == 4:
device.primary_ip4 = ipaddress
elif ipaddress.family == 6:
device.primary_ip6 = ipaddress
device.save()
if '_addanother' in request.POST:
return redirect('dcim:ipaddress_assign', pk=device.pk)
else:
return redirect('dcim:device', pk=device.pk)
else:
form = forms.IPAddressForm(device, initial=request.GET)
return render(request, 'dcim/ipaddress_assign.html', {
'device': device,
'form': form,
'return_url': reverse('dcim:device', kwargs={'pk': device.pk}),
})
# #
# Modules # Modules
# #

View File

@ -6,7 +6,7 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, BootstrapMixin, BulkImportForm, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch, APISelect, BootstrapMixin, BulkImportForm, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch,
SlugField, add_blank_choice, ReturnURLForm, SlugField, add_blank_choice,
) )
from .models import ( from .models import (
@ -307,21 +307,46 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
# IP addresses # IP addresses
# #
class IPAddressForm(BootstrapMixin, CustomFieldForm): class IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
nat_site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site', interface_site = forms.ModelChoiceField(
widget=forms.Select(attrs={'filter-for': 'nat_device'})) queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
nat_device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device', attrs={'filter-for': 'interface_rack'}
widget=APISelect(api_url='/api/dcim/devices/?site_id={{nat_site}}', )
display_field='display_name', )
attrs={'filter-for': 'nat_inside'})) interface_rack = forms.ModelChoiceField(
livesearch = forms.CharField(required=False, label='IP Address', widget=Livesearch( queryset=Rack.objects.all(), required=False, label='Rack', widget=APISelect(
query_key='q', query_url='ipam-api:ipaddress_list', field_to_update='nat_inside', obj_label='address') api_url='/api/dcim/racks/?site_id={{interface_site}}', display_field='display_name',
attrs={'filter-for': 'interface_device'}
)
)
interface_device = forms.ModelChoiceField(
queryset=Device.objects.all(), required=False, label='Device', widget=APISelect(
api_url='/api/dcim/devices/?site_id={{interface_site}}&rack_id={{interface_rack}}',
display_field='display_name', attrs={'filter-for': 'interface'}
)
)
nat_site = forms.ModelChoiceField(
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
attrs={'filter-for': 'nat_device'}
)
)
nat_device = forms.ModelChoiceField(
queryset=Device.objects.all(), required=False, label='Device', widget=APISelect(
api_url='/api/dcim/devices/?site_id={{nat_site}}', display_field='display_name',
attrs={'filter-for': 'nat_inside'}
)
)
livesearch = forms.CharField(
required=False, label='IP Address', widget=Livesearch(
query_key='q', query_url='ipam-api:ipaddress_list', field_to_update='nat_inside', obj_label='address'
)
) )
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = ['address', 'vrf', 'tenant', 'status', 'nat_inside', 'description'] fields = ['address', 'vrf', 'tenant', 'status', 'interface', 'nat_inside', 'description']
widgets = { widgets = {
'interface': APISelect(api_url='/api/dcim/devices/{{interface_device}}/interfaces/'),
'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address') 'nat_inside': APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address')
} }
@ -330,8 +355,37 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
self.fields['vrf'].empty_label = 'Global' self.fields['vrf'].empty_label = 'Global'
if self.instance.nat_inside: # If an interface has been assigned, initialize site, rack, and device
if self.instance.interface:
self.initial['interface_site'] = self.instance.interface.device.site
self.initial['interface_rack'] = self.instance.interface.device.rack
self.initial['interface_device'] = self.instance.interface.device
# Limit rack choices
if self.is_bound and self.data.get('interface_site'):
self.fields['interface_rack'].queryset = Rack.objects.filter(site__pk=self.data['interface_site'])
elif self.initial.get('interface_site'):
self.fields['interface_rack'].queryset = Rack.objects.filter(site=self.initial['interface_site'])
else:
self.fields['interface_rack'].choices = []
# Limit device choices
if self.is_bound and self.data.get('interface_rack'):
self.fields['interface_device'].queryset = Device.objects.filter(rack=self.data['interface_rack'])
elif self.initial.get('interface_rack'):
self.fields['interface_device'].queryset = Device.objects.filter(rack=self.initial['interface_rack'])
else:
self.fields['interface_device'].choices = []
# Limit interface choices
if self.is_bound and self.data.get('interface_device'):
self.fields['interface'].queryset = Interface.objects.filter(device=self.data['interface_device'])
elif self.initial.get('interface_device'):
self.fields['interface'].queryset = Interface.objects.filter(device=self.initial['interface_device'])
else:
self.fields['interface'].choices = []
if self.instance.nat_inside:
nat_inside = self.instance.nat_inside nat_inside = self.instance.nat_inside
# If the IP is assigned to an interface, populate site/device fields accordingly # If the IP is assigned to an interface, populate site/device fields accordingly
if self.instance.nat_inside.interface: if self.instance.nat_inside.interface:
@ -345,9 +399,7 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
) )
else: else:
self.fields['nat_inside'].queryset = IPAddress.objects.filter(pk=nat_inside.pk) self.fields['nat_inside'].queryset = IPAddress.objects.filter(pk=nat_inside.pk)
else: else:
# Initialize nat_device choices if nat_site is set # Initialize nat_device choices if nat_site is set
if self.is_bound and self.data.get('nat_site'): if self.is_bound and self.data.get('nat_site'):
self.fields['nat_device'].queryset = Device.objects.filter(site__pk=self.data['nat_site']) self.fields['nat_device'].queryset = Device.objects.filter(site__pk=self.data['nat_site'])
@ -355,7 +407,6 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
self.fields['nat_device'].queryset = Device.objects.filter(site=self.initial['nat_site']) self.fields['nat_device'].queryset = Device.objects.filter(site=self.initial['nat_site'])
else: else:
self.fields['nat_device'].choices = [] self.fields['nat_device'].choices = []
# Initialize nat_inside choices if nat_device is set # Initialize nat_inside choices if nat_device is set
if self.is_bound and self.data.get('nat_device'): if self.is_bound and self.data.get('nat_device'):
self.fields['nat_inside'].queryset = IPAddress.objects.filter( self.fields['nat_inside'].queryset = IPAddress.objects.filter(

View File

@ -57,8 +57,6 @@ urlpatterns = [
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
url(r'^ip-addresses/(?P<pk>\d+)/$', views.ipaddress, name='ipaddress'), url(r'^ip-addresses/(?P<pk>\d+)/$', views.ipaddress, name='ipaddress'),
url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'), url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
url(r'^ip-addresses/(?P<pk>\d+)/assign/$', views.ipaddress_assign, name='ipaddress_assign'),
url(r'^ip-addresses/(?P<pk>\d+)/remove/$', views.ipaddress_remove, name='ipaddress_remove'),
url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'), url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
# VLAN groups # VLAN groups

View File

@ -571,75 +571,6 @@ def ipaddress(request, pk):
}) })
@permission_required(['dcim.change_device', 'ipam.change_ipaddress'])
def ipaddress_assign(request, pk):
ipaddress = get_object_or_404(IPAddress, pk=pk)
if request.method == 'POST':
form = forms.IPAddressAssignForm(request.POST)
if form.is_valid():
interface = form.cleaned_data['interface']
ipaddress.interface = interface
ipaddress.save()
messages.success(request, u"Assigned IP address {} to interface {}.".format(ipaddress, ipaddress.interface))
if form.cleaned_data['set_as_primary']:
device = interface.device
if ipaddress.family == 4:
device.primary_ip4 = ipaddress
elif ipaddress.family == 6:
device.primary_ip6 = ipaddress
device.save()
return redirect('ipam:ipaddress', pk=ipaddress.pk)
else:
assert False, form.errors
else:
form = forms.IPAddressAssignForm()
return render(request, 'ipam/ipaddress_assign.html', {
'ipaddress': ipaddress,
'form': form,
'return_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
})
@permission_required(['dcim.change_device', 'ipam.change_ipaddress'])
def ipaddress_remove(request, pk):
ipaddress = get_object_or_404(IPAddress, pk=pk)
if request.method == 'POST':
form = ConfirmationForm(request.POST)
if form.is_valid():
device = ipaddress.interface.device
ipaddress.interface = None
ipaddress.save()
messages.success(request, u"Removed IP address {} from {}.".format(ipaddress, device))
if device.primary_ip4 == ipaddress.pk:
device.primary_ip4 = None
device.save()
elif device.primary_ip6 == ipaddress.pk:
device.primary_ip6 = None
device.save()
return redirect('ipam:ipaddress', pk=ipaddress.pk)
else:
form = ConfirmationForm()
return render(request, 'ipam/ipaddress_unassign.html', {
'ipaddress': ipaddress,
'form': form,
'return_url': reverse('ipam:ipaddress', kwargs={'pk': ipaddress.pk}),
})
class IPAddressEditView(PermissionRequiredMixin, ObjectEditView): class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_ipaddress' permission_required = 'ipam.change_ipaddress'
model = IPAddress model = IPAddress

View File

@ -59,7 +59,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if perms.ipam.add_ipaddress %} {% if perms.ipam.add_ipaddress %}
<a href="{% url 'dcim:ipaddress_assign' pk=device.pk %}?interface={{ iface.pk }}" class="btn btn-xs btn-success" title="Assign IP address"> <a href="{% url 'ipam:ipaddress_add' %}?interface_site={{ device.site.pk }}&interface_rack={{ device.rack.pk }}&interface_device={{ device.pk }}&interface={{ iface.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i> <i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}

View File

@ -98,14 +98,8 @@
<td> <td>
{% if ipaddress.interface %} {% if ipaddress.interface %}
<span><a href="{% url 'dcim:device' pk=ipaddress.interface.device.pk %}">{{ ipaddress.interface.device }}</a> ({{ ipaddress.interface }})</span> <span><a href="{% url 'dcim:device' pk=ipaddress.interface.device.pk %}">{{ ipaddress.interface.device }}</a> ({{ ipaddress.interface }})</span>
{% if perms.dcim.change_device and perms.ipam.change_ipaddress %}
<a href="{% url 'ipam:ipaddress_remove' pk=ipaddress.pk %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-remove"></i> Remove</a>
{% endif %}
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-muted">None</span>
{% if perms.dcim.change_device and perms.ipam.change_ipaddress %}
<a href="{% url 'ipam:ipaddress_assign' pk=ipaddress.pk %}" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-plus"></i> Assign</a>
{% endif %}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>

View File

@ -16,39 +16,20 @@
{% render_field form.vrf %} {% render_field form.vrf %}
{% render_field form.tenant %} {% render_field form.tenant %}
{% render_field form.status %} {% render_field form.status %}
{% if obj.pk %}
<div class="form-group">
<label class="col-md-3 control-label">Device</label>
<div class="col-md-9">
<p class="form-control-static">
{% if obj.interface %}
<a href="{% url 'dcim:device' pk=obj.interface.device.pk %}">{{ obj.interface.device }}</a>
<a href="{% url 'ipam:ipaddress_remove' pk=obj.pk %}" class="btn btn-xs btn-danger"><i class="glyphicon glyphicon-remove"></i> Remove</a>
{% else %}
<span class="text-muted">None</span>
{% if obj.pk %}
<a href="{% url 'ipam:ipaddress_assign' pk=obj.pk %}" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-plus"></i> Assign</a>
{% endif %}
{% endif %}
</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label">Interface</label>
<div class="col-md-9">
<p class="form-control-static">
{% if obj.interface %}
{{ obj.interface }}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</p>
</div>
</div>
{% endif %}
{% render_field form.description %} {% render_field form.description %}
</div> </div>
</div> </div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Interface Assignment</strong>
</div>
<div class="panel-body">
{% render_field form.interface_site %}
{% render_field form.interface_rack %}
{% render_field form.interface_device %}
{% render_field form.interface %}
</div>
</div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>NAT IP (Inside)</strong></div> <div class="panel-heading"><strong>NAT IP (Inside)</strong></div>
<div class="panel-body"> <div class="panel-body">

View File

@ -177,7 +177,9 @@ class ObjectEditView(GetReturnURLMixin, View):
obj = self.get_object(kwargs) obj = self.get_object(kwargs)
obj = self.alter_obj(obj, request, args, kwargs) obj = self.alter_obj(obj, request, args, kwargs)
form = self.form_class(instance=obj, initial=request.GET) # Parse initial data manually to avoid setting field values as lists
initial_data = {k: request.GET[k] for k in request.GET}
form = self.form_class(instance=obj, initial=initial_data)
return render(request, self.template_name, { return render(request, self.template_name, {
'obj': obj, 'obj': obj,