1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'develop' into v2-develop

Conflicts:
	netbox/dcim/forms.py
	netbox/dcim/views.py
	netbox/ipam/forms.py
	netbox/templates/_base.html
	netbox/utilities/views.py
This commit is contained in:
Jeremy Stretch
2017-04-13 15:42:50 -04:00
26 changed files with 290 additions and 378 deletions

View File

@ -20,7 +20,8 @@ Python 3:
```no-highlight ```no-highlight
# yum install -y epel-release # yum install -y epel-release
# yum install -y gcc python3 python3-devel python3-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel # yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel
# easy_install-3.4 pip
``` ```
Python 2: Python 2:

View File

@ -95,7 +95,7 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
model = CircuitType model = CircuitType
form_class = forms.CircuitTypeForm form_class = forms.CircuitTypeForm
def get_return_url(self, obj): def get_return_url(self, request, obj):
return reverse('circuits:circuittype_list') return reverse('circuits:circuittype_list')
@ -142,7 +142,6 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuit' permission_required = 'circuits.change_circuit'
model = Circuit model = Circuit
form_class = forms.CircuitForm form_class = forms.CircuitForm
fields_initial = ['provider']
template_name = 'circuits/circuit_edit.html' template_name = 'circuits/circuit_edit.html'
default_return_url = 'circuits:circuit_list' default_return_url = 'circuits:circuit_list'
@ -230,7 +229,6 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuittermination' permission_required = 'circuits.change_circuittermination'
model = CircuitTermination model = CircuitTermination
form_class = forms.CircuitTerminationForm form_class = forms.CircuitTerminationForm
fields_initial = ['term_side']
template_name = 'circuits/circuittermination_edit.html' template_name = 'circuits/circuittermination_edit.html'
def alter_obj(self, obj, request, url_args, url_kwargs): def alter_obj(self, obj, request, url_args, url_kwargs):
@ -238,7 +236,7 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
obj.circuit = get_object_or_404(Circuit, pk=url_kwargs['circuit']) obj.circuit = get_object_or_404(Circuit, pk=url_kwargs['circuit'])
return obj return obj
def get_return_url(self, obj): def get_return_url(self, request, obj):
return obj.circuit.get_absolute_url() return obj.circuit.get_absolute_url()

View File

@ -1422,9 +1422,16 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs) super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
# Limit LAG choices to interfaces which belong to the parent device. # Limit LAG choices to interfaces which belong to the parent device.
device = None
if self.initial.get('device'): if self.initial.get('device'):
self.fields['lag'].queryset = Interface.objects.filter( try:
device=self.initial['device'], form_factor=IFACE_FF_LAG device = Device.objects.get(pk=self.initial.get('device'))
except Device.DoesNotExist:
pass
if device is not None:
interface_ordering = device.device_type.interface_ordering
self.fields['lag'].queryset = Interface.objects.order_naturally(method=interface_ordering).filter(
device=device, form_factor=IFACE_FF_LAG
) )
else: else:
self.fields['lag'].choices = [] self.fields['lag'].choices = []
@ -1684,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.all()
self.fields['interface'].queryset = interfaces
self.fields['interface'].required = True
# If this device has only one interface, select it by default.
if 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
# #
# Inventory items # Inventory items
# #

View File

@ -121,7 +121,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'),
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),

View File

