mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-30 21:58:19 +01:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17873706b7 | ||
|
|
5037046624 | ||
|
|
5c0614d656 | ||
|
|
697866d1ba | ||
|
|
38d826d152 | ||
|
|
401357b8cb | ||
|
|
599e1bb220 | ||
|
|
f9a33bfc14 | ||
|
|
610b412506 | ||
|
|
09000ad9b3 | ||
|
|
f70f0f8d62 | ||
|
|
d5c3f9e780 | ||
|
|
b42dab3eef | ||
|
|
7cbea49c2d | ||
|
|
6dcc5a1169 | ||
|
|
53129125dd | ||
|
|
ba1a4f06ff | ||
|
|
cf5be85dad | ||
|
|
3b48a270fc | ||
|
|
105e9da866 | ||
|
|
d3b16ba443 | ||
|
|
abc51fdc5d | ||
|
|
e0ad2b4555 | ||
|
|
35a0a658a7 | ||
|
|
2c99a8bee4 | ||
|
|
1dd2bdcb8e | ||
|
|
f3eee25527 | ||
|
|
78b0072051 | ||
|
|
7766e1f684 | ||
|
|
78adaecb89 | ||
|
|
f89d91783b | ||
|
|
a18e1a0161 | ||
|
|
4308b8a4a5 | ||
|
|
aa54e14c37 | ||
|
|
3ffe36e5ed | ||
|
|
3b2c74042e | ||
|
|
11ae938146 | ||
|
|
f11bb254a5 | ||
|
|
0b681c471e | ||
|
|
05d3354570 | ||
|
|
6813787fc7 | ||
|
|
28761fc960 | ||
|
|
e8fd0f3531 | ||
|
|
8103c399d5 | ||
|
|
a51f5edbc8 | ||
|
|
be393a9d10 | ||
|
|
ef59f38ec4 | ||
|
|
47120fae01 | ||
|
|
c0417c1989 | ||
|
|
fb6cfa45fd | ||
|
|
b875cea10d | ||
|
|
32bf17c076 | ||
|
|
66a6a8f33c | ||
|
|
05b71564d8 | ||
|
|
1682de59df | ||
|
|
f26253ec49 | ||
|
|
f2dc287f14 | ||
|
|
3fe3151af7 | ||
|
|
1c1fd8f210 | ||
|
|
3ce2f0d100 | ||
|
|
92d726bbd4 | ||
|
|
e2ef0bc3a6 | ||
|
|
13c29cb7a9 |
@@ -9,9 +9,7 @@ env:
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install pep8
|
||||
|
||||
@@ -90,6 +90,22 @@ NetBox does not have the ability to generate graphs natively, but this feature a
|
||||
* **Source URL:** The source of the image to be embedded. The associated object will be available as a template variable named `obj`.
|
||||
* **Link URL (optional):** A URL to which the graph will be linked. The associated object will be available as a template variable named `obj`.
|
||||
|
||||
## Examples
|
||||
|
||||
You only need to define one graph object for each graph you want to include when viewing an object. For example, if you want to include a graph of traffic through an interface over the past five minutes, your graph source might looks like this:
|
||||
|
||||
```
|
||||
https://my.nms.local/graphs/?node={{ obj.device.name }}&interface={{ obj.name }}&duration=5m
|
||||
```
|
||||
|
||||
You can define several graphs to provide multiple contexts when viewing an object. For example:
|
||||
|
||||
```
|
||||
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
||||
https://my.nms.local/graphs/?type=throughput&node={{ obj.device.name }}&interface={{ obj.name }}&duration=24h
|
||||
https://my.nms.local/graphs/?type=errors&node={{ obj.device.name }}&interface={{ obj.name }}&duration=60m
|
||||
```
|
||||
|
||||
# Topology Maps
|
||||
|
||||
NetBox can generate simple topology maps from the physical network connections recorded in its database. First, you'll need to create a topology map definition under the admin UI at Extras > Topology Maps.
|
||||
|
||||
@@ -6,6 +6,7 @@ Python 3:
|
||||
|
||||
```no-highlight
|
||||
# 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:
|
||||
@@ -20,7 +21,9 @@ Python 3:
|
||||
|
||||
```no-highlight
|
||||
# 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:
|
||||
@@ -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.)
|
||||
|
||||
Python 3:
|
||||
|
||||
```no-highlight
|
||||
# pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
Python 2:
|
||||
|
||||
```no-highlight
|
||||
# pip install -r requirements.txt
|
||||
```
|
||||
@@ -172,7 +183,7 @@ Superuser created successfully.
|
||||
# Collect Static Files
|
||||
|
||||
```no-highlight
|
||||
# ./manage.py collectstatic
|
||||
# ./manage.py collectstatic --no-input
|
||||
|
||||
You have requested to collect static files at the destination
|
||||
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**
|
||||
|
||||
```no-highlight
|
||||
# apt-get install -y postgresql libpq-dev python-psycopg2
|
||||
# apt-get update
|
||||
# apt-get install -y postgresql libpq-dev
|
||||
```
|
||||
|
||||
**CentOS/RHEL**
|
||||
|
||||
```no-highlight
|
||||
# yum install -y postgresql postgresql-server postgresql-devel python-psycopg2
|
||||
# yum install -y postgresql postgresql-server postgresql-devel
|
||||
# postgresql-setup initdb
|
||||
```
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ from django.db.models import Q
|
||||
from dcim.models import Site
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
|
||||
from .models import Provider, Circuit, CircuitType
|
||||
|
||||
|
||||
class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -42,6 +43,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -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)
|
||||
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)
|
||||
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)')
|
||||
upstream_speed = models.PositiveIntegerField(blank=True, null=True, verbose_name='Upstream speed (Kbps)',
|
||||
help_text='Upstream speed, if different from port speed')
|
||||
upstream_speed = models.PositiveIntegerField(
|
||||
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')
|
||||
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
|
||||
form_class = forms.CircuitTypeForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('circuits:circuittype_list')
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuit'
|
||||
model = Circuit
|
||||
form_class = forms.CircuitForm
|
||||
fields_initial = ['provider']
|
||||
template_name = 'circuits/circuit_edit.html'
|
||||
default_return_url = 'circuits:circuit_list'
|
||||
|
||||
@@ -230,7 +229,6 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'circuits.change_circuittermination'
|
||||
model = CircuitTermination
|
||||
form_class = forms.CircuitTerminationForm
|
||||
fields_initial = ['term_side']
|
||||
template_name = 'circuits/circuittermination_edit.html'
|
||||
|
||||
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'])
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.circuit.get_absolute_url()
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.db.models import Q
|
||||
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
from .models import (
|
||||
ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection,
|
||||
Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackReservation, RackRole, Region, Site,
|
||||
@@ -14,6 +14,7 @@ from .models import (
|
||||
|
||||
|
||||
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -81,6 +82,7 @@ class RackGroupFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -145,6 +147,33 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class RackReservationFilter(django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site',
|
||||
queryset=Site.objects.all(),
|
||||
label='Site (ID)',
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack__site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Site (slug)',
|
||||
)
|
||||
group_id = NullableModelMultipleChoiceFilter(
|
||||
name='rack__group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
label='Group (ID)',
|
||||
)
|
||||
group = NullableModelMultipleChoiceFilter(
|
||||
name='rack__group',
|
||||
queryset=RackGroup.objects.all(),
|
||||
to_field_name='slug',
|
||||
label='Group',
|
||||
)
|
||||
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||
name='rack',
|
||||
queryset=Rack.objects.all(),
|
||||
@@ -155,8 +184,19 @@ class RackReservationFilter(django_filters.FilterSet):
|
||||
model = RackReservation
|
||||
fields = ['rack', 'user']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(rack__name__icontains=value) |
|
||||
Q(rack__facility_id__icontains=value) |
|
||||
Q(user__username__icontains=value) |
|
||||
Q(description__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -191,6 +231,7 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -405,6 +446,10 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
method='filter_type',
|
||||
label='Interface type',
|
||||
)
|
||||
mac_address = django_filters.CharFilter(
|
||||
method='_mac_address',
|
||||
label='MAC address',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
@@ -420,48 +465,73 @@ class InterfaceFilter(django_filters.FilterSet):
|
||||
return queryset.filter(form_factor=IFACE_FF_LAG)
|
||||
return queryset
|
||||
|
||||
def _mac_address(self, queryset, name, value):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return queryset
|
||||
try:
|
||||
return queryset.filter(mac_address=value)
|
||||
except AddrFormatError:
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class ConsoleConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = []
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
)
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(cs_port__device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(cs_port__device__name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class PowerConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = []
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
)
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(power_outlet__device__site__slug=value)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(device__name__icontains=value) |
|
||||
Q(power_outlet__device__name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterfaceConnection
|
||||
fields = []
|
||||
device = django_filters.CharFilter(
|
||||
method='filter_device',
|
||||
label='Device',
|
||||
)
|
||||
|
||||
def filter_site(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@@ -470,3 +540,11 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
|
||||
Q(interface_a__device__site__slug=value) |
|
||||
Q(interface_b__device__site__slug=value)
|
||||
)
|
||||
|
||||
def filter_device(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(interface_a__device__name__icontains=value) |
|
||||
Q(interface_b__device__name__icontains=value)
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ from .models import (
|
||||
Interface, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate,
|
||||
Manufacturer, Module, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_TYPE_CHOICES,
|
||||
RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD,
|
||||
VIRTUAL_IFACE_TYPES
|
||||
SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES
|
||||
)
|
||||
|
||||
|
||||
@@ -330,6 +330,19 @@ class RackReservationForm(BootstrapMixin, forms.ModelForm):
|
||||
return unit_choices
|
||||
|
||||
|
||||
class RackReservationFilterForm(BootstrapMixin, forms.Form):
|
||||
q = forms.CharField(required=False, label='Search')
|
||||
site = FilterChoiceField(
|
||||
queryset=Site.objects.annotate(filter_count=Count('racks__reservations')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
group_id = FilterChoiceField(
|
||||
queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__reservations')),
|
||||
label='Rack group',
|
||||
null_option=(0, 'None')
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
@@ -375,6 +388,21 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
|
||||
to_field_name='slug'
|
||||
)
|
||||
is_console_server = forms.BooleanField(
|
||||
required=False, label='Is a console server', widget=forms.CheckboxInput(attrs={'value': 'True'}))
|
||||
is_pdu = forms.BooleanField(
|
||||
required=False, label='Is a PDU', widget=forms.CheckboxInput(attrs={'value': 'True'})
|
||||
)
|
||||
is_network_device = forms.BooleanField(
|
||||
required=False, label='Is a network device', widget=forms.CheckboxInput(attrs={'value': 'True'})
|
||||
)
|
||||
subdevice_role = forms.NullBooleanField(
|
||||
required=False, label='Subdevice role', widget=forms.Select(choices=(
|
||||
('', '---------'),
|
||||
(SUBDEVICE_ROLE_PARENT, 'Parent'),
|
||||
(SUBDEVICE_ROLE_CHILD, 'Child'),
|
||||
))
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
@@ -1394,9 +1422,16 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Limit LAG choices to interfaces which belong to the parent device.
|
||||
device = None
|
||||
if self.initial.get('device'):
|
||||
self.fields['lag'].queryset = Interface.objects.filter(
|
||||
device=self.initial['device'], form_factor=IFACE_FF_LAG
|
||||
try:
|
||||
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:
|
||||
self.fields['lag'].choices = []
|
||||
@@ -1466,7 +1501,7 @@ class InterfaceConnectionForm(BootstrapMixin, forms.ModelForm):
|
||||
super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Initialize interface A choices
|
||||
device_a_interfaces = Interface.objects.filter(device=device_a).exclude(
|
||||
device_a_interfaces = Interface.objects.order_naturally().filter(device=device_a).exclude(
|
||||
form_factor__in=VIRTUAL_IFACE_TYPES
|
||||
).select_related(
|
||||
'circuit_termination', 'connected_as_a', 'connected_as_b'
|
||||
@@ -1643,44 +1678,17 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
||||
|
||||
class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
device = forms.CharField(required=False, label='Device name')
|
||||
|
||||
|
||||
class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
device = forms.CharField(required=False, label='Device name')
|
||||
|
||||
|
||||
class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
|
||||
site = forms.ModelChoiceField(required=False, queryset=Site.objects.all(), to_field_name='slug')
|
||||
|
||||
|
||||
#
|
||||
# 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
|
||||
device = forms.CharField(required=False, label='Device name')
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from collections import OrderedDict
|
||||
from itertools import count, groupby
|
||||
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
|
||||
@@ -571,6 +572,15 @@ class RackReservation(models.Model):
|
||||
)
|
||||
})
|
||||
|
||||
@property
|
||||
def unit_list(self):
|
||||
"""
|
||||
Express the assigned units as a string of summarized ranges. For example:
|
||||
[0, 1, 2, 10, 14, 15, 16] => "0-2, 10, 14-16"
|
||||
"""
|
||||
group = (list(x) for _, x in groupby(sorted(self.units), lambda x, c=count(): next(c) - x))
|
||||
return ', '.join('-'.join(map(str, (g[0], g[-1])[:len(g)])) for g in group)
|
||||
|
||||
|
||||
#
|
||||
# Device Types
|
||||
@@ -781,9 +791,9 @@ class InterfaceManager(models.Manager):
|
||||
IFACE_ORDERING_CHOICES (typically indicated by a parent Device's DeviceType).
|
||||
|
||||
To order interfaces naturally, the `name` field is split into five distinct components: leading text (name),
|
||||
slot, subslot, position, and channel:
|
||||
slot, subslot, position, channel, and virtual circuit:
|
||||
|
||||
{name}{slot}/{subslot}/{position}:{channel}
|
||||
{name}{slot}/{subslot}/{position}:{channel}.{vc}
|
||||
|
||||
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would
|
||||
be parsed as follows:
|
||||
@@ -793,21 +803,23 @@ class InterfaceManager(models.Manager):
|
||||
subslot = 0
|
||||
position = 1
|
||||
channel = None
|
||||
vc = 0
|
||||
|
||||
The chosen sorting method will determine which fields are ordered first in the query.
|
||||
"""
|
||||
queryset = self.get_queryset()
|
||||
sql_col = '{}.name'.format(queryset.model._meta.db_table)
|
||||
ordering = {
|
||||
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_name'),
|
||||
IFACE_ORDERING_NAME: ('_name', '_slot', '_subslot', '_position', '_channel'),
|
||||
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_name'),
|
||||
IFACE_ORDERING_NAME: ('_name', '_slot', '_subslot', '_position', '_channel', '_vc'),
|
||||
}[method]
|
||||
return queryset.extra(select={
|
||||
'_name': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
|
||||
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_channel': "CAST(SUBSTRING({} FROM ':([0-9]+)$') AS integer)".format(sql_col),
|
||||
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
|
||||
'_channel': "COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col),
|
||||
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
|
||||
}).order_by(*ordering)
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from utilities.tables import BaseTable, ToggleColumn
|
||||
from .models import (
|
||||
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType,
|
||||
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
|
||||
RackGroup, Region, Site,
|
||||
RackGroup, RackReservation, Region, Site,
|
||||
)
|
||||
|
||||
|
||||
@@ -64,6 +64,12 @@ RACK_ROLE = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
RACKRESERVATION_ACTIONS = """
|
||||
{% if perms.dcim.change_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=record.pk %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
DEVICEROLE_ACTIONS = """
|
||||
{% if perms.dcim.change_devicerole %}
|
||||
<a href="{% url 'dcim:devicerole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
|
||||
@@ -100,6 +106,10 @@ DEVICE_PRIMARY_IP = """
|
||||
{{ record.primary_ip4.address.ip|default:"" }}
|
||||
"""
|
||||
|
||||
SUBDEVICE_ROLE_TEMPLATE = """
|
||||
{% if record.subdevice_role == True %}Parent{% elif record.subdevice_role == False %}Child{% else %}—{% endif %}
|
||||
"""
|
||||
|
||||
UTILIZATION_GRAPH = """
|
||||
{% load helpers %}
|
||||
{% utilization_graph value %}
|
||||
@@ -222,6 +232,23 @@ class RackImportTable(BaseTable):
|
||||
fields = ('site', 'group', 'name', 'facility_id', 'tenant', 'u_height')
|
||||
|
||||
|
||||
#
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationTable(BaseTable):
|
||||
pk = ToggleColumn()
|
||||
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
|
||||
unit_list = tables.Column(orderable=False, verbose_name='Units')
|
||||
actions = tables.TemplateColumn(
|
||||
template_code=RACKRESERVATION_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name=''
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = RackReservation
|
||||
fields = ('pk', 'rack', 'unit_list', 'user', 'created', 'description', 'actions')
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
@@ -249,11 +276,18 @@ class DeviceTypeTable(BaseTable):
|
||||
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
|
||||
part_number = tables.Column(verbose_name='Part Number')
|
||||
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
|
||||
is_console_server = tables.BooleanColumn(verbose_name='CS')
|
||||
is_pdu = tables.BooleanColumn(verbose_name='PDU')
|
||||
is_network_device = tables.BooleanColumn(verbose_name='Net')
|
||||
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
|
||||
instance_count = tables.Column(verbose_name='Instances')
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = DeviceType
|
||||
fields = ('pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count')
|
||||
fields = (
|
||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
|
||||
'is_network_device', 'subdevice_role', 'instance_count'
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -36,6 +36,8 @@ urlpatterns = [
|
||||
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
|
||||
|
||||
# Rack reservations
|
||||
url(r'^rack-reservations/$', views.RackReservationListView.as_view(), name='rackreservation_list'),
|
||||
url(r'^rack-reservations/delete/$', views.RackReservationBulkDeleteView.as_view(), name='rackreservation_bulk_delete'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/edit/$', views.RackReservationEditView.as_view(), name='rackreservation_edit'),
|
||||
url(r'^rack-reservations/(?P<pk>\d+)/delete/$', views.RackReservationDeleteView.as_view(), name='rackreservation_delete'),
|
||||
|
||||
@@ -114,7 +116,6 @@ urlpatterns = [
|
||||
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+)/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<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.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 extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
|
||||
from utilities.forms import ConfirmationForm
|
||||
@@ -90,7 +90,12 @@ class ComponentCreateView(View):
|
||||
self.parent_field: parent.pk,
|
||||
'name': name,
|
||||
}
|
||||
component_data.update(data)
|
||||
# Replace objects with their primary key to keep component_form.clean() happy
|
||||
for k, v in data.items():
|
||||
if hasattr(v, 'pk'):
|
||||
component_data[k] = v.pk
|
||||
else:
|
||||
component_data[k] = v
|
||||
component_form = self.model_form(component_data)
|
||||
if component_form.is_valid():
|
||||
new_components.append(component_form.save(commit=False))
|
||||
@@ -119,13 +124,13 @@ class ComponentCreateView(View):
|
||||
|
||||
class ComponentEditView(ObjectEditView):
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.device.get_absolute_url()
|
||||
|
||||
|
||||
class ComponentDeleteView(ObjectDeleteView):
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.device.get_absolute_url()
|
||||
|
||||
|
||||
@@ -144,7 +149,7 @@ class RegionEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Region
|
||||
form_class = forms.RegionForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('dcim:region_list')
|
||||
|
||||
|
||||
@@ -237,7 +242,7 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = RackGroup
|
||||
form_class = forms.RackGroupForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('dcim:rackgroup_list')
|
||||
|
||||
|
||||
@@ -263,7 +268,7 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = RackRole
|
||||
form_class = forms.RackRoleForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('dcim:rackrole_list')
|
||||
|
||||
|
||||
@@ -355,6 +360,14 @@ class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
# Rack reservations
|
||||
#
|
||||
|
||||
class RackReservationListView(ObjectListView):
|
||||
queryset = RackReservation.objects.all()
|
||||
filter = filters.RackReservationFilter
|
||||
filter_form = forms.RackReservationFilterForm
|
||||
table = tables.RackReservationTable
|
||||
template_name = 'dcim/rackreservation_list.html'
|
||||
|
||||
|
||||
class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_rackreservation'
|
||||
model = RackReservation
|
||||
@@ -366,7 +379,7 @@ class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
obj.user = request.user
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.rack.get_absolute_url()
|
||||
|
||||
|
||||
@@ -374,10 +387,16 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'dcim.delete_rackreservation'
|
||||
model = RackReservation
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.rack.get_absolute_url()
|
||||
|
||||
|
||||
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||
permission_required = 'dcim.delete_rackreservation'
|
||||
cls = RackReservation
|
||||
default_return_url = 'dcim:rackreservation_list'
|
||||
|
||||
|
||||
#
|
||||
# Manufacturers
|
||||
#
|
||||
@@ -393,7 +412,7 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Manufacturer
|
||||
form_class = forms.ManufacturerForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('dcim:manufacturer_list')
|
||||
|
||||
|
||||
@@ -613,7 +632,7 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = DeviceRole
|
||||
form_class = forms.DeviceRoleForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('dcim:devicerole_list')
|
||||
|
||||
|
||||
@@ -638,7 +657,7 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Platform
|
||||
form_class = forms.PlatformForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('dcim:platform_list')
|
||||
|
||||
|
||||
@@ -681,19 +700,15 @@ def device(request, pk):
|
||||
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
|
||||
.filter(device=device, mgmt_only=False)\
|
||||
.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)\
|
||||
.filter(device=device, mgmt_only=True)\
|
||||
.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(
|
||||
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
|
||||
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)
|
||||
secrets = device.secrets.all()
|
||||
|
||||
@@ -724,7 +739,6 @@ def device(request, pk):
|
||||
'interfaces': interfaces,
|
||||
'mgmt_interfaces': mgmt_interfaces,
|
||||
'device_bays': device_bays,
|
||||
'ip_addresses': ip_addresses,
|
||||
'services': services,
|
||||
'secrets': secrets,
|
||||
'related_devices': related_devices,
|
||||
@@ -736,7 +750,6 @@ class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'dcim.change_device'
|
||||
model = Device
|
||||
form_class = forms.DeviceForm
|
||||
fields_initial = ['site', 'rack', 'position', 'face', 'device_bay']
|
||||
template_name = 'dcim/device_edit.html'
|
||||
default_return_url = 'dcim:device_list'
|
||||
|
||||
@@ -1445,9 +1458,10 @@ def interfaceconnection_add(request, pk):
|
||||
))
|
||||
if '_addanother' in request.POST:
|
||||
base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk})
|
||||
device_b = interfaceconnection.interface_b.device
|
||||
params = urlencode({
|
||||
'rack_b': interfaceconnection.interface_b.device.rack.pk,
|
||||
'device_b': interfaceconnection.interface_b.device.pk,
|
||||
'rack_b': device_b.rack.pk if device_b.rack else '',
|
||||
'device_b': device_b.pk,
|
||||
})
|
||||
return HttpResponseRedirect('{}?{}'.format(base_url, params))
|
||||
else:
|
||||
@@ -1547,47 +1561,6 @@ class InterfaceConnectionsListView(ObjectListView):
|
||||
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
|
||||
#
|
||||
|
||||
20
netbox/extras/migrations/0005_useraction_add_bulk_create.py
Normal file
20
netbox/extras/migrations/0005_useraction_add_bulk_create.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-04-04 19:45
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0004_topologymap_change_comma_to_semicolon'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='useraction',
|
||||
name='action',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, b'created'), (7, b'bulk created'), (2, b'imported'), (3, b'modified'), (4, b'bulk edited'), (5, b'deleted'), (6, b'bulk deleted')]),
|
||||
),
|
||||
]
|
||||
@@ -56,13 +56,15 @@ ACTION_EDIT = 3
|
||||
ACTION_BULK_EDIT = 4
|
||||
ACTION_DELETE = 5
|
||||
ACTION_BULK_DELETE = 6
|
||||
ACTION_BULK_CREATE = 7
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_CREATE, 'created'),
|
||||
(ACTION_BULK_CREATE, 'bulk created'),
|
||||
(ACTION_IMPORT, 'imported'),
|
||||
(ACTION_EDIT, 'modified'),
|
||||
(ACTION_BULK_EDIT, 'bulk edited'),
|
||||
(ACTION_DELETE, 'deleted'),
|
||||
(ACTION_BULK_DELETE, 'bulk deleted')
|
||||
(ACTION_BULK_DELETE, 'bulk deleted'),
|
||||
)
|
||||
|
||||
|
||||
@@ -328,6 +330,9 @@ class UserActionManager(models.Manager):
|
||||
def log_import(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_IMPORT, message)
|
||||
|
||||
def log_bulk_create(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_BULK_CREATE, message)
|
||||
|
||||
def log_bulk_edit(self, user, content_type, message=''):
|
||||
self.log_bulk_action(user, content_type, ACTION_BULK_EDIT, message)
|
||||
|
||||
@@ -358,7 +363,7 @@ class UserAction(models.Model):
|
||||
return u'{} {} {}'.format(self.user, self.get_action_display(), self.content_type)
|
||||
|
||||
def icon(self):
|
||||
if self.action in [ACTION_CREATE, ACTION_IMPORT]:
|
||||
if self.action in [ACTION_CREATE, ACTION_BULK_CREATE, ACTION_IMPORT]:
|
||||
return mark_safe('<i class="glyphicon glyphicon-plus text-success"></i>')
|
||||
elif self.action in [ACTION_EDIT, ACTION_BULK_EDIT]:
|
||||
return mark_safe('<i class="glyphicon glyphicon-pencil text-warning"></i>')
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
# This script will generate a random 50-character string suitable for use as a SECRET_KEY.
|
||||
import os
|
||||
import random
|
||||
|
||||
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)'
|
||||
random.seed = (os.urandom(2048))
|
||||
print(''.join(random.choice(charset) for c in range(50)))
|
||||
secure_random = random.SystemRandom()
|
||||
print(''.join(secure_random.sample(charset, 50)))
|
||||
|
||||
@@ -7,12 +7,13 @@ from django.db.models import Q
|
||||
from dcim.models import Site, Device, Interface
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from tenancy.models import Tenant
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
|
||||
class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -44,6 +45,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class RIRFilter(django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
|
||||
class Meta:
|
||||
model = RIR
|
||||
@@ -51,6 +53,7 @@ class RIRFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -84,6 +87,7 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -182,6 +186,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
|
||||
|
||||
class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
@@ -283,6 +288,7 @@ class VLANGroupFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -6,7 +6,7 @@ from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFi
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import (
|
||||
APISelect, BootstrapMixin, BulkImportForm, CSVDataField, ExpandableIPAddressField, FilterChoiceField, Livesearch,
|
||||
SlugField, add_blank_choice,
|
||||
ReturnURLForm, SlugField, add_blank_choice,
|
||||
)
|
||||
|
||||
from .models import (
|
||||
@@ -210,28 +210,33 @@ class PrefixFromCSVForm(forms.ModelForm):
|
||||
site = self.cleaned_data.get('site')
|
||||
vlan_group_name = self.cleaned_data.get('vlan_group_name')
|
||||
vlan_vid = self.cleaned_data.get('vlan_vid')
|
||||
|
||||
# Validate VLAN
|
||||
vlan_group = None
|
||||
vlan = None
|
||||
|
||||
# Validate VLAN group
|
||||
if vlan_group_name:
|
||||
try:
|
||||
vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
|
||||
except VLANGroup.DoesNotExist:
|
||||
self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
|
||||
if vlan_vid and vlan_group:
|
||||
if site:
|
||||
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:
|
||||
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:
|
||||
self.add_error('vlan_vid', "Invalid VLAN ID ({} - {}).".format(vlan_group, vlan_vid))
|
||||
elif vlan_vid and site:
|
||||
try:
|
||||
self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid)
|
||||
except VLAN.DoesNotExist:
|
||||
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site))
|
||||
if site:
|
||||
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site))
|
||||
elif vlan_group:
|
||||
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for group {}.".format(vlan_vid, vlan_group_name))
|
||||
elif not vlan_group_name:
|
||||
self.add_error('vlan_vid', "Invalid global VLAN ID ({}).".format(vlan_vid))
|
||||
except VLAN.MultipleObjectsReturned:
|
||||
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
|
||||
elif vlan_vid:
|
||||
self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.")
|
||||
self.instance.vlan = vlan
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -302,21 +307,46 @@ class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
|
||||
# IP addresses
|
||||
#
|
||||
|
||||
class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
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 IPAddressForm(BootstrapMixin, ReturnURLForm, CustomFieldForm):
|
||||
interface_site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
|
||||
attrs={'filter-for': 'interface_rack'}
|
||||
)
|
||||
)
|
||||
interface_rack = forms.ModelChoiceField(
|
||||
queryset=Rack.objects.all(), required=False, label='Rack', widget=APISelect(
|
||||
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:
|
||||
model = IPAddress
|
||||
fields = ['address', 'vrf', 'tenant', 'status', 'nat_inside', 'description']
|
||||
fields = ['address', 'vrf', 'tenant', 'status', 'interface', 'nat_inside', 'description']
|
||||
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')
|
||||
}
|
||||
|
||||
@@ -325,8 +355,37 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
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
|
||||
# If the IP is assigned to an interface, populate site/device fields accordingly
|
||||
if self.instance.nat_inside.interface:
|
||||
@@ -340,9 +399,7 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
)
|
||||
else:
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(pk=nat_inside.pk)
|
||||
|
||||
else:
|
||||
|
||||
# Initialize nat_device choices if nat_site is set
|
||||
if self.is_bound and self.data.get('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'])
|
||||
else:
|
||||
self.fields['nat_device'].choices = []
|
||||
|
||||
# Initialize nat_inside choices if nat_device is set
|
||||
if self.is_bound and self.data.get('nat_device'):
|
||||
self.fields['nat_inside'].queryset = IPAddress.objects.filter(
|
||||
@@ -362,12 +418,15 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
|
||||
self.fields['nat_inside'].choices = []
|
||||
|
||||
|
||||
class IPAddressBulkAddForm(BootstrapMixin, forms.Form):
|
||||
address = ExpandableIPAddressField()
|
||||
class IPAddressBulkAddForm(BootstrapMixin, CustomFieldForm):
|
||||
address_pattern = ExpandableIPAddressField(label='Address Pattern')
|
||||
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)
|
||||
description = forms.CharField(max_length=100, required=False)
|
||||
|
||||
pattern_map = ('address_pattern', 'address')
|
||||
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = ['address_pattern', 'vrf', 'tenant', 'status', 'description']
|
||||
|
||||
|
||||
class IPAddressAssignForm(BootstrapMixin, forms.Form):
|
||||
@@ -586,27 +645,51 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
|
||||
|
||||
|
||||
class VLANFromCSVForm(forms.ModelForm):
|
||||
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'})
|
||||
group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'VLAN group not found.'})
|
||||
tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'})
|
||||
site = forms.ModelChoiceField(
|
||||
queryset=Site.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Site not found.'}
|
||||
)
|
||||
group_name = forms.CharField(required=False)
|
||||
tenant = forms.ModelChoiceField(
|
||||
Tenant.objects.all(), to_field_name='name', required=False,
|
||||
error_messages={'invalid_choice': 'Tenant not found.'}
|
||||
)
|
||||
status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
|
||||
role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid role.'})
|
||||
role = forms.ModelChoiceField(
|
||||
queryset=Role.objects.all(), required=False, to_field_name='name',
|
||||
error_messages={'invalid_choice': 'Invalid role.'}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLAN
|
||||
fields = ['site', 'group', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
|
||||
fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
|
||||
|
||||
def clean(self):
|
||||
|
||||
super(VLANFromCSVForm, self).clean()
|
||||
|
||||
# Validate VLANGroup
|
||||
group_name = self.cleaned_data.get('group_name')
|
||||
if group_name:
|
||||
try:
|
||||
vlan_group = VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
|
||||
except VLANGroup.DoesNotExist:
|
||||
self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
m = super(VLANFromCSVForm, self).save(commit=False)
|
||||
|
||||
vlan = super(VLANFromCSVForm, self).save(commit=False)
|
||||
|
||||
# Assign VLANGroup by site and name
|
||||
if self.cleaned_data['group_name']:
|
||||
vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=self.cleaned_data['group_name'])
|
||||
|
||||
# Assign VLAN status by name
|
||||
m.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||
vlan.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
|
||||
|
||||
if kwargs.get('commit'):
|
||||
m.save()
|
||||
return m
|
||||
vlan.save()
|
||||
return vlan
|
||||
|
||||
|
||||
class VLANImportForm(BootstrapMixin, BulkImportForm):
|
||||
|
||||
@@ -57,8 +57,6 @@ urlpatterns = [
|
||||
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+)/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'),
|
||||
|
||||
# VLAN groups
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django_tables2 import RequestConfig
|
||||
import netaddr
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.contrib import messages
|
||||
@@ -243,7 +244,7 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = RIR
|
||||
form_class = forms.RIRForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('ipam:rir_list')
|
||||
|
||||
|
||||
@@ -295,7 +296,12 @@ def aggregate(request, pk):
|
||||
prefix_table = tables.PrefixTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
prefix_table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table)
|
||||
|
||||
paginate = {
|
||||
'klass': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(prefix_table)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
@@ -364,7 +370,7 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Role
|
||||
form_class = forms.RoleForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('ipam:role_list')
|
||||
|
||||
|
||||
@@ -427,7 +433,12 @@ def prefix(request, pk):
|
||||
child_prefix_table = tables.PrefixTable(child_prefixes)
|
||||
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
|
||||
child_prefix_table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(child_prefix_table)
|
||||
|
||||
paginate = {
|
||||
'klass': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(child_prefix_table)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
@@ -453,7 +464,6 @@ class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = Prefix
|
||||
form_class = forms.PrefixForm
|
||||
template_name = 'ipam/prefix_edit.html'
|
||||
fields_initial = ['vrf', 'tenant', 'site', 'prefix', 'vlan']
|
||||
default_return_url = 'ipam:prefix_list'
|
||||
|
||||
|
||||
@@ -500,7 +510,12 @@ def prefix_ipaddresses(request, pk):
|
||||
ip_table = tables.IPAddressTable(ipaddresses)
|
||||
if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
|
||||
ip_table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table)
|
||||
|
||||
paginate = {
|
||||
'klass': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(ip_table)
|
||||
|
||||
# Compile permissions list for rendering the object table
|
||||
permissions = {
|
||||
@@ -556,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):
|
||||
permission_required = 'ipam.change_ipaddress'
|
||||
model = IPAddress
|
||||
form_class = forms.IPAddressForm
|
||||
fields_initial = ['address', 'vrf']
|
||||
template_name = 'ipam/ipaddress_edit.html'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
@@ -643,7 +588,7 @@ class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
|
||||
permission_required = 'ipam.add_ipaddress'
|
||||
form = forms.IPAddressBulkAddForm
|
||||
model = IPAddress
|
||||
model_form = forms.IPAddressForm
|
||||
template_name = 'ipam/ipaddress_bulk_add.html'
|
||||
default_return_url = 'ipam:ipaddress_list'
|
||||
|
||||
@@ -702,7 +647,7 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = VLANGroup
|
||||
form_class = forms.VLANGroupForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('ipam:vlangroup_list')
|
||||
|
||||
|
||||
@@ -791,7 +736,7 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
|
||||
return obj
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return obj.device.get_absolute_url()
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ except ImportError:
|
||||
"the documentation.")
|
||||
|
||||
|
||||
VERSION = '1.9.2'
|
||||
VERSION = '1.9.6'
|
||||
|
||||
# Import local configuration
|
||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from rest_framework_swagger.views import get_swagger_view
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
@@ -7,6 +9,7 @@ from users.views import login, logout
|
||||
|
||||
|
||||
handler500 = handle_500
|
||||
swagger_view = get_swagger_view(title='NetBox API')
|
||||
|
||||
_patterns = [
|
||||
|
||||
@@ -31,7 +34,7 @@ _patterns = [
|
||||
url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')),
|
||||
url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')),
|
||||
url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')),
|
||||
url(r'^api/docs/', include('rest_framework_swagger.urls')),
|
||||
url(r'^api/docs/', swagger_view, name='api_docs'),
|
||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
|
||||
# Error testing
|
||||
|
||||
@@ -313,6 +313,16 @@ li.occupied + li.available {
|
||||
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 */
|
||||
.banner-bottom {
|
||||
margin-bottom: 50px;
|
||||
|
||||
@@ -4,9 +4,11 @@ from django.db.models import Q
|
||||
|
||||
from .models import Secret, SecretRole
|
||||
from dcim.models import Device
|
||||
from utilities.filters import NumericInFilter
|
||||
|
||||
|
||||
class SecretFilter(django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -30,7 +30,7 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = SecretRole
|
||||
form_class = forms.SecretRoleForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('secrets:secretrole_list')
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>NetBox - {% block title %}Home{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{% static 'bootstrap-3.3.6-dist/css/bootstrap.min.css' %}">
|
||||
<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 '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 'css/base.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/base.css' %}">
|
||||
<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">
|
||||
</head>
|
||||
@@ -28,7 +28,7 @@
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
{% if request.user.is_authenticated or not settings.LOGIN_REQUIRED %}
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="dropdown{% if request.path|startswith:'/dcim/sites/' or 'tenancy' in request.path %} active{% endif %}">
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/sites/,/dcim/regions/,/tenancy/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:site_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Sites</a></li>
|
||||
@@ -54,7 +54,7 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/dcim/rack' %} active{% endif %}">
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/rack' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li>
|
||||
@@ -72,9 +72,11 @@
|
||||
{% if perms.dcim.add_rackrole %}
|
||||
<li><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Role</a></li>
|
||||
{% endif %}
|
||||
<li class="divider"></li>
|
||||
<li><a href="{% url 'dcim:rackreservation_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Reservations</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/dcim/device' or request.path|startswith:'/dcim/manufacturers/' or request.path|startswith:'/dcim/platforms/' %} active{% endif %}">
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li>
|
||||
@@ -110,7 +112,7 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/dcim/console-connections/' or request.path|startswith:'/dcim/power-connections/' or request.path|startswith:'/dcim/interface-connections/' %} active{% endif %}">
|
||||
<li class="dropdown{% if request.path|contains:'/dcim/console-connections/,/dcim/power-connections/,/dcim/interface-connections/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Console Connections</a></li>
|
||||
@@ -133,7 +135,7 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/ipam/' and not request.path|startswith:'/ipam/vlan' %} active{% endif %}">
|
||||
<li class="dropdown{% if request.path|contains:'/ipam/' and not request.path|contains:'/ipam/vlan' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="fa fa-search" aria-hidden="true"></i> IP Addresses</a></li>
|
||||
@@ -179,7 +181,7 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/ipam/vlan' %} active{% endif %}">
|
||||
<li class="dropdown{% if request.path|contains:'/ipam/vlan' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li>
|
||||
@@ -199,7 +201,7 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown{% if request.path|startswith:'/circuits/' %} active{% endif %}">
|
||||
<li class="dropdown{% if request.path|contains:'/circuits/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'circuits:provider_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Providers</a></li>
|
||||
@@ -223,7 +225,7 @@
|
||||
</ul>
|
||||
</li>
|
||||
{% if request.user.is_authenticated %}
|
||||
<li class="dropdown{% if request.path|startswith:'/secrets/' %} active{% endif %}">
|
||||
<li class="dropdown{% if request.path|contains:'/secrets/' %} active{% endif %}">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'secrets:secret_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secrets</a></li>
|
||||
@@ -254,10 +256,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container wrapper">
|
||||
<div class="container wrapper">
|
||||
{% if settings.BANNER_TOP %}
|
||||
<div class="alert alert-info text-center" role="alert">
|
||||
{{ settings.BANNER_TOP|safe }}
|
||||
{{ settings.BANNER_TOP|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if settings.MAINTENANCE_MODE %}
|
||||
@@ -266,24 +268,24 @@
|
||||
<p>NetBox is currently in maintenance mode. Functionality may be limited.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissable" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
{{ message|safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% block content %}{% endblock %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissable" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% block content %}{% endblock %}
|
||||
<div class="push"></div>
|
||||
{% if settings.BANNER_BOTTOM %}
|
||||
<div class="alert alert-info text-center banner-bottom" role="alert">
|
||||
{% if settings.BANNER_BOTTOM %}
|
||||
<div class="alert alert-info text-center banner-bottom" role="alert">
|
||||
{{ settings.BANNER_BOTTOM|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<p class="text-muted">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</p>
|
||||
@@ -294,13 +296,14 @@
|
||||
<div class="col-xs-4 text-right">
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/" target="_blank">Docs</a> ·
|
||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'django.swagger.base.view' %}">API</a> ·
|
||||
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a>
|
||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'api_docs' %}">API</a> ·
|
||||
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a> ·
|
||||
<i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/digitalocean/netbox/wiki">Help</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="text/javascript">
|
||||
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
|
||||
</script>
|
||||
|
||||
@@ -194,35 +194,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% 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-heading">
|
||||
<strong>Services</strong>
|
||||
@@ -250,7 +221,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Critical Connections</strong>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for iface in mgmt_interfaces %}
|
||||
{% include 'dcim/inc/interface.html' with icon='wrench' %}
|
||||
{% empty %}
|
||||
@@ -375,7 +346,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for devicebay in device_bays %}
|
||||
{% include 'dcim/inc/devicebay.html' with selectable=True %}
|
||||
{% empty %}
|
||||
@@ -416,6 +387,9 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Interfaces</strong>
|
||||
<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 %}
|
||||
<button class="btn btn-default btn-xs toggle">
|
||||
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
|
||||
@@ -428,7 +402,7 @@
|
||||
{% endif %}
|
||||
</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 %}
|
||||
{% include 'dcim/inc/interface.html' with selectable=True %}
|
||||
{% empty %}
|
||||
@@ -485,7 +459,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for csp in cs_ports %}
|
||||
{% include 'dcim/inc/consoleserverport.html' with selectable=True %}
|
||||
{% empty %}
|
||||
@@ -537,7 +511,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover panel-body">
|
||||
<table class="table table-hover panel-body component-list">
|
||||
{% for po in power_outlets %}
|
||||
{% include 'dcim/inc/poweroutlet.html' with selectable=True %}
|
||||
{% empty %}
|
||||
@@ -628,6 +602,18 @@ $(".powerport-toggle").click(function() {
|
||||
$(".interface-toggle").click(function() {
|
||||
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 src="{% static 'js/graphs.js' %}"></script>
|
||||
<script src="{% static 'js/secrets.js' %}"></script>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rack</td>
|
||||
<td>Rack name</td>
|
||||
<td>Rack name (optional)</td>
|
||||
<td>R101</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -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 %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ cp.pk }}" />
|
||||
@@ -7,7 +7,6 @@
|
||||
<td>
|
||||
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
|
||||
</td>
|
||||
<td></td>
|
||||
{% if cp.cs_port %}
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td colspan="2" class="text-right">
|
||||
{% if perms.dcim.change_consoleport %}
|
||||
{% if cp.cs_port %}
|
||||
{% 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 %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ csp.pk }}" />
|
||||
@@ -19,7 +19,7 @@
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td colspan="2" class="text-right">
|
||||
{% if perms.dcim.change_consoleserverport %}
|
||||
{% if csp.connected_console %}
|
||||
{% 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 %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
|
||||
@@ -19,7 +19,7 @@
|
||||
<span class="text-muted">Vacant</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td colspan="2" class="text-right">
|
||||
{% if perms.dcim.change_devicebay %}
|
||||
{% if devicebay.installed_device %}
|
||||
<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 %}
|
||||
<td class="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>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<small>{{ iface.mac_address|default:'' }}</small>
|
||||
</td>
|
||||
{% if iface.is_virtual %}
|
||||
{% if iface.is_lag %}
|
||||
<td colspan="2" class="text-muted">LAG interface</td>
|
||||
{% elif iface.is_virtual %}
|
||||
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||
{% elif iface.connection %}
|
||||
{% with iface.connected_interface as connected_iface %}
|
||||
@@ -35,7 +34,13 @@
|
||||
<td colspan="2">
|
||||
<i class="fa fa-fw fa-globe" title="Circuit"></i>
|
||||
{% if peer_termination %}
|
||||
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a> via
|
||||
{% if peer_termination.interface %}
|
||||
<a href="{% url 'dcim:device' pk=peer_termination.interface.device.pk %}">{{ peer_termination.interface.device }}</a>
|
||||
(<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a>)
|
||||
{% else %}
|
||||
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">{{ peer_termination.site }}</a>
|
||||
{% endif %}
|
||||
via
|
||||
{% endif %}
|
||||
<a href="{% url 'circuits:circuit' pk=iface.circuit_termination.circuit_id %}">{{ iface.circuit_termination.circuit }}</a>
|
||||
</td>
|
||||
@@ -45,7 +50,7 @@
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td colspan="2" class="text-right">
|
||||
{% if show_graphs %}
|
||||
{% 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">
|
||||
@@ -53,6 +58,11 @@
|
||||
</button>
|
||||
{% 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 not iface.is_virtual %}
|
||||
{% if iface.connection %}
|
||||
@@ -65,19 +75,19 @@
|
||||
<i class="fa fa-plug" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Delete connection">
|
||||
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
||||
<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-resize-full" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% 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">
|
||||
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
|
||||
</button>
|
||||
<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>
|
||||
{% else %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -98,3 +108,41 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</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 %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ po.pk }}" />
|
||||
@@ -19,7 +19,7 @@
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td colspan="2" class="text-right">
|
||||
{% if perms.dcim.change_poweroutlet %}
|
||||
{% if po.connected_port %}
|
||||
{% 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 %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ pp.pk }}" />
|
||||
@@ -7,7 +7,6 @@
|
||||
<td>
|
||||
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
|
||||
</td>
|
||||
<td></td>
|
||||
{% if pp.power_outlet %}
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right">
|
||||
<td colspan="2" class="text-right">
|
||||
{% if perms.dcim.change_powerport %}
|
||||
{% if pp.power_outlet %}
|
||||
{% if pp.connection_status %}
|
||||
|
||||
@@ -43,12 +43,14 @@
|
||||
{% render_field form.set_as_primary %}
|
||||
</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 %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-9 col-md-offset-3">
|
||||
<button type="submit" name="_create" class="btn btn-primary">Create</button>
|
||||
|
||||
@@ -210,18 +210,18 @@
|
||||
</tr>
|
||||
{% for resv in reservations %}
|
||||
<tr>
|
||||
<td>{{ resv.units|join:', ' }}</td>
|
||||
<td>{{ resv.unit_list }}</td>
|
||||
<td>
|
||||
{{ resv.description }}<br />
|
||||
<small>{{ resv.user }} · {{ resv.created }}</small>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if perms.change_rackreservation %}
|
||||
{% if perms.dcim.change_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_edit' pk=resv.pk %}" class="btn btn-warning btn-xs" title="Edit reservation">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.delete_rackreservation %}
|
||||
{% if perms.dcim.delete_rackreservation %}
|
||||
<a href="{% url 'dcim:rackreservation_delete' pk=resv.pk %}" class="btn btn-danger btn-xs" title="Delete reservation">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
14
netbox/templates/dcim/rackreservation_list.html
Normal file
14
netbox/templates/dcim/rackreservation_list.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends '_base.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% block title %}Rack Reservations{% endblock %}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackreservation_bulk_delete' %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{% include 'inc/search_panel.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -98,14 +98,8 @@
|
||||
<td>
|
||||
{% if ipaddress.interface %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -10,13 +10,21 @@
|
||||
|
||||
{% block form %}
|
||||
<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">
|
||||
{% render_field form.address %}
|
||||
{% render_field form.address_pattern %}
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% render_field form.status %}
|
||||
{% render_field form.description %}
|
||||
</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 %}
|
||||
|
||||
@@ -16,39 +16,20 @@
|
||||
{% render_field form.vrf %}
|
||||
{% render_field form.tenant %}
|
||||
{% 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 %}
|
||||
</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-heading"><strong>NAT IP (Inside)</strong></div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<form action="." method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="panel panel-{{ panel_class|default:"danger" }}">
|
||||
<div class="panel-heading">{% block title %}{% endblock %}</div>
|
||||
<div class="panel-body">
|
||||
{% block message %}<p>Are you sure?</p>{% endblock %}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
<div class="form-group">
|
||||
<div class="checkbox{% if form.confirm.errors %} has-error{% endif %}">
|
||||
<label for="{{ form.confirm.id_for_label }}">
|
||||
|
||||
@@ -3,11 +3,12 @@ import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from extras.filters import CustomFieldFilterSet
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter
|
||||
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
|
||||
from .models import Tenant, TenantGroup
|
||||
|
||||
|
||||
class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
||||
id__in = NumericInFilter(name='id', lookup_expr='in')
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
||||
@@ -29,7 +29,7 @@ class TenantGroupEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
model = TenantGroup
|
||||
form_class = forms.TenantGroupForm
|
||||
|
||||
def get_return_url(self, obj):
|
||||
def get_return_url(self, request, obj):
|
||||
return reverse('tenancy:tenantgroup_list')
|
||||
|
||||
|
||||
@@ -81,7 +81,6 @@ class TenantEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'tenancy.change_tenant'
|
||||
model = Tenant
|
||||
form_class = forms.TenantForm
|
||||
fields_initial = ['group']
|
||||
template_name = 'tenancy/tenant_edit.html'
|
||||
default_return_url = 'tenancy:tenant_list'
|
||||
|
||||
|
||||
@@ -6,6 +6,17 @@ from django.db.models import Q
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
|
||||
#
|
||||
# Filters
|
||||
#
|
||||
|
||||
class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
"""
|
||||
Filters for a set of numeric values. Example: id__in=100,200,300
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||
"""
|
||||
This field operates like a normal ModelMultipleChoiceField except that it allows for one additional choice which is
|
||||
|
||||
@@ -437,13 +437,18 @@ class BootstrapMixin(forms.BaseForm):
|
||||
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
|
||||
be specified to direct the user to a specific URL after the action has been taken.
|
||||
Provides a hidden return URL field to control where the user is directed after the form is submitted.
|
||||
"""
|
||||
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)
|
||||
return_url = forms.CharField(required=False, widget=forms.HiddenInput())
|
||||
|
||||
|
||||
class BulkEditForm(forms.Form):
|
||||
|
||||
@@ -5,7 +5,8 @@ from django.core.paginator import Paginator, Page
|
||||
class EnhancedPaginator(Paginator):
|
||||
|
||||
def __init__(self, object_list, per_page, **kwargs):
|
||||
per_page = getattr(settings, 'PAGINATE_COUNT', 50)
|
||||
if not isinstance(per_page, int) or per_page < 1:
|
||||
per_page = getattr(settings, 'PAGINATE_COUNT', 50)
|
||||
super(EnhancedPaginator, self).__init__(object_list, per_page, **kwargs)
|
||||
|
||||
def _get_page(self, *args, **kwargs):
|
||||
|
||||
@@ -45,11 +45,11 @@ def gfm(value):
|
||||
|
||||
|
||||
@register.filter()
|
||||
def startswith(value, arg):
|
||||
def contains(value, arg):
|
||||
"""
|
||||
Test whether a string starts with the given argument
|
||||
Test whether a value contains any of a given set of strings. `arg` should be a comma-separated list of strings.
|
||||
"""
|
||||
return str(value).startswith(arg)
|
||||
return any(s in value for s in arg.split(','))
|
||||
|
||||
|
||||
#
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from collections import OrderedDict
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -11,7 +12,9 @@ from django.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInpu
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.template import TemplateSyntaxError
|
||||
from django.utils.html import escape
|
||||
from django.utils.http import is_safe_url
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.views.generic import View
|
||||
|
||||
from extras.forms import CustomFieldForm
|
||||
@@ -38,6 +41,23 @@ class CustomFieldQueryset:
|
||||
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):
|
||||
"""
|
||||
List a series of objects.
|
||||
@@ -101,7 +121,13 @@ class ObjectListView(View):
|
||||
table = self.table(self.queryset)
|
||||
if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
|
||||
table.base_columns['pk'].visible = True
|
||||
RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(table)
|
||||
|
||||
# Apply the request context
|
||||
paginate = {
|
||||
'klass': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(table)
|
||||
|
||||
context = {
|
||||
'table': table,
|
||||
@@ -121,21 +147,18 @@ class ObjectListView(View):
|
||||
return {}
|
||||
|
||||
|
||||
class ObjectEditView(View):
|
||||
class ObjectEditView(GetReturnURLMixin, View):
|
||||
"""
|
||||
Create or edit a single object.
|
||||
|
||||
model: The model of the object being edited
|
||||
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
|
||||
default_return_url: The name of the URL used to display a list of this object type
|
||||
"""
|
||||
model = None
|
||||
form_class = None
|
||||
fields_initial = []
|
||||
template_name = 'utilities/obj_edit.html'
|
||||
default_return_url = 'home'
|
||||
|
||||
def get_object(self, kwargs):
|
||||
# Look up object by slug or PK. Return None if neither was provided.
|
||||
@@ -150,24 +173,19 @@ class ObjectEditView(View):
|
||||
# given some parameter from the request URL.
|
||||
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):
|
||||
|
||||
obj = self.get_object(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)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'obj': obj,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'form': form,
|
||||
'return_url': self.get_return_url(obj),
|
||||
'return_url': self.get_return_url(request, obj),
|
||||
})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -187,10 +205,10 @@ class ObjectEditView(View):
|
||||
msg = u'Created ' if obj_created else u'Modified '
|
||||
msg += self.model._meta.verbose_name
|
||||
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:
|
||||
msg = u'{} {}'.format(msg, obj)
|
||||
messages.success(request, msg)
|
||||
msg = u'{} {}'.format(msg, escape(obj))
|
||||
messages.success(request, mark_safe(msg))
|
||||
if obj_created:
|
||||
UserAction.objects.log_create(request.user, obj, msg)
|
||||
else:
|
||||
@@ -198,17 +216,22 @@ class ObjectEditView(View):
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
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, {
|
||||
'obj': obj,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'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.
|
||||
|
||||
@@ -218,7 +241,6 @@ class ObjectDeleteView(View):
|
||||
"""
|
||||
model = None
|
||||
template_name = 'utilities/obj_delete.html'
|
||||
default_return_url = 'home'
|
||||
|
||||
def get_object(self, kwargs):
|
||||
# Look up object by slug if one has been provided. Otherwise, use PK.
|
||||
@@ -227,24 +249,16 @@ class ObjectDeleteView(View):
|
||||
else:
|
||||
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):
|
||||
|
||||
obj = self.get_object(kwargs)
|
||||
initial_data = {
|
||||
'return_url': request.GET.get('return_url'),
|
||||
}
|
||||
form = ConfirmationForm(initial=initial_data)
|
||||
form = ConfirmationForm(initial=request.GET)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'obj': obj,
|
||||
'form': form,
|
||||
'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):
|
||||
@@ -263,17 +277,17 @@ class ObjectDeleteView(View):
|
||||
messages.success(request, msg)
|
||||
UserAction.objects.log_delete(request.user, obj, msg)
|
||||
|
||||
return_url = form.cleaned_data['return_url']
|
||||
if return_url and is_safe_url(url=return_url, host=request.get_host()):
|
||||
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(obj))
|
||||
return redirect(self.get_return_url(request, obj))
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'obj': obj,
|
||||
'form': form,
|
||||
'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),
|
||||
})
|
||||
|
||||
|
||||
@@ -282,12 +296,12 @@ class BulkAddView(View):
|
||||
Create new objects in bulk.
|
||||
|
||||
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
|
||||
default_return_url: Name of the URL to which the user is redirected after creating the objects
|
||||
"""
|
||||
form = None
|
||||
model = None
|
||||
model_form = None
|
||||
template_name = None
|
||||
default_return_url = 'home'
|
||||
|
||||
@@ -296,45 +310,44 @@ class BulkAddView(View):
|
||||
form = self.form()
|
||||
|
||||
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,
|
||||
'return_url': reverse(self.default_return_url),
|
||||
})
|
||||
|
||||
def post(self, request):
|
||||
|
||||
model = self.model_form._meta.model
|
||||
form = self.form(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
# The first field will be used as the pattern
|
||||
field_names = list(form.fields.keys())
|
||||
pattern_field = field_names[0]
|
||||
# Read the pattern field and target from the form's pattern_map
|
||||
pattern_field, pattern_target = form.pattern_map
|
||||
pattern = form.cleaned_data[pattern_field]
|
||||
|
||||
# All other fields will be copied as object attributes
|
||||
kwargs = {k: form.cleaned_data[k] for k in field_names[1:]}
|
||||
model_form_data = form.cleaned_data
|
||||
|
||||
new_objs = []
|
||||
try:
|
||||
with transaction.atomic():
|
||||
for value in pattern:
|
||||
obj = self.model(**kwargs)
|
||||
setattr(obj, pattern_field, value)
|
||||
obj.full_clean()
|
||||
obj.save()
|
||||
model_form_data[pattern_target] = value
|
||||
model_form = self.model_form(model_form_data)
|
||||
obj = model_form.save()
|
||||
new_objs.append(obj)
|
||||
except ValidationError as e:
|
||||
form.add_error(None, e)
|
||||
|
||||
if not form.errors:
|
||||
messages.success(request, 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)
|
||||
UserAction.objects.log_bulk_create(request.user, ContentType.objects.get_for_model(model), msg)
|
||||
if '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
return redirect(self.default_return_url)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'obj_type': self.model._meta.verbose_name,
|
||||
'obj_type': model._meta.verbose_name,
|
||||
'return_url': reverse(self.default_return_url),
|
||||
})
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
cffi>=1.8
|
||||
cryptography>=1.4
|
||||
Django>=1.10
|
||||
Django>=1.10,<1.11
|
||||
django-debug-toolbar>=1.6
|
||||
django-filter>=1.0.1
|
||||
django-mptt==0.8.7
|
||||
django-rest-swagger==0.3.10
|
||||
django-rest-swagger>=2.1.0
|
||||
django-tables2>=1.2.5
|
||||
djangorestframework>=3.5.0
|
||||
graphviz>=0.4.10
|
||||
Markdown>=2.6.7
|
||||
natsort>=5.0.0
|
||||
ncclient==0.5.2
|
||||
ncclient==0.5.3
|
||||
netaddr==0.7.18
|
||||
paramiko>=2.0.0
|
||||
psycopg2>=2.6.1
|
||||
|
||||
@@ -15,8 +15,12 @@ if [ "$(whoami)" = "root" ]; then
|
||||
PREFIX=""
|
||||
fi
|
||||
|
||||
# Fall back to pip3 if pip is missing
|
||||
PIP="pip"
|
||||
type $PIP >/dev/null 2>&1 || PIP="pip3"
|
||||
|
||||
# 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)..."
|
||||
eval $COMMAND
|
||||
|
||||
@@ -24,4 +28,4 @@ eval $COMMAND
|
||||
./netbox/manage.py migrate
|
||||
|
||||
# Collect static files
|
||||
./netbox/manage.py collectstatic --noinput
|
||||
./netbox/manage.py collectstatic --no-input
|
||||
|
||||
Reference in New Issue
Block a user