Merge pull request #1094 from digitalocean/develop

Release v1.9.6
This commit is contained in:
Jeremy Stretch 2017-04-21 14:52:53 -04:00 committed by GitHub
commit 17873706b7
33 changed files with 376 additions and 417 deletions

View File

@ -6,6 +6,7 @@ Python 3:
```no-highlight ```no-highlight
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev # apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev
# update-alternatives --install /usr/bin/python python /usr/bin/python3 1
``` ```
Python 2: Python 2:
@ -20,7 +21,9 @@ 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
# ln -s -f python3.4 /usr/bin/python
``` ```
Python 2: Python 2:
@ -83,6 +86,14 @@ Checking connectivity... done.
Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.) Install the required Python packages using pip. (If you encounter any compilation errors during this step, ensure that you've installed all of the system dependencies listed above.)
Python 3:
```no-highlight
# pip3 install -r requirements.txt
```
Python 2:
```no-highlight ```no-highlight
# pip install -r requirements.txt # pip install -r requirements.txt
``` ```
@ -172,7 +183,7 @@ Superuser created successfully.
# Collect Static Files # Collect Static Files
```no-highlight ```no-highlight
# ./manage.py collectstatic # ./manage.py collectstatic --no-input
You have requested to collect static files at the destination You have requested to collect static files at the destination
location as specified in your settings: location as specified in your settings:

View File

@ -5,13 +5,14 @@ NetBox requires a PostgreSQL database to store data. (Please note that MySQL is
**Debian/Ubuntu** **Debian/Ubuntu**
```no-highlight ```no-highlight
# apt-get install -y postgresql libpq-dev python-psycopg2 # apt-get update
# apt-get install -y postgresql libpq-dev
``` ```
**CentOS/RHEL** **CentOS/RHEL**
```no-highlight ```no-highlight
# yum install -y postgresql postgresql-server postgresql-devel python-psycopg2 # yum install -y postgresql postgresql-server postgresql-devel
# postgresql-setup initdb # postgresql-setup initdb
``` ```

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-04-19 17:17
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('circuits', '0007_circuit_add_description'),
]
operations = [
migrations.AlterField(
model_name='circuittermination',
name='interface',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='circuit_termination', to='dcim.Interface'),
),
]

View File

@ -150,10 +150,14 @@ class CircuitTermination(models.Model):
circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE) circuit = models.ForeignKey('Circuit', related_name='terminations', on_delete=models.CASCADE)
term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination') term_side = models.CharField(max_length=1, choices=TERM_SIDE_CHOICES, verbose_name='Termination')
site = models.ForeignKey('dcim.Site', related_name='circuit_terminations', on_delete=models.PROTECT) site = models.ForeignKey('dcim.Site', related_name='circuit_terminations', on_delete=models.PROTECT)
interface = models.OneToOneField('dcim.Interface', related_name='circuit_termination', blank=True, null=True) interface = models.OneToOneField(
'dcim.Interface', related_name='circuit_termination', blank=True, null=True, on_delete=models.PROTECT
)
port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)') port_speed = models.PositiveIntegerField(verbose_name='Port speed (Kbps)')
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)', upstream_speed = models.PositiveIntegerField(
help_text='Upstream speed, if different from port speed') blank=True, null=True, verbose_name='Upstream speed (Kbps)',
help_text='Upstream speed, if different from port speed'
)
xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID') xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)') pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')

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
# #
# 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

@ -13,7 +13,7 @@ from django.shortcuts import get_object_or_404, redirect, render
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}),
})
# #
# Modules # Modules
# #

View File