@ -13,7 +13,7 @@ from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from django.views.generic import View from django.views.generic import View
from ipam.models import Prefix, IPAddress, Service, VLAN from ipam.models import Prefix, Service, VLAN
from circuits.models import Circuit from circuits.models import Circuit
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
@ -124,13 +124,13 @@ class ComponentCreateView(View):
class ComponentEditView(ObjectEditView): class ComponentEditView(ObjectEditView):
def get_return_url(self, obj): def get_return_url(self, request, obj):
return obj.device.get_absolute_url() return obj.device.get_absolute_url()
class ComponentDeleteView(ObjectDeleteView): class ComponentDeleteView(ObjectDeleteView):
def get_return_url(self, obj): def get_return_url(self, request, obj):
return obj.device.get_absolute_url() return obj.device.get_absolute_url()
@ -149,7 +149,7 @@ class RegionEditView(PermissionRequiredMixin, ObjectEditView):
model = Region model = Region
form_class = forms.RegionForm form_class = forms.RegionForm
def get_return_url(self, obj): def get_return_url(self, request, obj):
return reverse('dcim:region_list') return reverse('dcim:region_list')
@ -242,7 +242,7 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
model = RackGroup model = RackGroup
form_class = forms.RackGroupForm form_class = forms.RackGroupForm
def get_return_url(self, obj): def get_return_url(self, request, obj):
return reverse('dcim:rackgroup_list') return reverse('dcim:rackgroup_list')
@ -268,7 +268,7 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
model = RackRole model = RackRole
form_class = forms.RackRoleForm form_class = forms.RackRoleForm
def get_return_url(self, obj): def get_return_url(self, request, obj):
return reverse('dcim:rackrole_list') return reverse('dcim:rackrole_list')
@ -379,7 +379,7 @@ class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
obj.user = request.user obj.user = request.user
return obj return obj
def get_return_url(self, obj): def get_return_url(self, request, obj):
return obj.rack.get_absolute_url() return obj.rack.get_absolute_url()
@ -387,7 +387,7 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_rackreservation' permission_required = 'dcim.delete_rackreservation'
model = RackReservation model = RackReservation
def get_return_url(self, obj): def get_return_url(self, request, obj):
return obj.rack.get_absolute_url() return obj.rack.get_absolute_url()
@ -412,7 +412,7 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
model = Manufacturer model = Manufacturer
form_class = forms.ManufacturerForm form_class = forms.ManufacturerForm
def get_return_url(self, obj): def get_return_url(self, request, obj):
return reverse('dcim:manufacturer_list') return reverse('dcim:manufacturer_list')
@ -632,7 +632,7 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
model = DeviceRole model = DeviceRole
form_class = forms.DeviceRoleForm form_class = forms.DeviceRoleForm
def get_return_url(self, obj): def get_return_url(self, request, obj):
return reverse('dcim:devicerole_list') return reverse('dcim:devicerole_list')
@ -657,7 +657,7 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
model = Platform model = Platform
form_class = forms.PlatformForm form_class = forms.PlatformForm
def get_return_url(self, obj): def get_return_url(self, request, obj):
return reverse('dcim:platform_list') return reverse('dcim:platform_list')
@ -700,19 +700,15 @@ def device(request, pk):
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
.filter(device=device, mgmt_only=False)\ .filter(device=device, mgmt_only=False)\
.select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
'circuit_termination__circuit') 'circuit_termination__circuit').prefetch_related('ip_addresses')
mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
.filter(device=device, mgmt_only=True)\ .filter(device=device, mgmt_only=True)\
.select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
'circuit_termination__circuit') 'circuit_termination__circuit').prefetch_related('ip_addresses')
device_bays = natsorted( device_bays = natsorted(
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
key=attrgetter('name') key=attrgetter('name')
) )
# Gather relevant device objects
ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface', 'vrf')\
.order_by('address')
services = Service.objects.filter(device=device) services = Service.objects.filter(device=device)
secrets = device.secrets.all() secrets = device.secrets.all()
@ -743,7 +739,6 @@ def device(request, pk):
'interfaces': interfaces, 'interfaces': interfaces,
'mgmt_interfaces': mgmt_interfaces, 'mgmt_interfaces': mgmt_interfaces,
'device_bays': device_bays, 'device_bays': device_bays,
'ip_addresses': ip_addresses,
'services': services, 'services': services,
'secrets': secrets, 'secrets': secrets,
'related_devices': related_devices, 'related_devices': related_devices,
@ -755,7 +750,6 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_device' permission_required = 'dcim.change_device'
model = Device model = Device
form_class = forms.DeviceForm form_class = forms.DeviceForm
fields_initial = ['site', 'rack', 'position', 'face', 'device_bay']
template_name = 'dcim/device_edit.html' template_name = 'dcim/device_edit.html'
default_return_url = 'dcim:device_list' default_return_url = 'dcim:device_list'
@ -1567,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)
return render(request, 'dcim/ipaddress_assign.html', {
'device': device,
'form': form,
'return_url': reverse('dcim:device', kwargs={'pk': device.pk}),
})
# #
# Inventory items # Inventory items
# #

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 (
@ -210,28 +210,33 @@ class PrefixFromCSVForm(forms.ModelForm):
site = self.cleaned_data.get('site') site = self.cleaned_data.get('site')
vlan_group_name = self.cleaned_data.get('vlan_group_name') vlan_group_name = self.cleaned_data.get('vlan_group_name')
vlan_vid = self.cleaned_data.get('vlan_vid') vlan_vid = self.cleaned_data.get('vlan_vid')
# Validate VLAN
vlan_group = None vlan_group = None
vlan = None
# Validate VLAN group
if vlan_group_name: if vlan_group_name:
try: try:
vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name) vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
except VLANGroup.DoesNotExist: except VLANGroup.DoesNotExist:
self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name)) if site:
if vlan_vid and vlan_group: self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
else:
self.add_error('vlan_group_name', "Invalid global VLAN group ({}).".format(vlan_group_name))
# Validate VLAN
if vlan_vid:
try: try:
self.instance.vlan = VLAN.objects.get(group=vlan_group, vid=vlan_vid) self.instance.vlan = VLAN.objects.get(site=site, group=vlan_group, vid=vlan_vid)
except VLAN.DoesNotExist: except VLAN.DoesNotExist:
self.add_error('vlan_vid', "Invalid VLAN ID ({} - {}).".format(vlan_group, vlan_vid)) if site:
elif vlan_vid and site: self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site))
try: elif vlan_group:
self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid) self.add_error('vlan_vid', "Invalid VLAN ID ({}) for group {}.".format(vlan_vid, vlan_group_name))
except VLAN.DoesNotExist: elif not vlan_group_name:
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site)) self.add_error('vlan_vid', "Invalid global VLAN ID ({}).".format(vlan_vid))
except VLAN.MultipleObjectsReturned: except VLAN.MultipleObjectsReturned:
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid)) self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
elif vlan_vid: self.instance.vlan = vlan
self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.")
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -302,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/interfaces/?device_id={{interface_device}}'),
'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')
} }
@ -325,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:
@ -340,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'])
@ -350,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

