mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-19 09:53:34 -06:00
commit
17873706b7
@ -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:
|
||||||
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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)')
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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'),
|
||||||
|
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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)))
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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']:
|
||||||
|
@ -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;
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
|
||||||
|
@ -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">×</span>
|
<span aria-hidden="true">×</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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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">
|
||||||
|
@ -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 %}
|
||||||
|
@ -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>
|
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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">
|
||||||
|
@ -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 }}">
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user