@ -1,8 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# This script will generate a random 50-character string suitable for use as a SECRET_KEY. # This script will generate a random 50-character string suitable for use as a SECRET_KEY.
import os
import random import random
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)' charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
random.seed = (os.urandom(2048)) secure_random = random.SystemRandom()
print(''.join(random.choice(charset) for c in range(50))) print(''.join(secure_random.sample(charset, 50)))

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/{{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')
} }
@ -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(
@ -362,12 +418,15 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
self.fields['nat_inside'].choices = [] self.fields['nat_inside'].choices = []
class IPAddressBulkAddForm(BootstrapMixin, forms.Form): class IPAddressBulkAddForm(BootstrapMixin, CustomFieldForm):
address = ExpandableIPAddressField() address_pattern = ExpandableIPAddressField(label='Address Pattern')
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global') vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF', empty_label='Global')
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
status = forms.ChoiceField(choices=IPADDRESS_STATUS_CHOICES) pattern_map = ('address_pattern', 'address')
description = forms.CharField(max_length=100, required=False)
class Meta:
model = IPAddress
fields = ['address_pattern', 'vrf', 'tenant', 'status', 'description']
class IPAddressAssignForm(BootstrapMixin, forms.Form): class IPAddressAssignForm(BootstrapMixin, forms.Form):

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

@ -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'
@ -659,7 +588,7 @@ class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView): class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
permission_required = 'ipam.add_ipaddress' permission_required = 'ipam.add_ipaddress'
form = forms.IPAddressBulkAddForm form = forms.IPAddressBulkAddForm
model = IPAddress model_form = forms.IPAddressForm
template_name = 'ipam/ipaddress_bulk_add.html' template_name = 'ipam/ipaddress_bulk_add.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

@ -12,7 +12,7 @@ except ImportError:
"the documentation.") "the documentation.")
VERSION = '1.9.5' VERSION = '1.9.6'
# Import local configuration # Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:

View File

@ -313,6 +313,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

@ -30,7 +30,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

@ -3,11 +3,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>NetBox - {% block title %}Home{% endblock %}</title> <title>NetBox - {% block title %}Home{% endblock %}</title>
<link rel="stylesheet" href="{% static 'bootstrap-3.3.6-dist/css/bootstrap.min.css' %}"> <link rel="stylesheet" href="{% static 'bootstrap-3.3.6-dist/css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'font-awesome-4.6.3/css/font-awesome.min.css' %}"> <link rel="stylesheet" href="{% static 'font-awesome-4.6.3/css/font-awesome.min.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.11.4/jquery-ui.css' %}"> <link rel="stylesheet" href="{% static 'jquery-ui-1.11.4/jquery-ui.css' %}">
<link rel="stylesheet" href="{% static 'css/base.css' %}"> <link rel="stylesheet" href="{% static 'css/base.css' %}">
<link rel="icon" type="image/png" href="{% static 'img/netbox.ico' %}" /> <link rel="icon" type="image/png" href="{% static 'img/netbox.ico' %}" />
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width"> <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
</head> </head>
@ -256,10 +256,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 %}
@ -268,24 +268,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>
@ -302,8 +302,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 %}
@ -375,7 +346,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 %}
@ -416,6 +387,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
@ -428,7 +402,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 %}
@ -485,7 +459,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 %}
@ -537,7 +511,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 %}
@ -628,6 +602,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' %}"></script> <script src="{% static 'js/graphs.js' %}"></script>
<script src="{% static 'js/secrets.js' %}"></script> <script src="{% static 'js/secrets.js' %}"></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