@ -58,8 +58,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

@ -244,7 +244,7 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
model = RIR model = RIR
form_class = forms.RIRForm form_class = forms.RIRForm
def get_return_url(self, obj): def get_return_url(self, request, obj):
return reverse('ipam:rir_list') return reverse('ipam:rir_list')
@ -370,7 +370,7 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
model = Role model = Role
form_class = forms.RoleForm form_class = forms.RoleForm
def get_return_url(self, obj): def get_return_url(self, request, obj):
return reverse('ipam:role_list') return reverse('ipam:role_list')
@ -464,7 +464,6 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
model = Prefix model = Prefix
form_class = forms.PrefixForm form_class = forms.PrefixForm
template_name = 'ipam/prefix_edit.html' template_name = 'ipam/prefix_edit.html'
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
default_return_url = 'ipam:prefix_list' default_return_url = 'ipam:prefix_list'
@ -572,80 +571,10 @@ 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
form_class = forms.IPAddressForm form_class = forms.IPAddressForm
fields_initial = ['address', 'vrf']
template_name = 'ipam/ipaddress_edit.html' template_name = 'ipam/ipaddress_edit.html'
default_return_url = 'ipam:ipaddress_list' default_return_url = 'ipam:ipaddress_list'
@ -718,7 +647,7 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
model = VLANGroup model = VLANGroup
form_class = forms.VLANGroupForm form_class = forms.VLANGroupForm
def get_return_url(self, obj): def get_return_url(self, request, obj):
return reverse('ipam:vlangroup_list') return reverse('ipam:vlangroup_list')
@ -807,7 +736,7 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
obj.device = get_object_or_404(Device, pk=url_kwargs['device']) obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
return obj return obj
def get_return_url(self, obj): def get_return_url(self, request, obj):
return obj.device.get_absolute_url() return obj.device.get_absolute_url()

View File

@ -316,6 +316,16 @@ li.occupied + li.available {
border-top: 1px solid #474747; border-top: 1px solid #474747;
} }
/* Devices */
table.component-list tr.ipaddress td {
background-color: #eeffff;
padding-bottom: 4px;
padding-top: 4px;
}
table.component-list tr.ipaddress:hover td {
background-color: #e6f7f7;
}
/* Misc */ /* Misc */
.banner-bottom { .banner-bottom {
margin-bottom: 50px; margin-bottom: 50px;

View File

@ -42,7 +42,7 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
model = SecretRole model = SecretRole
form_class = forms.SecretRoleForm form_class = forms.SecretRoleForm
def get_return_url(self, obj): def get_return_url(self, request, obj):
return reverse('secrets:secretrole_list') return reverse('secrets:secretrole_list')

View File

@ -274,10 +274,10 @@
</div> </div>
</div> </div>
</nav> </nav>
<div class="container wrapper"> <div class="container wrapper">
{% if settings.BANNER_TOP %} {% if settings.BANNER_TOP %}
<div class="alert alert-info text-center" role="alert"> <div class="alert alert-info text-center" role="alert">
{{ settings.BANNER_TOP|safe }} {{ settings.BANNER_TOP|safe }}
</div> </div>
{% endif %} {% endif %}
{% if settings.MAINTENANCE_MODE %} {% if settings.MAINTENANCE_MODE %}
@ -286,24 +286,24 @@
<p>NetBox is currently in maintenance mode. Functionality may be limited.</p> <p>NetBox is currently in maintenance mode. Functionality may be limited.</p>
</div> </div>
{% endif %} {% endif %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissable" role="alert"> <div class="alert alert-{{ message.tags }} alert-dismissable" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
{{ message|safe }} {{ message }}
</div> </div>
{% endfor %} {% endfor %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
<div class="push"></div> <div class="push"></div>
{% if settings.BANNER_BOTTOM %} {% if settings.BANNER_BOTTOM %}
<div class="alert alert-info text-center banner-bottom" role="alert"> <div class="alert alert-info text-center banner-bottom" role="alert">
{{ settings.BANNER_BOTTOM|safe }} {{ settings.BANNER_BOTTOM|safe }}
</div> </div>
{% endif %} {% endif %}
</div> </div>
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-xs-4"> <div class="col-xs-4">
<p class="text-muted">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</p> <p class="text-muted">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</p>
@ -320,8 +320,8 @@
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</footer> </footer>
<script type="text/javascript"> <script type="text/javascript">
var netbox_api_path = "/{{ settings.BASE_PATH }}api/"; var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
</script> </script>

View File

@ -194,35 +194,6 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>IP Addresses</strong>
</div>
{% if ip_addresses %}
<table class="table table-hover panel-body">
{% for ip in ip_addresses %}
{% include 'dcim/inc/ipaddress.html' %}
{% endfor %}
</table>
{% elif interfaces or mgmt_interfaces %}
<div class="panel-body text-muted">
None assigned
</div>
{% else %}
<div class="panel-body">
<a href="{% url 'dcim:interface_add' pk=device.pk %}">Create an interface</a> to assign an IP.
</div>
{% endif %}
{% if perms.ipam.add_ipaddress %}
{% if interfaces or mgmt_interfaces %}
<div class="panel-footer text-right">
<a href="{% url 'dcim:ipaddress_assign' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign IP address
</a>
</div>
{% endif %}
{% endif %}
</div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Services</strong> <strong>Services</strong>
@ -250,7 +221,7 @@
<div class="panel-heading"> <div class="panel-heading">
<strong>Critical Connections</strong> <strong>Critical Connections</strong>
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body component-list">
{% for iface in mgmt_interfaces %} {% for iface in mgmt_interfaces %}
{% include 'dcim/inc/interface.html' with icon='wrench' %} {% include 'dcim/inc/interface.html' with icon='wrench' %}
{% empty %} {% empty %}
@ -389,7 +360,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body component-list">
{% for devicebay in device_bays %} {% for devicebay in device_bays %}
{% include 'dcim/inc/devicebay.html' with selectable=True %} {% include 'dcim/inc/devicebay.html' with selectable=True %}
{% empty %} {% empty %}
@ -430,6 +401,9 @@
<div class="panel-heading"> <div class="panel-heading">
<strong>Interfaces</strong> <strong>Interfaces</strong>
<div class="pull-right"> <div class="pull-right">
<button class="btn btn-default btn-xs toggle-ips" selected="selected">
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
</button>
{% if perms.dcim.change_interface and interfaces|length > 1 %} {% if perms.dcim.change_interface and interfaces|length > 1 %}
<button class="btn btn-default btn-xs toggle"> <button class="btn btn-default btn-xs toggle">
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all <span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
@ -442,7 +416,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<table class="table table-hover panel-body"> <table id="interfaces_table" class="table table-hover panel-body component-list">
{% for iface in interfaces %} {% for iface in interfaces %}
{% include 'dcim/inc/interface.html' with selectable=True %} {% include 'dcim/inc/interface.html' with selectable=True %}
{% empty %} {% empty %}
@ -499,7 +473,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body component-list">
{% for csp in cs_ports %} {% for csp in cs_ports %}
{% include 'dcim/inc/consoleserverport.html' with selectable=True %} {% include 'dcim/inc/consoleserverport.html' with selectable=True %}
{% empty %} {% empty %}
@ -551,7 +525,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body component-list">
{% for po in power_outlets %} {% for po in power_outlets %}
{% include 'dcim/inc/poweroutlet.html' with selectable=True %} {% include 'dcim/inc/poweroutlet.html' with selectable=True %}
{% empty %} {% empty %}
@ -642,6 +616,18 @@ $(".powerport-toggle").click(function() {
$(".interface-toggle").click(function() { $(".interface-toggle").click(function() {
return toggleConnection($(this), "dcim/interface-connections/"); return toggleConnection($(this), "dcim/interface-connections/");
}); });
// Toggle the display of IP addresses under interfaces
$('button.toggle-ips').click(function() {
var selected = $(this).attr('selected');
if (selected) {
$('#interfaces_table tr.ipaddress').hide();
} else {
$('#interfaces_table tr.ipaddress').show();
}
$(this).attr('selected', !selected);
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
return false;
});
</script> </script>
<script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script> <script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
<script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script> <script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>

View File

@ -1,4 +1,4 @@
<tr{% if cp.cs_port and not cp.connection_status %} class="info"{% endif %}> <tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_consoleport or perms.dcim.delete_consoleport %} {% if selectable and perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ cp.pk }}" /> <input name="pk" type="checkbox" value="{{ cp.pk }}" />
@ -7,7 +7,6 @@
<td> <td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }} <i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
</td> </td>
<td></td>
{% if cp.cs_port %} {% if cp.cs_port %}
<td> <td>
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a> <a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
@ -20,7 +19,7 @@
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td class="text-right"> <td colspan="2" class="text-right">
{% if perms.dcim.change_consoleport %} {% if perms.dcim.change_consoleport %}
{% if cp.cs_port %} {% if cp.cs_port %}
{% if cp.connection_status %} {% if cp.connection_status %}

View File

@ -1,4 +1,4 @@
<tr{% if csp.connected_console and not csp.connected_console.connection_status %} class="info"{% endif %}> <tr class="consoleserverport{% if csp.connected_console and not csp.connected_console.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %} {% if selectable and perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ csp.pk }}" /> <input name="pk" type="checkbox" value="{{ csp.pk }}" />
@ -19,7 +19,7 @@
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td class="text-right"> <td colspan="2" class="text-right">
{% if perms.dcim.change_consoleserverport %} {% if perms.dcim.change_consoleserverport %}
{% if csp.connected_console %} {% if csp.connected_console %}
{% if csp.connected_console.connection_status %} {% if csp.connected_console.connection_status %}

View File

@ -1,4 +1,4 @@
<tr> <tr class="devicebay">
{% if selectable and perms.dcim.change_devicebay or perms.dcim.delete_devicebay %} {% if selectable and perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" /> <input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
@ -19,7 +19,7 @@
<span class="text-muted">Vacant</span> <span class="text-muted">Vacant</span>
</td> </td>
{% endif %} {% endif %}
<td class="text-right"> <td colspan="2" class="text-right">
{% if perms.dcim.change_devicebay %} {% if perms.dcim.change_devicebay %}
{% if devicebay.installed_device %} {% if devicebay.installed_device %}
<a href="{% url 'dcim:devicebay_depopulate' pk=devicebay.pk %}" class="btn btn-danger btn-xs"> <a href="{% url 'dcim:devicebay_depopulate' pk=devicebay.pk %}" class="btn btn-danger btn-xs">

View File

@ -1,4 +1,4 @@
<tr{% if iface.connection and not iface.connection.connection_status %} class="info"{% endif %}> <tr class="interface{% if iface.connection and not iface.connection.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ iface.pk }}" /> <input name="pk" type="checkbox" value="{{ iface.pk }}" />
@ -16,10 +16,9 @@
<br /><small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small> <br /><small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
{% endif %} {% endif %}
</td> </td>
<td> {% if iface.is_lag %}
<small>{{ iface.mac_address|default:'' }}</small> <td colspan="2" class="text-muted">LAG interface</td>
</td> {% elif iface.is_virtual %}
{% if iface.is_virtual %}
<td colspan="2" class="text-muted">Virtual interface</td> <td colspan="2" class="text-muted">Virtual interface</td>
{% elif iface.connection %} {% elif iface.connection %}
{% with iface.connected_interface as connected_iface %} {% with iface.connected_interface as connected_iface %}
@ -51,7 +50,7 @@
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td class="text-right"> <td colspan="2" class="text-right">
{% if show_graphs %} {% if show_graphs %}
{% if iface.circuit_termination or iface.connection %} {% if iface.circuit_termination or iface.connection %}
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs"> <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
@ -59,6 +58,11 @@
</button> </button>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if perms.ipam.add_ipaddress %}
<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>
</a>
{% endif %}
{% if perms.dcim.change_interface %} {% if perms.dcim.change_interface %}
{% if not iface.is_virtual %} {% if not iface.is_virtual %}
{% if iface.connection %} {% if iface.connection %}
@ -71,19 +75,19 @@
<i class="fa fa-plug" aria-hidden="true"></i> <i class="fa fa-plug" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Delete connection"> <a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Disconnect">
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a> </a>
{% elif iface.circuit_termination and perms.circuits.change_circuittermination %} {% elif iface.circuit_termination and perms.circuits.change_circuittermination %}
<button class="btn btn-warning btn-xs interface-toggle connected" disabled="disabled" title="Circuits cannot be marked as planned or connected"> <button class="btn btn-warning btn-xs interface-toggle connected" disabled="disabled" title="Circuits cannot be marked as planned or connected">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i> <i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</button> </button>
<a href="{% url 'circuits:circuittermination_edit' pk=iface.circuit_termination.pk %}" class="btn btn-danger btn-xs" title="Edit circuit termination"> <a href="{% url 'circuits:circuittermination_edit' pk=iface.circuit_termination.pk %}" class="btn btn-danger btn-xs" title="Edit circuit termination">
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a> </a>
{% else %} {% else %}
<a href="{% url 'dcim:interfaceconnection_add' pk=device.pk %}?interface_a={{ iface.pk }}" class="btn btn-success btn-xs" title="Connect"> <a href="{% url 'dcim:interfaceconnection_add' pk=device.pk %}?interface_a={{ iface.pk }}" class="btn btn-success btn-xs" title="Connect">
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i> <i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
{% endif %} {% endif %}
@ -104,3 +108,41 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% for ip in iface.ip_addresses.all %}
<tr class="ipaddress">
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
<td></td>
{% endif %}
<td colspan="2">
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
{% if ip.description %}
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
{% endif %}
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
<span class="label label-success">Primary</span>
{% endif %}
</td>
<td class="text-right">
{% if ip.vrf %}
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
{% else %}
<span class="text-muted">Global</span>
{% endif %}
</td>
<td>
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
</td>
<td class="text-right">
{% if perms.ipam.edit_ipaddress %}
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
</a>
{% endif %}
{% if perms.ipam.delete_ipaddress %}
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@ -1,21 +0,0 @@
<tr>
<td>
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
</td>
<td>
{{ ip.vrf|default:"Global" }}
</td>
<td>{{ ip.interface }}</td>
<td>
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
<span class="label label-success">Primary</span>
{% endif %}
</td>
<td class="text-right">
{% if perms.ipam.delete_ipaddress %}
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
</a>
{% endif %}
</td>
</tr>

View File

@ -1,4 +1,4 @@
<tr{% if po.connected_port and not po.connected_port.connection_status %} class="info"{% endif %}> <tr class="poweroutlet{% if po.connected_port and not po.connected_port.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %} {% if selectable and perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ po.pk }}" /> <input name="pk" type="checkbox" value="{{ po.pk }}" />
@ -19,7 +19,7 @@
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td class="text-right"> <td colspan="2" class="text-right">
{% if perms.dcim.change_poweroutlet %} {% if perms.dcim.change_poweroutlet %}
{% if po.connected_port %} {% if po.connected_port %}
{% if po.connected_port.connection_status %} {% if po.connected_port.connection_status %}

View File

@ -1,4 +1,4 @@
<tr{% if pp.power_outlet and not pp.connection_status %} class="info"{% endif %}> <tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_powerport or perms.dcim.delete_powerport %} {% if selectable and perms.dcim.change_powerport or perms.dcim.delete_powerport %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ pp.pk }}" /> <input name="pk" type="checkbox" value="{{ pp.pk }}" />
@ -7,7 +7,6 @@
<td> <td>
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }} <i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
</td> </td>
<td></td>
{% if pp.power_outlet %} {% if pp.power_outlet %}
<td> <td>
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a> <a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
@ -20,7 +19,7 @@
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td class="text-right"> <td colspan="2" class="text-right">
{% if perms.dcim.change_powerport %} {% if perms.dcim.change_powerport %}
{% if pp.power_outlet %} {% if pp.power_outlet %}
{% if pp.connection_status %} {% if pp.connection_status %}

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