@ -10,13 +10,21 @@
{% block form %} {% block form %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>IP Address</strong></div> <div class="panel-heading"><strong>IP Addresses</strong></div>
<div class="panel-body"> <div class="panel-body">
{% render_field form.address %} {% render_field form.address_pattern %}
{% render_field form.vrf %} {% render_field form.vrf %}
{% render_field form.tenant %} {% render_field form.tenant %}
{% render_field form.status %} {% render_field form.status %}
{% render_field form.description %} {% render_field form.description %}
</div> </div>
</div> </div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

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

@ -437,13 +437,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.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInpu
from django.http import HttpResponse 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.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),
}) })
@ -289,12 +296,12 @@ class BulkAddView(View):
Create new objects in bulk. Create new objects in bulk.
form: Form class form: Form class
model: The model of the objects being created model_form: The ModelForm used to create individual objects
template_name: The name of the template template_name: The name of the template
default_return_url: Name of the URL to which the user is redirected after creating the objects default_return_url: Name of the URL to which the user is redirected after creating the objects
""" """
form = None form = None
model = None model_form = None
template_name = None template_name = None
default_return_url = 'home' default_return_url = 'home'
@ -303,47 +310,44 @@ class BulkAddView(View):
form = self.form() form = self.form()
return render(request, self.template_name, { return render(request, self.template_name, {
'obj_type': self.model._meta.verbose_name, 'obj_type': self.model_form._meta.model._meta.verbose_name,
'form': form, 'form': form,
'return_url': reverse(self.default_return_url), 'return_url': reverse(self.default_return_url),
}) })
def post(self, request): def post(self, request):
model = self.model_form._meta.model
form = self.form(request.POST) form = self.form(request.POST)
if form.is_valid(): if form.is_valid():
# The first field will be used as the pattern # Read the pattern field and target from the form's pattern_map
field_names = list(form.fields.keys()) pattern_field, pattern_target = form.pattern_map
pattern_field = field_names[0]
pattern = form.cleaned_data[pattern_field] pattern = form.cleaned_data[pattern_field]
model_form_data = form.cleaned_data
# All other fields will be copied as object attributes
kwargs = {k: form.cleaned_data[k] for k in field_names[1:]}
new_objs = [] new_objs = []
try: try:
with transaction.atomic(): with transaction.atomic():
for value in pattern: for value in pattern:
obj = self.model(**kwargs) model_form_data[pattern_target] = value
setattr(obj, pattern_field, value) model_form = self.model_form(model_form_data)
obj.full_clean() obj = model_form.save()
obj.save()
new_objs.append(obj) new_objs.append(obj)
except ValidationError as e: except ValidationError as e:
form.add_error(None, e) form.add_error(None, e)
if not form.errors: if not form.errors:
msg = u"Added {} {}".format(len(new_objs), self.model._meta.verbose_name_plural) msg = u"Added {} {}".format(len(new_objs), model._meta.verbose_name_plural)
messages.success(request, msg) messages.success(request, msg)
UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(self.model), msg) UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(model), msg)
if '_addanother' in request.POST: if '_addanother' in request.POST:
return redirect(request.path) return redirect(request.path)
return redirect(self.default_return_url) return redirect(self.default_return_url)
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
'obj_type': self.model._meta.verbose_name, 'obj_type': model._meta.verbose_name,
'return_url': reverse(self.default_return_url), 'return_url': reverse(self.default_return_url),
}) })

View File

@ -10,7 +10,7 @@ djangorestframework>=3.5.0
graphviz>=0.4.10 graphviz>=0.4.10
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
psycopg2>=2.6.1 psycopg2>=2.6.1

View File

@ -15,8 +15,12 @@ if [ "$(whoami)" = "root" ]; then
PREFIX="" PREFIX=""
fi fi
# Fall back to pip3 if pip is missing
PIP="pip"
type $PIP >/dev/null 2>&1 || PIP="pip3"
# Install any new Python packages # Install any new Python packages
COMMAND="${PREFIX}pip install -r requirements.txt --upgrade" COMMAND="${PREFIX}${PIP} install -r requirements.txt --upgrade"
echo "Updating required Python packages ($COMMAND)..." echo "Updating required Python packages ($COMMAND)..."
eval $COMMAND eval $COMMAND
@ -24,4 +28,4 @@ eval $COMMAND
./netbox/manage.py migrate ./netbox/manage.py migrate
# Collect static files # Collect static files
./netbox/manage.py collectstatic --noinput ./netbox/manage.py collectstatic --no-input