@ -6,13 +6,13 @@
<div class="col-md-6 col-md-offset-3"> <div class="col-md-6 col-md-offset-3">
<form action="." method="post" class="form"> <form action="." method="post" class="form">
{% csrf_token %} {% csrf_token %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<div class="panel panel-{{ panel_class|default:"danger" }}"> <div class="panel panel-{{ panel_class|default:"danger" }}">
<div class="panel-heading">{% block title %}{% endblock %}</div> <div class="panel-heading">{% block title %}{% endblock %}</div>
<div class="panel-body"> <div class="panel-body">
{% block message %}<p>Are you sure?</p>{% endblock %} {% block message %}<p>Are you sure?</p>{% endblock %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<div class="form-group"> <div class="form-group">
<div class="checkbox{% if form.confirm.errors %} has-error{% endif %}"> <div class="checkbox{% if form.confirm.errors %} has-error{% endif %}">
<label for="{{ form.confirm.id_for_label }}"> <label for="{{ form.confirm.id_for_label }}">

View File

@ -29,7 +29,7 @@ class TenantGroupEditView(PermissionRequiredMixin, ObjectEditView):
model = TenantGroup model = TenantGroup
form_class = forms.TenantGroupForm form_class = forms.TenantGroupForm
def get_return_url(self, obj): def get_return_url(self, request, obj):
return reverse('tenancy:tenantgroup_list') return reverse('tenancy:tenantgroup_list')
@ -81,7 +81,6 @@ class TenantEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'tenancy.change_tenant' permission_required = 'tenancy.change_tenant'
model = Tenant model = Tenant
form_class = forms.TenantForm form_class = forms.TenantForm
fields_initial = ['group']
template_name = 'tenancy/tenant_edit.html' template_name = 'tenancy/tenant_edit.html'
default_return_url = 'tenancy:tenant_list' default_return_url = 'tenancy:tenant_list'

View File

@ -398,13 +398,18 @@ class BootstrapMixin(forms.BaseForm):
field.widget.attrs['placeholder'] = field.label field.widget.attrs['placeholder'] = field.label
class ConfirmationForm(BootstrapMixin, forms.Form): class ReturnURLForm(forms.Form):
""" """
A generic confirmation form. The form is not valid unless the confirm field is checked. An optional return_url can Provides a hidden return URL field to control where the user is directed after the form is submitted.
be specified to direct the user to a specific URL after the action has been taken. """
return_url = forms.CharField(required=False, widget=forms.HiddenInput())
class ConfirmationForm(BootstrapMixin, ReturnURLForm):
"""
A generic confirmation form. The form is not valid unless the confirm field is checked.
""" """
confirm = forms.BooleanField(required=True) confirm = forms.BooleanField(required=True)
return_url = forms.CharField(required=False, widget=forms.HiddenInput())
class BulkEditForm(forms.Form): class BulkEditForm(forms.Form):

View File

@ -12,7 +12,9 @@ from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.template import TemplateSyntaxError from django.template import TemplateSyntaxError
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape
from django.utils.http import is_safe_url from django.utils.http import is_safe_url
from django.utils.safestring import mark_safe
from django.views.generic import View from django.views.generic import View
from extras.forms import CustomFieldForm from extras.forms import CustomFieldForm
@ -39,6 +41,23 @@ class CustomFieldQueryset:
yield obj yield obj
class GetReturnURLMixin(object):
"""
Provides logic for determining where a user should be redirected after processing a form.
"""
default_return_url = None
def get_return_url(self, request, obj):
query_param = request.GET.get('return_url')
if query_param and is_safe_url(url=query_param, host=request.get_host()):
return query_param
elif obj.pk and hasattr(obj, 'get_absolute_url'):
return obj.get_absolute_url()
elif self.default_return_url is not None:
return reverse(self.default_return_url)
return reverse('home')
class ObjectListView(View): class ObjectListView(View):
""" """
List a series of objects. List a series of objects.
@ -128,21 +147,18 @@ class ObjectListView(View):
return {} return {}
class ObjectEditView(View): class ObjectEditView(GetReturnURLMixin, View):
""" """
Create or edit a single object. Create or edit a single object.
model: The model of the object being edited model: The model of the object being edited
form_class: The form used to create or edit the object form_class: The form used to create or edit the object
fields_initial: A set of fields that will be prepopulated in the form from the request parameters
template_name: The name of the template template_name: The name of the template
default_return_url: The name of the URL used to display a list of this object type default_return_url: The name of the URL used to display a list of this object type
""" """
model = None model = None
form_class = None form_class = None
fields_initial = []
template_name = 'utilities/obj_edit.html' template_name = 'utilities/obj_edit.html'
default_return_url = 'home'
def get_object(self, kwargs): def get_object(self, kwargs):
# Look up object by slug or PK. Return None if neither was provided. # Look up object by slug or PK. Return None if neither was provided.
@ -157,24 +173,19 @@ class ObjectEditView(View):
# given some parameter from the request URL. # given some parameter from the request URL.
return obj return obj
def get_return_url(self, obj):
# Determine where to redirect the user after updating an object (or aborting an update).
if obj.pk and hasattr(obj, 'get_absolute_url'):
return obj.get_absolute_url()
return reverse(self.default_return_url)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
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)
initial_data = {k: request.GET[k] for k in self.fields_initial if k in 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) 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,
'obj_type': self.model._meta.verbose_name, 'obj_type': self.model._meta.verbose_name,
'form': form, 'form': form,
'return_url': self.get_return_url(obj), 'return_url': self.get_return_url(request, obj),
}) })
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -194,10 +205,10 @@ class ObjectEditView(View):
msg = u'Created ' if obj_created else u'Modified ' msg = u'Created ' if obj_created else u'Modified '
msg += self.model._meta.verbose_name msg += self.model._meta.verbose_name
if hasattr(obj, 'get_absolute_url'): if hasattr(obj, 'get_absolute_url'):
msg = u'{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), obj) msg = u'{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
else: else:
msg = u'{} {}'.format(msg, obj) msg = u'{} {}'.format(msg, escape(obj))
messages.success(request, msg) messages.success(request, mark_safe(msg))
if obj_created: if obj_created:
UserAction.objects.log_create(request.user, obj, msg) UserAction.objects.log_create(request.user, obj, msg)
else: else:
@ -205,17 +216,22 @@ class ObjectEditView(View):
if '_addanother' in request.POST: if '_addanother' in request.POST:
return redirect(request.path) return redirect(request.path)
return redirect(self.get_return_url(obj))
return_url = form.cleaned_data.get('return_url')
if return_url is not None and is_safe_url(url=return_url, host=request.get_host()):
return redirect(return_url)
else:
return redirect(self.get_return_url(request, obj))
return render(request, self.template_name, { return render(request, self.template_name, {
'obj': obj, 'obj': obj,
'obj_type': self.model._meta.verbose_name, 'obj_type': self.model._meta.verbose_name,
'form': form, 'form': form,
'return_url': self.get_return_url(obj), 'return_url': self.get_return_url(request, obj),
}) })
class ObjectDeleteView(View): class ObjectDeleteView(GetReturnURLMixin, View):
""" """
Delete a single object. Delete a single object.
@ -225,7 +241,6 @@ class ObjectDeleteView(View):
""" """
model = None model = None
template_name = 'utilities/obj_delete.html' template_name = 'utilities/obj_delete.html'
default_return_url = 'home'
def get_object(self, kwargs): def get_object(self, kwargs):
# Look up object by slug if one has been provided. Otherwise, use PK. # Look up object by slug if one has been provided. Otherwise, use PK.
@ -234,24 +249,16 @@ class ObjectDeleteView(View):
else: else:
return get_object_or_404(self.model, pk=kwargs['pk']) return get_object_or_404(self.model, pk=kwargs['pk'])
def get_return_url(self, obj):
if obj.pk and hasattr(obj, 'get_absolute_url'):
return obj.get_absolute_url()
return reverse(self.default_return_url)
def get(self, request, **kwargs): def get(self, request, **kwargs):
obj = self.get_object(kwargs) obj = self.get_object(kwargs)
initial_data = { form = ConfirmationForm(initial=request.GET)
'return_url': request.GET.get('return_url'),
}
form = ConfirmationForm(initial=initial_data)
return render(request, self.template_name, { return render(request, self.template_name, {
'obj': obj, 'obj': obj,
'form': form, 'form': form,
'obj_type': self.model._meta.verbose_name, 'obj_type': self.model._meta.verbose_name,
'return_url': request.GET.get('return_url') or self.get_return_url(obj), 'return_url': self.get_return_url(request, obj),
}) })
def post(self, request, **kwargs): def post(self, request, **kwargs):
@ -270,17 +277,17 @@ class ObjectDeleteView(View):
messages.success(request, msg) messages.success(request, msg)
UserAction.objects.log_delete(request.user, obj, msg) UserAction.objects.log_delete(request.user, obj, msg)
return_url = form.cleaned_data['return_url'] return_url = form.cleaned_data.get('return_url')
if return_url and is_safe_url(url=return_url, host=request.get_host()): if return_url is not None and is_safe_url(url=return_url, host=request.get_host()):
return redirect(return_url) return redirect(return_url)
else: else:
return redirect(self.get_return_url(obj)) return redirect(self.get_return_url(request, obj))
return render(request, self.template_name, { return render(request, self.template_name, {
'obj': obj, 'obj': obj,
'form': form, 'form': form,
'obj_type': self.model._meta.verbose_name, 'obj_type': self.model._meta.verbose_name,
'return_url': request.GET.get('return_url') or self.get_return_url(obj), 'return_url': self.get_return_url(request, obj),
}) })

View File

@ -11,7 +11,7 @@ djangorestframework>=3.6.2
graphviz>=0.6 graphviz>=0.6
Markdown>=2.6.7 Markdown>=2.6.7
natsort>=5.0.0 natsort>=5.0.0
ncclient==0.5.2 ncclient==0.5.3
netaddr==0.7.18 netaddr==0.7.18
paramiko>=2.0.0 paramiko>=2.0.0
Pillow>=4.0.0 Pillow>=4.0.0