Compare commits

...

44 Commits

Author SHA1 Message Date
Jeremy Stretch
a355783377 Merge pull request #1316 from digitalocean/develop
Release v2.0.8
2017-07-05 14:36:08 -04:00
Jeremy Stretch
dafdbc9ddb Release v2.0.8 2017-07-05 14:34:46 -04:00
Jeremy Stretch
14f5204548 Fixes #1289: Retain inside NAT assignment when editing an IP address 2017-07-05 14:29:40 -04:00
Jeremy Stretch
5233463f0b Merge pull request #1315 from s11-charendt/develop
Preserve fileextension, regardless of upper or lower case on imageupload
2017-07-05 11:02:18 -04:00
Jeremy Stretch
1d4a416100 Fixes #1297: Allow passing custom field choice selection PKs as string-quoted integers 2017-07-05 11:00:43 -04:00
Jeremy Stretch
25ee796d5b Include instructions when displaying an inactive user key 2017-07-05 10:51:25 -04:00
Christian Harendt
e08107063a Preserve fileextension, regardless of upper or lower case on imageuploads 2017-07-04 14:26:35 +02:00
Jeremy Stretch
cd5a86bfcf Closes #1303: Highlight installed interface connections in green on device view 2017-06-29 13:35:54 -04:00
Jeremy Stretch
97b67d0f93 Fixes #1299: Corrected permission to add a service to a device 2017-06-28 12:05:26 -04:00
Jeremy Stretch
3f82be7192 Closes #1298: Calculate prefix utilization based on its status (container or non-container) 2017-06-26 17:36:24 -04:00
Jeremy Stretch
adfcb5f7b6 Fixes #1295: Docstring typo 2017-06-26 09:31:20 -04:00
Jeremy Stretch
5aba1d9aec Fixes #1288: Corrected permission name for deleting image attachments 2017-06-19 09:20:03 -04:00
Jeremy Stretch
afdf5750b5 Fixes #1279: Fix primary_ip assignment during IP address import 2017-06-16 12:45:42 -04:00
Jeremy Stretch
ea869d4ffc Fixes #1282: Fixed tooltips on "mark connected/planned" toggle buttons for device connections 2017-06-16 10:02:14 -04:00
Jeremy Stretch
9d89eed873 Fixes #1281: Show LLDP neighbors tab on device view only if necessary conditions are met 2017-06-16 09:32:58 -04:00
Jeremy Stretch
c00eea7991 Post-release version bump 2017-06-15 14:28:35 -04:00
Jeremy Stretch
88239e0b0d Merge pull request #1278 from digitalocean/develop
Release v2.0.7
2017-06-15 14:26:38 -04:00
Jeremy Stretch
9930e2745f Release v2.0.7 2017-06-15 14:22:16 -04:00
Jeremy Stretch
da3879e928 Fixes #1275: Raise validation error on prefix import when multiple VLANs are found 2017-06-15 14:13:20 -04:00
Jeremy Stretch
7195b7c803 Closes #626: Added bulk disconnect function for console/power/interface connections on device view 2017-06-15 14:01:49 -04:00
Jeremy Stretch
9b082eea14 Fixes #1274: Exclude unterminated circuits from topology maps 2017-06-15 10:05:14 -04:00
Jeremy Stretch
a16218b311 Fixes #1273: Corrected status choices in IP address import form 2017-06-14 16:22:49 -04:00
Jeremy Stretch
29a71fd903 #1265: Improved livesearch UI Javascript 2017-06-14 14:50:12 -04:00
Jeremy Stretch
fcacac7c6f Fixes #1265: Fix console/power/interface connection validation when selecting a device via live search 2017-06-14 13:00:36 -04:00
Jeremy Stretch
78d74261e9 Fixes #1266: Prevent termination a circuit to an already-connected interface 2017-06-14 10:57:43 -04:00
Jeremy Stretch
16d694734b Fixes #1268: Fix CSV import error under Python 3 2017-06-14 09:55:52 -04:00
Jeremy Stretch
252ab0fbab Fixes #1238: Fix error when editing an IP with a NAT assignment which has no assigned device 2017-06-13 16:57:25 -04:00
Jeremy Stretch
8eb9c451a1 Renamed AddViews to CreateViews for consistency 2017-06-13 16:48:21 -04:00
Jeremy Stretch
469c52be28 Fixes #1263: Differentiate add and edit permissions for objects 2017-06-13 16:41:57 -04:00
Jeremy Stretch
54fa51eeff Post-release version bump 2017-06-13 15:55:58 -04:00
Jeremy Stretch
5456af6867 Removed 'update-alternatives' from Python3 instructions 2017-06-13 14:28:38 -04:00
Jeremy Stretch
180446c34d Removed Debian/RHEL references from installation docs 2017-06-12 10:06:19 -04:00
Jeremy Stretch
5c63a499d5 Merge pull request #1259 from digitalocean/develop
Release v2.0.6
2017-06-12 09:51:15 -04:00
Jeremy Stretch
3a2c5b318a Release v2.0.6 2017-06-12 09:44:09 -04:00
Jeremy Stretch
cfff69a715 Closes #1180: Simplified the process of finding related devices when viewing a device 2017-06-09 17:04:09 -04:00
Jeremy Stretch
08883d86ef Closes #913: Added headers to object CSV exports 2017-06-09 16:24:59 -04:00
Jeremy Stretch
8a849ebeff Closes #990: Enable logging configuration in configuration.py 2017-06-09 15:03:10 -04:00
Jeremy Stretch
05a796faf1 Closes #704: Allow filtering VLANs by group when editing prefixes 2017-06-09 14:15:12 -04:00
Jeremy Stretch
9e1d03b383 Formatting cleanup 2017-06-09 12:19:32 -04:00
Jeremy Stretch
0a929f2971 Fixes #1253: Improved upgrade.sh to allow forcing Python2 2017-06-09 12:13:47 -04:00
Jeremy Stretch
7878992570 First stab at an interactive shell which pre-imports all models 2017-06-08 16:38:25 -04:00
Jeremy Stretch
4f95926cbd Added utilization percetange to aggregate and prefix views 2017-06-08 12:48:49 -04:00
Jeremy Stretch
f3e997ea39 Closes #40: Added IP utilization graph to prefix list 2017-06-08 12:37:25 -04:00
Jeremy Stretch
2b921c21ff Post-release version bump 2017-06-08 10:12:39 -04:00
47 changed files with 753 additions and 294 deletions

View File

@@ -83,6 +83,34 @@ Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce uni
---
## LOGGING
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`.
The Django framework on which NetBox runs allows for the customization of logging, e.g. to write logs to file. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/1.11/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a file:
```
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': '/var/log/netbox.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'INFO',
},
},
}
```
---
## LOGIN_REQUIRED
Default: False

View File

@@ -28,6 +28,9 @@ Create a file in the same directory as `configuration.py` (typically `netbox/net
## General Server Configuration
!!! info
When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure.
```python
import ldap
@@ -49,11 +52,11 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
LDAP_IGNORE_CERT_ERRORS = True
```
!!! info
When using Windows Server 2012 you may need to specify a port on AUTH_LDAP_SERVER_URI - 3269 for secure, 3268 for non-secure.
## User Authentication
!!! info
When using Windows Server, `2012 AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
```python
from django_auth_ldap.config import LDAPSearch
@@ -73,9 +76,6 @@ AUTH_LDAP_USER_ATTR_MAP = {
}
```
!!! info
When using Windows Server 2012 AUTH_LDAP_USER_DN_TEMPLATE should be set to None.
# User Groups for Permissions
```python
@@ -109,12 +109,11 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
* `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
* `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
!!! info
It is also possible map user attributes to Django attributes:
It is also possible map user attributes to Django attributes:
```no-highlight
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn"
}
```
```python
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
}
```

View File

@@ -1,12 +1,11 @@
# Installation
**Debian/Ubuntu**
**Ubuntu**
Python 3:
```no-highlight
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
# update-alternatives --install /usr/bin/python python /usr/bin/python3 1
```
Python 2:
@@ -15,7 +14,7 @@ Python 2:
# apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev
```
**CentOS/RHEL**
**CentOS**
Python 3:
@@ -57,13 +56,13 @@ Create the base directory for the NetBox installation. For this guide, we'll use
If `git` is not already installed, install it:
**Debian/Ubuntu**
**Ubuntu**
```no-highlight
# apt-get install -y git
```
**CentOS/RHEL**
**CentOS**
```no-highlight
# yum install -y git
@@ -150,11 +149,14 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a
# Run Database Migrations
Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
!!! warning
The examples on the rest of this page call the `python` executable, which will be Python2 on most systems. Replace this with `python3` if you're running NetBox on Python3.
Before NetBox can run, we need to install the database schema. This is done by running `python manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example):
```no-highlight
# cd /opt/netbox/netbox/
# ./manage.py migrate
# python manage.py migrate
Operations to perform:
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
Running migrations:
@@ -172,7 +174,7 @@ If this step results in a PostgreSQL authentication error, ensure that the usern
NetBox does not come with any predefined user accounts. You'll need to create a super user to be able to log into NetBox:
```no-highlight
# ./manage.py createsuperuser
# python manage.py createsuperuser
Username: admin
Email address: admin@example.com
Password:
@@ -183,7 +185,7 @@ Superuser created successfully.
# Collect Static Files
```no-highlight
# ./manage.py collectstatic --no-input
# python manage.py collectstatic --no-input
You have requested to collect static files at the destination
location as specified in your settings:
@@ -204,7 +206,7 @@ NetBox ships with some initial data to help you get started: RIR definitions, co
This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch.
```no-highlight
# ./manage.py loaddata initial_data
# python manage.py loaddata initial_data
Installed 43 object(s) from 4 fixture(s)
```
@@ -213,7 +215,7 @@ Installed 43 object(s) from 4 fixture(s)
At this point, NetBox should be able to run. We can verify this by starting a development instance:
```no-highlight
# ./manage.py runserver 0.0.0.0:8000 --insecure
# python manage.py runserver 0.0.0.0:8000 --insecure
Performing system checks...
System check identified no issues (0 silenced).

View File

@@ -1,15 +1,18 @@
NetBox requires a PostgreSQL database to store data. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/9.1/static/datatype-net-types.html).)
!!! note
The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 6.9. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors.
# Installation
**Debian/Ubuntu**
**Ubuntu**
```no-highlight
# apt-get update
# apt-get install -y postgresql libpq-dev
```
**CentOS/RHEL**
**CentOS**
```no-highlight
# yum install -y postgresql postgresql-server postgresql-devel

View File

@@ -52,6 +52,13 @@ Once the new code is in place, run the upgrade script (which may need to be run
# ./upgrade.sh
```
!!! warning
The upgrade script will prefer Python3 and pip3 if both executables are available. To force it to use Python2 and pip, use the `-2` argument as below.
```no-highlight
# ./upgrade.sh -2
```
This script:
* Installs or upgrades any new required Python packages

View File

@@ -3,7 +3,7 @@
We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence.
!!! info
Only Debian/Ubuntu instructions are provided here, but the installation process for CentOS/RHEL does not differ much. Please consult the documentation for those distributions for details.
For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed.
```no-highlight
# apt-get install -y gunicorn supervisor

View File

@@ -220,7 +220,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
label='Interface',
widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical',
disabled_indicator='is_connected'
disabled_indicator='connection'
)
)

View File

@@ -52,6 +52,8 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url']
class Meta:
ordering = ['name']
@@ -107,6 +109,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
class Meta:
ordering = ['provider', 'cid']
unique_together = ['provider', 'cid']

View File

@@ -10,7 +10,7 @@ urlpatterns = [
# Providers
url(r'^providers/$', views.ProviderListView.as_view(), name='provider_list'),
url(r'^providers/add/$', views.ProviderEditView.as_view(), name='provider_add'),
url(r'^providers/add/$', views.ProviderCreateView.as_view(), name='provider_add'),
url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'),
url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'),
url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
@@ -20,13 +20,13 @@ urlpatterns = [
# Circuit types
url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'),
url(r'^circuit-types/add/$', views.CircuitTypeEditView.as_view(), name='circuittype_add'),
url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'),
url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'),
url(r'^circuit-types/(?P<slug>[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'),
# Circuits
url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'),
url(r'^circuits/add/$', views.CircuitEditView.as_view(), name='circuit_add'),
url(r'^circuits/add/$', views.CircuitCreateView.as_view(), name='circuit_add'),
url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'),
url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'),
url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'),
@@ -36,7 +36,7 @@ urlpatterns = [
url(r'^circuits/(?P<pk>\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'),
# Circuit terminations
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),

View File

@@ -49,14 +49,18 @@ class ProviderView(View):
})
class ProviderEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_provider'
class ProviderCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_provider'
model = Provider
form_class = forms.ProviderForm
template_name = 'circuits/provider_edit.html'
default_return_url = 'circuits:provider_list'
class ProviderEditView(ProviderCreateView):
permission_required = 'circuits.change_provider'
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'circuits.delete_provider'
model = Provider
@@ -96,8 +100,8 @@ class CircuitTypeListView(ObjectListView):
template_name = 'circuits/circuittype_list.html'
class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuittype'
class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuittype'
model = CircuitType
form_class = forms.CircuitTypeForm
@@ -105,6 +109,10 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('circuits:circuittype_list')
class CircuitTypeEditView(CircuitTypeCreateView):
permission_required = 'circuits.change_circuittype'
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuittype'
cls = CircuitType
@@ -146,14 +154,18 @@ class CircuitView(View):
})
class CircuitEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuit'
class CircuitCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuit'
model = Circuit
form_class = forms.CircuitForm
template_name = 'circuits/circuit_edit.html'
default_return_url = 'circuits:circuit_list'
class CircuitEditView(CircuitCreateView):
permission_required = 'circuits.change_circuit'
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'circuits.delete_circuit'
model = Circuit
@@ -232,8 +244,8 @@ def circuit_terminations_swap(request, pk):
# Circuit terminations
#
class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuittermination'
class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.add_circuittermination'
model = CircuitTermination
form_class = forms.CircuitTerminationForm
template_name = 'circuits/circuittermination_edit.html'
@@ -247,6 +259,10 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
return obj.circuit.get_absolute_url()
class CircuitTerminationEditView(CircuitTerminationCreateView):
permission_required = 'circuits.change_circuittermination'
class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'circuits.delete_circuittermination'
model = CircuitTermination

View File

@@ -13,8 +13,8 @@ from tenancy.forms import TenancyForm
from tenancy.models import Tenant
from utilities.forms import (
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVChoiceField, ExpandableNameField, FilterChoiceField,
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField,
FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
FilterTreeNodeMultipleChoiceField,
)
from .formfields import MACAddressFormField
@@ -1174,6 +1174,10 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.
}
class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
#
# Power ports
#
@@ -1431,6 +1435,10 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
}
class PowerOutletBulkDisconnectForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
#
# Interfaces
#
@@ -1508,6 +1516,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
self.fields['lag'].choices = []
class InterfaceBulkDisconnectForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
#
# Interface connections
#
@@ -1594,9 +1606,10 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
]
# Mark connected interfaces as disabled
self.fields['interface_b'].choices = [
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset
]
if self.data.get('device_b'):
self.fields['interface_b'].choices = [
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset
]
class InterfaceConnectionCSVForm(forms.ModelForm):

View File

@@ -280,6 +280,10 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
objects = SiteManager()
csv_headers = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
]
class Meta:
ordering = ['name']
@@ -402,6 +406,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
objects = RackManager()
csv_headers = [
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
]
class Meta:
ordering = ['site', 'name']
unique_together = [
@@ -981,6 +989,11 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
objects = DeviceManager()
csv_headers = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'site', 'rack_group', 'rack_name', 'position', 'face',
]
class Meta:
ordering = ['name']
unique_together = ['rack', 'position', 'face']
@@ -1096,6 +1109,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
self.asset_tag,
self.get_status_display(),
self.site.name,
self.rack.group.name if self.rack and self.rack.group else None,
self.rack.name if self.rack else None,
self.position,
self.get_face_display(),
@@ -1162,6 +1176,8 @@ class ConsolePort(models.Model):
verbose_name='Console server port', blank=True, null=True)
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
csv_headers = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
class Meta:
ordering = ['device', 'name']
unique_together = ['device', 'name']
@@ -1231,6 +1247,8 @@ class PowerPort(models.Model):
blank=True, null=True)
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
csv_headers = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
class Meta:
ordering = ['device', 'name']
unique_together = ['device', 'name']
@@ -1392,6 +1410,8 @@ class InterfaceConnection(models.Model):
connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED,
verbose_name='Status')
csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
def clean(self):
try:
if self.interface_a == self.interface_b:

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from extras.views import ImageAttachmentEditView
from ipam.views import ServiceEditView
from ipam.views import ServiceCreateView
from secrets.views import secret_add
from .models import Device, Rack, Site
from . import views
@@ -14,13 +14,13 @@ urlpatterns = [
# Regions
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'),
url(r'^regions/add/$', views.RegionEditView.as_view(), name='region_add'),
url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'),
url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
# Sites
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'),
url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'),
url(r'^sites/add/$', views.SiteCreateView.as_view(), name='site_add'),
url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
@@ -30,13 +30,13 @@ urlpatterns = [
# Rack groups
url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'),
url(r'^rack-groups/add/$', views.RackGroupEditView.as_view(), name='rackgroup_add'),
url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'),
url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
# Rack roles
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'),
url(r'^rack-roles/add/$', views.RackRoleEditView.as_view(), name='rackrole_add'),
url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'),
url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'),
@@ -56,18 +56,18 @@ urlpatterns = [
url(r'^racks/(?P<pk>\d+)/$', views.RackView.as_view(), name='rack'),
url(r'^racks/(?P<pk>\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'),
url(r'^racks/(?P<pk>\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'),
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'),
url(r'^racks/(?P<rack>\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'),
url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
# Manufacturers
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'),
url(r'^manufacturers/add/$', views.ManufacturerEditView.as_view(), name='manufacturer_add'),
url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'),
url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
# Device types
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'),
url(r'^device-types/add/$', views.DeviceTypeEditView.as_view(), name='devicetype_add'),
url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'),
url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'),
url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'),
@@ -75,45 +75,45 @@ urlpatterns = [
url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
# Console port templates
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(), name='devicetype_add_consoleport'),
url(r'^device-types/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'),
url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
# Console server port templates
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(), name='devicetype_add_consoleserverport'),
url(r'^device-types/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'),
url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
# Power port templates
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(), name='devicetype_add_powerport'),
url(r'^device-types/(?P<pk>\d+)/power-ports/add/$', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'),
url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
# Power outlet templates
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(), name='devicetype_add_poweroutlet'),
url(r'^device-types/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'),
url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
# Interface templates
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'),
url(r'^device-types/(?P<pk>\d+)/interfaces/add/$', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'),
url(r'^device-types/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'),
url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
# Device bay templates
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(), name='devicetype_add_devicebay'),
url(r'^device-types/(?P<pk>\d+)/device-bays/add/$', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'),
url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
# Device roles
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'),
url(r'^device-roles/add/$', views.DeviceRoleEditView.as_view(), name='devicerole_add'),
url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'),
url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
# Platforms
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'),
url(r'^platforms/add/$', views.PlatformEditView.as_view(), name='platform_add'),
url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'),
url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
# Devices
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'),
url(r'^devices/add/$', views.DeviceEditView.as_view(), name='device_add'),
url(r'^devices/add/$', views.DeviceCreateView.as_view(), name='device_add'),
url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'),
url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'),
url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
@@ -124,12 +124,12 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'),
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceCreateView.as_view(), name='service_assign'),
url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
# Console ports
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortAddView.as_view(), name='consoleport_add'),
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'),
url(r'^devices/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'),
url(r'^console-ports/(?P<pk>\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
@@ -138,7 +138,8 @@ urlpatterns = [
# Console server ports
url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortAddView.as_view(), name='consoleserverport_add'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'),
url(r'^devices/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'),
url(r'^console-server-ports/(?P<pk>\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
@@ -147,7 +148,7 @@ urlpatterns = [
# Power ports
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortAddView.as_view(), name='powerport_add'),
url(r'^devices/(?P<pk>\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'),
url(r'^devices/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'),
url(r'^power-ports/(?P<pk>\d+)/connect/$', views.powerport_connect, name='powerport_connect'),
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
@@ -156,7 +157,8 @@ urlpatterns = [
# Power outlets
url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletAddView.as_view(), name='poweroutlet_add'),
url(r'^devices/(?P<pk>\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'),
url(r'^devices/(?P<pk>\d+)/power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'),
url(r'^devices/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'),
url(r'^power-outlets/(?P<pk>\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
@@ -165,8 +167,9 @@ urlpatterns = [
# Interfaces
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceAddView.as_view(), name='interface_add'),
url(r'^devices/(?P<pk>\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'),
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'),
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
@@ -175,7 +178,7 @@ urlpatterns = [
# Device bays
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'),
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayAddView.as_view(), name='devicebay_add'),
url(r'^devices/(?P<pk>\d+)/bays/add/$', views.DeviceBayCreateView.as_view(), name='devicebay_add'),
url(r'^devices/(?P<pk>\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'),
url(r'^device-bays/(?P<pk>\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),

View File

@@ -8,7 +8,7 @@ from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.paginator import EmptyPage, PageNotAnInteger
from django.db.models import Count
from django.db.models import Count, Q
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@@ -141,6 +141,44 @@ class ComponentDeleteView(ObjectDeleteView):
return obj.device.get_absolute_url()
class BulkDisconnectView(View):
"""
An extendable view for disconnection console/power/interface components in bulk.
"""
model = None
form = None
template_name = 'dcim/bulk_disconnect.html'
def disconnect_objects(self, objects):
raise NotImplementedError()
def post(self, request, pk):
device = get_object_or_404(Device, pk=pk)
selected_objects = []
if '_confirm' in request.POST:
form = self.form(request.POST)
if form.is_valid():
count = self.disconnect_objects(form.cleaned_data['pk'])
messages.success(request, "Disconnected {} {} on {}".format(
count, self.model._meta.verbose_name_plural, device
))
return redirect(device.get_absolute_url())
else:
form = self.form(initial={'pk': request.POST.getlist('pk')})
selected_objects = self.model.objects.filter(pk__in=form.initial['pk'])
return render(request, self.template_name, {
'form': form,
'device': device,
'obj_type_plural': self.model._meta.verbose_name_plural,
'selected_objects': selected_objects,
'return_url': device.get_absolute_url(),
})
#
# Regions
#
@@ -151,8 +189,8 @@ class RegionListView(ObjectListView):
template_name = 'dcim/region_list.html'
class RegionEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_region'
class RegionCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_region'
model = Region
form_class = forms.RegionForm
@@ -160,6 +198,10 @@ class RegionEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:region_list')
class RegionEditView(RegionCreateView):
permission_required = 'dcim.change_region'
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_region'
cls = Region
@@ -203,14 +245,18 @@ class SiteView(View):
})
class SiteEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_site'
class SiteCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_site'
model = Site
form_class = forms.SiteForm
template_name = 'dcim/site_edit.html'
default_return_url = 'dcim:site_list'
class SiteEditView(SiteCreateView):
permission_required = 'dcim.change_site'
class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_site'
model = Site
@@ -245,8 +291,8 @@ class RackGroupListView(ObjectListView):
template_name = 'dcim/rackgroup_list.html'
class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackgroup'
class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackgroup'
model = RackGroup
form_class = forms.RackGroupForm
@@ -254,6 +300,10 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:rackgroup_list')
class RackGroupEditView(RackGroupCreateView):
permission_required = 'dcim.change_rackgroup'
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackgroup'
cls = RackGroup
@@ -271,8 +321,8 @@ class RackRoleListView(ObjectListView):
template_name = 'dcim/rackrole_list.html'
class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackrole'
class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackrole'
model = RackRole
form_class = forms.RackRoleForm
@@ -280,6 +330,10 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:rackrole_list')
class RackRoleEditView(RackRoleCreateView):
permission_required = 'dcim.change_rackrole'
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackrole'
cls = RackRole
@@ -373,14 +427,18 @@ class RackView(View):
})
class RackEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rack'
class RackCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rack'
model = Rack
form_class = forms.RackForm
template_name = 'dcim/rack_edit.html'
default_return_url = 'dcim:rack_list'
class RackEditView(RackCreateView):
permission_required = 'dcim.change_rack'
class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_rack'
model = Rack
@@ -422,8 +480,8 @@ class RackReservationListView(ObjectListView):
template_name = 'dcim/rackreservation_list.html'
class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackreservation'
class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_rackreservation'
model = RackReservation
form_class = forms.RackReservationForm
@@ -437,6 +495,10 @@ class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
return obj.rack.get_absolute_url()
class RackReservationEditView(RackReservationCreateView):
permission_required = 'dcim.change_rackreservation'
class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_rackreservation'
model = RackReservation
@@ -461,8 +523,8 @@ class ManufacturerListView(ObjectListView):
template_name = 'dcim/manufacturer_list.html'
class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_manufacturer'
class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_manufacturer'
model = Manufacturer
form_class = forms.ManufacturerForm
@@ -470,6 +532,10 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:manufacturer_list')
class ManufacturerEditView(ManufacturerCreateView):
permission_required = 'dcim.change_manufacturer'
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_manufacturer'
cls = Manufacturer
@@ -541,14 +607,18 @@ class DeviceTypeView(View):
})
class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_devicetype'
class DeviceTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_devicetype'
model = DeviceType
form_class = forms.DeviceTypeForm
template_name = 'dcim/devicetype_edit.html'
default_return_url = 'dcim:devicetype_list'
class DeviceTypeEditView(DeviceTypeCreateView):
permission_required = 'dcim.change_devicetype'
class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_devicetype'
model = DeviceType
@@ -575,7 +645,7 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Device type components
#
class ConsolePortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleporttemplate'
parent_model = DeviceType
parent_field = 'device_type'
@@ -592,7 +662,7 @@ class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
parent_cls = DeviceType
class ConsoleServerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverporttemplate'
parent_model = DeviceType
parent_field = 'device_type'
@@ -607,7 +677,7 @@ class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDelet
parent_cls = DeviceType
class PowerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerporttemplate'
parent_model = DeviceType
parent_field = 'device_type'
@@ -622,7 +692,7 @@ class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
parent_cls = DeviceType
class PowerOutletTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlettemplate'
parent_model = DeviceType
parent_field = 'device_type'
@@ -637,7 +707,7 @@ class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
parent_cls = DeviceType
class InterfaceTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_interfacetemplate'
parent_model = DeviceType
parent_field = 'device_type'
@@ -660,7 +730,7 @@ class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
parent_cls = DeviceType
class DeviceBayTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebaytemplate'
parent_model = DeviceType
parent_field = 'device_type'
@@ -685,8 +755,8 @@ class DeviceRoleListView(ObjectListView):
template_name = 'dcim/devicerole_list.html'
class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_devicerole'
class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_devicerole'
model = DeviceRole
form_class = forms.DeviceRoleForm
@@ -694,6 +764,10 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:devicerole_list')
class DeviceRoleEditView(DeviceRoleCreateView):
permission_required = 'dcim.change_devicerole'
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicerole'
cls = DeviceRole
@@ -710,8 +784,8 @@ class PlatformListView(ObjectListView):
template_name = 'dcim/platform_list.html'
class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_platform'
class PlatformCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_platform'
model = Platform
form_class = forms.PlatformForm
@@ -719,6 +793,10 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:platform_list')
class PlatformEditView(PlatformCreateView):
permission_required = 'dcim.change_platform'
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_platform'
cls = Platform
@@ -776,20 +854,14 @@ class DeviceView(View):
services = Service.objects.filter(device=device)
secrets = device.secrets.all()
# Find any related devices for convenient linking in the UI
related_devices = []
if device.name:
if re.match('.+[0-9]+$', device.name):
# Strip 1 or more trailing digits (e.g. core-switch1)
base_name = re.match('(.*?)[0-9]+$', device.name).group(1)
elif re.match('.+\d[a-z]$', device.name.lower()):
# Strip a trailing letter if preceded by a digit (e.g. dist-switch3a -> dist-switch3)
base_name = re.match('(.*\d+)[a-z]$', device.name.lower()).group(1)
else:
base_name = None
if base_name:
related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
.select_related('rack', 'device_type__manufacturer')[:10]
# Find up to ten devices in the same site with the same functional role for quick reference.
related_devices = Device.objects.filter(
site=device.site, device_role=device.device_role
).exclude(
pk=device.pk
).select_related(
'rack', 'device_type__manufacturer'
)[:10]
# Show graph button on interfaces only if at least one graph has been created.
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()
@@ -848,14 +920,18 @@ class DeviceLLDPNeighborsView(View):
})
class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_device'
class DeviceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_device'
model = Device
form_class = forms.DeviceForm
template_name = 'dcim/device_edit.html'
default_return_url = 'dcim:device_list'
class DeviceEditView(DeviceCreateView):
permission_required = 'dcim.change_device'
class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_device'
model = Device
@@ -909,7 +985,7 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Console ports
#
class ConsolePortAddView(PermissionRequiredMixin, ComponentCreateView):
class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleport'
parent_model = Device
parent_field = 'device'
@@ -1022,7 +1098,7 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
# Console server ports
#
class ConsoleServerPortAddView(PermissionRequiredMixin, ComponentCreateView):
class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverport'
parent_model = Device
parent_field = 'device'
@@ -1121,6 +1197,15 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
model = ConsoleServerPort
class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
permission_required = 'dcim.change_consoleserverport'
model = ConsoleServerPort
form = forms.ConsoleServerPortBulkDisconnectForm
def disconnect_objects(self, cs_ports):
return ConsolePort.objects.filter(cs_port__in=cs_ports).update(cs_port=None, connection_status=None)
class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverport'
cls = ConsoleServerPort
@@ -1131,7 +1216,7 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Power ports
#
class PowerPortAddView(PermissionRequiredMixin, ComponentCreateView):
class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerport'
parent_model = Device
parent_field = 'device'
@@ -1244,7 +1329,7 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
# Power outlets
#
class PowerOutletAddView(PermissionRequiredMixin, ComponentCreateView):
class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlet'
parent_model = Device
parent_field = 'device'
@@ -1343,6 +1428,17 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView):
model = PowerOutlet
class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
permission_required = 'dcim.change_poweroutlet'
model = PowerOutlet
form = forms.PowerOutletBulkDisconnectForm
def disconnect_objects(self, power_outlets):
return PowerPort.objects.filter(power_outlet__in=power_outlets).update(
power_outlet=None, connection_status=None
)
class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlet'
cls = PowerOutlet
@@ -1353,7 +1449,7 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Interfaces
#
class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView):
class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_interface'
parent_model = Device
parent_field = 'device'
@@ -1373,6 +1469,18 @@ class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
model = Interface
class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
permission_required = 'dcim.change_interface'
model = Interface
form = forms.InterfaceBulkDisconnectForm
def disconnect_objects(self, interfaces):
count, _ = InterfaceConnection.objects.filter(
Q(interface_a__in=interfaces) | Q(interface_b__in=interfaces)
).delete()
return count
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interface'
cls = Interface
@@ -1391,7 +1499,7 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Device bays
#
class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView):
class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebay'
parent_model = Device
parent_field = 'device'

View File

@@ -49,6 +49,10 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
# Validate selected choice
if cf.type == CF_TYPE_SELECT:
try:
value = int(value)
except ValueError:
raise ValidationError("{}: Choice selections must be passed as integers.".format(field_name))
valid_choices = [c.pk for c in cf.choices.all()]
if value not in valid_choices:
raise ValidationError("Invalid choice for field {}: {}".format(field_name, value))

View File

@@ -0,0 +1,62 @@
from __future__ import unicode_literals
import code
import platform
import sys
from django import get_version
from django.apps import apps
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db.models import Model
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users']
BANNER_TEXT = """### NetBox interactive shell ({node})
### Python {python} | Django {django} | NetBox {netbox}
### lsmodels() will show available models. Use help(<model>) for more info.""".format(
node=platform.node(),
python=platform.python_version(),
django=get_version(),
netbox=settings.VERSION
)
class Command(BaseCommand):
help = "Start the Django shell with all NetBox models already imported"
django_models = {}
def _lsmodels(self):
for app, models in self.django_models.items():
app_name = apps.get_app_config(app).verbose_name
print('{}:'.format(app_name))
for m in models:
print(' {}'.format(m))
def get_namespace(self):
namespace = {}
# Gather Django models from each app
for app in APPS:
self.django_models[app] = []
app_models = sys.modules['{}.models'.format(app)]
for name in dir(app_models):
model = getattr(app_models, name)
try:
if issubclass(model, Model):
namespace[name] = model
self.django_models[app].append(name)
except TypeError:
pass
# Load convenience commands
namespace.update({
'lsmodels': self._lsmodels,
})
return namespace
def handle(self, **options):
shell = code.interact(banner=BANNER_TEXT, local=self.get_namespace())
return shell

View File

@@ -371,7 +371,8 @@ class TopologyMap(models.Model):
# Add all circuits to the graph
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
peer_termination = termination.get_peer_termination()
if peer_termination is not None and peer_termination.interface.device in devices:
if (peer_termination is not None and peer_termination.interface is not None and
peer_termination.interface.device in devices):
graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
return graph.pipe(format=img_format)
@@ -386,7 +387,7 @@ def image_upload(instance, filename):
path = 'image-attachments/'
# Rename the file to the provided name, if any. Attempt to preserve the file extension.
extension = filename.rsplit('.')[-1]
extension = filename.rsplit('.')[-1].lower()
if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
filename = '.'.join([instance.name, extension])
elif instance.name:

View File

@@ -25,7 +25,7 @@ class ImageAttachmentEditView(PermissionRequiredMixin, ObjectEditView):
class ImageAttachmentDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_imageattachment'
permission_required = 'extras.delete_imageattachment'
model = ImageAttachment
def get_return_url(self, request, imageattachment):

View File

@@ -1,6 +1,7 @@
from __future__ import unicode_literals
from django import forms
from django.core.exceptions import MultipleObjectsReturned
from django.db.models import Count
from dcim.models import Site, Rack, Device, Interface
@@ -180,6 +181,18 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
required=False,
label='Site',
widget=forms.Select(
attrs={'filter-for': 'vlan_group', 'nullable': 'true'}
)
)
vlan_group = ChainedModelChoiceField(
queryset=VLANGroup.objects.all(),
chains=(
('site', 'site'),
),
required=False,
label='VLAN group',
widget=APISelect(
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
attrs={'filter-for': 'vlan', 'nullable': 'true'}
)
)
@@ -187,11 +200,12 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
queryset=VLAN.objects.all(),
chains=(
('site', 'site'),
('group', 'vlan_group'),
),
required=False,
label='VLAN',
widget=APISelect(
api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name'
api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', display_field='display_name'
)
)
@@ -200,6 +214,14 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant']
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
initial = kwargs.get('initial', {})
if instance and instance.vlan is not None:
initial['vlan_group'] = instance.vlan.group
kwargs['initial'] = initial
super(PrefixForm, self).__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global'
@@ -280,6 +302,10 @@ class PrefixCSVForm(forms.ModelForm):
))
else:
raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group))
except MultipleObjectsReturned:
raise forms.ValidationError(
"Multiple VLANs with VID {} found in group {}".format(vlan_vid, vlan_group)
)
elif vlan_vid:
try:
self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid)
@@ -288,6 +314,8 @@ class PrefixCSVForm(forms.ModelForm):
raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site))
else:
raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid))
except MultipleObjectsReturned:
raise forms.ValidationError("Multiple VLANs with VID {} found".format(vlan_vid))
class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -469,7 +497,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
initial['interface_site'] = instance.interface.device.site
initial['interface_rack'] = instance.interface.device.rack
initial['interface_device'] = instance.interface.device
if instance and instance.nat_inside is not None:
if instance and instance.nat_inside and instance.nat_inside.device is not None:
initial['nat_site'] = instance.nat_inside.device.site
initial['nat_rack'] = instance.nat_inside.device.rack
initial['nat_device'] = instance.nat_inside.device
@@ -561,7 +589,7 @@ class IPAddressCSVForm(forms.ModelForm):
}
)
status = CSVChoiceField(
choices=PREFIX_STATUS_CHOICES,
choices=IPADDRESS_STATUS_CHOICES,
help_text='Operational status'
)
device = FlexibleModelChoiceField(
@@ -613,16 +641,23 @@ class IPAddressCSVForm(forms.ModelForm):
# Set interface
if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
self.instance.interface = Interface.objects.get(device=self.cleaned_data['device'],
name=self.cleaned_data['interface_name'])
self.instance.interface = Interface.objects.get(
device=self.cleaned_data['device'],
name=self.cleaned_data['interface_name']
)
ipaddress = super(IPAddressCSVForm, self).save(*args, **kwargs)
# Set as primary for device
if self.cleaned_data['is_primary']:
device = self.cleaned_data['device']
if self.instance.address.version == 4:
self.instance.primary_ip4_for = self.cleaned_data['device']
device.primary_ip4 = ipaddress
elif self.instance.address.version == 6:
self.instance.primary_ip6_for = self.cleaned_data['device']
device.primary_ip6 = ipaddress
device.save()
return super(IPAddressCSVForm, self).save(*args, **kwargs)
return ipaddress
class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):

View File

@@ -1,6 +1,6 @@
from __future__ import unicode_literals
from netaddr import IPNetwork, cidr_merge
from netaddr import IPNetwork, IPSet
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
@@ -89,6 +89,8 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
description = models.CharField(max_length=100, blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
class Meta:
ordering = ['name']
verbose_name = 'VRF'
@@ -146,6 +148,8 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
description = models.CharField(max_length=100, blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['prefix', 'rir', 'date_added', 'description']
class Meta:
ordering = ['family', 'prefix']
@@ -200,15 +204,11 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
def get_utilization(self):
"""
Determine the utilization rate of the aggregate prefix and return it as a percentage.
Determine the prefix utilization of the aggregate and return it as a percentage.
"""
child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
# Remove overlapping prefixes from list of children
networks = cidr_merge([c.prefix for c in child_prefixes])
children_size = float(0)
for p in networks:
children_size += p.size
return int(children_size / self.prefix.size * 100)
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
child_prefixes = IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100)
@python_2_unicode_compatible
@@ -297,6 +297,10 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
objects = PrefixQuerySet.as_manager()
csv_headers = [
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
]
class Meta:
ordering = ['vrf', 'family', 'prefix']
verbose_name_plural = 'prefixes'
@@ -307,9 +311,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
def get_absolute_url(self):
return reverse('ipam:prefix', args=[self.pk])
def get_duplicates(self):
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
def clean(self):
if self.prefix:
@@ -357,6 +358,30 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
self.description,
])
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]
def get_duplicates(self):
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
def get_utilization(self):
"""
Determine the utilization of the prefix and return it as a percentage. For Prefixes with a status of
"container", calculate utilization based on child prefixes. For all others, count child IP addresses.
"""
if self.status == PREFIX_STATUS_CONTAINER:
queryset = Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
child_prefixes = IPSet([p.prefix for p in queryset])
return int(float(child_prefixes.size) / self.prefix.size * 100)
else:
child_count = IPAddress.objects.filter(
address__net_contained_or_equal=str(self.prefix), vrf=self.vrf
).count()
prefix_size = self.prefix.size
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
prefix_size -= 2
return int(float(child_count) / prefix_size * 100)
@property
def new_subnet(self):
if self.family == 4:
@@ -368,9 +393,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return None
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]
class IPAddressManager(models.Manager):
@@ -414,6 +436,8 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
objects = IPAddressManager()
csv_headers = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']
class Meta:
ordering = ['family', 'address']
verbose_name = 'IP address'
@@ -452,11 +476,12 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
def to_csv(self):
# Determine if this IP is primary for a Device
is_primary = False
if self.family == 4 and getattr(self, 'primary_ip4_for', False):
is_primary = True
elif self.family == 6 and getattr(self, 'primary_ip6_for', False):
is_primary = True
else:
is_primary = False
return csv_format([
self.address,
@@ -527,6 +552,8 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
description = models.CharField(max_length=100, blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
class Meta:
ordering = ['site', 'group', 'vid']
unique_together = [

View File

@@ -34,7 +34,7 @@ RIR_ACTIONS = """
UTILIZATION_GRAPH = """
{% load helpers %}
{% utilization_graph value %}
{% if record.pk %}{% utilization_graph value %}{% else %}&mdash;{% endif %}
"""
ROLE_ACTIONS = """
@@ -241,6 +241,7 @@ class PrefixTable(BaseTable):
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
tenant = tables.TemplateColumn(TENANT_LINK)
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
@@ -248,7 +249,7 @@ class PrefixTable(BaseTable):
class Meta(BaseTable.Meta):
model = Prefix
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
row_attrs = {
'class': lambda record: 'success' if not record.pk else '',
}

View File

@@ -10,7 +10,7 @@ urlpatterns = [
# VRFs
url(r'^vrfs/$', views.VRFListView.as_view(), name='vrf_list'),
url(r'^vrfs/add/$', views.VRFEditView.as_view(), name='vrf_add'),
url(r'^vrfs/add/$', views.VRFCreateView.as_view(), name='vrf_add'),
url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
@@ -20,13 +20,13 @@ urlpatterns = [
# RIRs
url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'),
url(r'^rirs/add/$', views.RIREditView.as_view(), name='rir_add'),
url(r'^rirs/add/$', views.RIRCreateView.as_view(), name='rir_add'),
url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
# Aggregates
url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'),
url(r'^aggregates/add/$', views.AggregateEditView.as_view(), name='aggregate_add'),
url(r'^aggregates/add/$', views.AggregateCreateView.as_view(), name='aggregate_add'),
url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
@@ -36,13 +36,13 @@ urlpatterns = [
# Roles
url(r'^roles/$', views.RoleListView.as_view(), name='role_list'),
url(r'^roles/add/$', views.RoleEditView.as_view(), name='role_add'),
url(r'^roles/add/$', views.RoleCreateView.as_view(), name='role_add'),
url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'),
# Prefixes
url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'),
url(r'^prefixes/add/$', views.PrefixEditView.as_view(), name='prefix_add'),
url(r'^prefixes/add/$', views.PrefixCreateView.as_view(), name='prefix_add'),
url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
@@ -53,8 +53,8 @@ urlpatterns = [
# IP addresses
url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'),
url(r'^ip-addresses/add/$', views.IPAddressEditView.as_view(), name='ipaddress_add'),
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkAddView.as_view(), name='ipaddress_bulk_add'),
url(r'^ip-addresses/add/$', views.IPAddressCreateView.as_view(), name='ipaddress_add'),
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'),
url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
@@ -64,13 +64,13 @@ urlpatterns = [
# VLAN groups
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'),
url(r'^vlan-groups/add/$', views.VLANGroupEditView.as_view(), name='vlangroup_add'),
url(r'^vlan-groups/add/$', views.VLANGroupCreateView.as_view(), name='vlangroup_add'),
url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
# VLANs
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'),
url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'),
url(r'^vlans/add/$', views.VLANCreateView.as_view(), name='vlan_add'),
url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),

View File

@@ -13,7 +13,7 @@ from django.views.generic import View
from dcim.models import Device
from utilities.paginator import EnhancedPaginator
from utilities.views import (
BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
)
from . import filters, forms, tables
from .models import (
@@ -114,14 +114,18 @@ class VRFView(View):
})
class VRFEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vrf'
class VRFCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vrf'
model = VRF
form_class = forms.VRFForm
template_name = 'ipam/vrf_edit.html'
default_return_url = 'ipam:vrf_list'
class VRFEditView(VRFCreateView):
permission_required = 'ipam.change_vrf'
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_vrf'
model = VRF
@@ -239,8 +243,8 @@ class RIRListView(ObjectListView):
}
class RIREditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_rir'
class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_rir'
model = RIR
form_class = forms.RIRForm
@@ -248,6 +252,10 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
return reverse('ipam:rir_list')
class RIREditView(RIRCreateView):
permission_required = 'ipam.change_rir'
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_rir'
cls = RIR
@@ -324,14 +332,18 @@ class AggregateView(View):
})
class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_aggregate'
class AggregateCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_aggregate'
model = Aggregate
form_class = forms.AggregateForm
template_name = 'ipam/aggregate_edit.html'
default_return_url = 'ipam:aggregate_list'
class AggregateEditView(AggregateCreateView):
permission_required = 'ipam.change_aggregate'
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_aggregate'
model = Aggregate
@@ -371,8 +383,8 @@ class RoleListView(ObjectListView):
template_name = 'ipam/role_list.html'
class RoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_role'
class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_role'
model = Role
form_class = forms.RoleForm
@@ -380,6 +392,10 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('ipam:role_list')
class RoleEditView(RoleCreateView):
permission_required = 'ipam.change_role'
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_role'
cls = Role
@@ -519,14 +535,18 @@ class PrefixIPAddressesView(View):
})
class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_prefix'
class PrefixCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_prefix'
model = Prefix
form_class = forms.PrefixForm
template_name = 'ipam/prefix_edit.html'
default_return_url = 'ipam:prefix_list'
class PrefixEditView(PrefixCreateView):
permission_required = 'ipam.change_prefix'
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_prefix'
model = Prefix
@@ -612,21 +632,25 @@ class IPAddressView(View):
})
class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_ipaddress'
class IPAddressCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_ipaddress'
model = IPAddress
form_class = forms.IPAddressForm
template_name = 'ipam/ipaddress_edit.html'
default_return_url = 'ipam:ipaddress_list'
class IPAddressEditView(IPAddressCreateView):
permission_required = 'ipam.change_ipaddress'
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_ipaddress'
model = IPAddress
default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView):
permission_required = 'ipam.add_ipaddress'
pattern_form = forms.IPAddressPatternForm
model_form = forms.IPAddressBulkAddForm
@@ -641,19 +665,6 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
table = tables.IPAddressTable
default_return_url = 'ipam:ipaddress_list'
def save_obj(self, obj):
obj.save()
# Update primary IP for device if needed. The Device must be updated directly in the database; otherwise we risk
# overwriting a previous IP assignment from the same import (see #861).
try:
if obj.family == 4 and obj.primary_ip4_for:
Device.objects.filter(pk=obj.primary_ip4_for.pk).update(primary_ip4=obj)
elif obj.family == 6 and obj.primary_ip6_for:
Device.objects.filter(pk=obj.primary_ip6_for.pk).update(primary_ip6=obj)
except Device.DoesNotExist:
pass
class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_ipaddress'
@@ -683,8 +694,8 @@ class VLANGroupListView(ObjectListView):
template_name = 'ipam/vlangroup_list.html'
class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlangroup'
class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vlangroup'
model = VLANGroup
form_class = forms.VLANGroupForm
@@ -692,6 +703,10 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('ipam:vlangroup_list')
class VLANGroupEditView(VLANGroupCreateView):
permission_required = 'ipam.change_vlangroup'
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlangroup'
cls = VLANGroup
@@ -728,14 +743,18 @@ class VLANView(View):
})
class VLANEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlan'
class VLANCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_vlan'
model = VLAN
form_class = forms.VLANForm
template_name = 'ipam/vlan_edit.html'
default_return_url = 'ipam:vlan_list'
class VLANEditView(VLANCreateView):
permission_required = 'ipam.change_vlan'
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_vlan'
model = VLAN
@@ -769,8 +788,8 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Services
#
class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_service'
class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.add_service'
model = Service
form_class = forms.ServiceForm
template_name = 'ipam/service_edit.html'
@@ -784,6 +803,10 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
return obj.device.get_absolute_url()
class ServiceEditView(ServiceCreateView):
permission_required = 'ipam.change_service'
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_service'
model = Service

View File

@@ -58,6 +58,11 @@ CORS_ORIGIN_REGEX_WHITELIST = [
# r'^(https?://)?(\w+\.)?example\.com$',
]
# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal
# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging
# on a production system.
DEBUG = False
# Email settings
EMAIL = {
'SERVER': 'localhost',
@@ -72,6 +77,10 @@ EMAIL = {
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
ENFORCE_GLOBAL_UNIQUE = False
# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
# https://docs.djangoproject.com/en/1.11/topics/logging/
LOGGING = {}
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
# are permitted to access most data in NetBox (excluding secrets) but not make any changes.
LOGIN_REQUIRED = False

View File

@@ -13,9 +13,9 @@ except ImportError:
)
VERSION = '2.0.5'
VERSION = '2.0.8'
# Import local configuration
# Import required configuration parameters
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
try:
@@ -25,33 +25,35 @@ for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
"Mandatory setting {} is missing from configuration.py.".format(setting)
)
# Default configurations
# Import optional configuration parameters
ADMINS = getattr(configuration, 'ADMINS', [])
DEBUG = getattr(configuration, 'DEBUG', False)
EMAIL = getattr(configuration, 'EMAIL', {})
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
BASE_PATH = getattr(configuration, 'BASE_PATH', '')
if BASE_PATH:
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
DEBUG = getattr(configuration, 'DEBUG', False)
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
EMAIL = getattr(configuration, 'EMAIL', {})
LOGGING = getattr(configuration, 'LOGGING', {})
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
# Attempt to import LDAP configuration if it has been defined

View File

@@ -1,6 +1,7 @@
$(document).ready(function() {
var search_field = $('#id_livesearch');
var real_field = $('#id_' + search_field.attr('data-field'));
var select_fields = $('#select select');
var search_key = search_field.attr('data-key');
var label = search_field.attr('data-label');
if (!label) {
@@ -40,13 +41,22 @@ $(document).ready(function() {
select: function(event, ui) {
event.preventDefault();
search_field.val(ui.item.label);
select_fields.val('');
select_fields.attr('disabled', 'disabled');
real_field.empty();
real_field.append($("<option></option>").attr('value', ui.item.value).text(ui.item.label));
real_field.change();
// If the field has a parent helper, reset the parent to no selection
$('select[filter-for="' + real_field.attr('name') + '"]').val('');
// Disable parent selection fields
// $('select[filter-for="' + real_field.attr('name') + '"]').val('');
},
minLength: 4,
delay: 500
});
search_field.change(function() {
if (!search_field.val()) {
select_fields.removeAttr('disabled');
select_fields.val('');
}
});
});

View File

@@ -291,6 +291,7 @@ class Secret(CreatedUpdatedModel):
hash = models.CharField(max_length=128, editable=False)
plaintext = None
csv_headers = ['device', 'role', 'name', 'plaintext']
class Meta:
ordering = ['device', 'role', 'name']

View File

@@ -10,7 +10,7 @@ urlpatterns = [
# Secret roles
url(r'^secret-roles/$', views.SecretRoleListView.as_view(), name='secretrole_list'),
url(r'^secret-roles/add/$', views.SecretRoleEditView.as_view(), name='secretrole_add'),
url(r'^secret-roles/add/$', views.SecretRoleCreateView.as_view(), name='secretrole_add'),
url(r'^secret-roles/delete/$', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'),
url(r'^secret-roles/(?P<slug>[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'),

View File

@@ -40,8 +40,8 @@ class SecretRoleListView(ObjectListView):
template_name = 'secrets/secretrole_list.html'
class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'secrets.change_secretrole'
class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'secrets.add_secretrole'
model = SecretRole
form_class = forms.SecretRoleForm
@@ -49,6 +49,10 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('secrets:secretrole_list')
class SecretRoleEditView(SecretRoleCreateView):
permission_required = 'secrets.change_secretrole'
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'secrets.delete_secretrole'
cls = SecretRole

View File

@@ -0,0 +1,13 @@
{% extends 'utilities/confirmation_form.html' %}
{% load helpers %}
{% block title %}Disconnect {{ obj_type_plural|bettertitle }}{% endblock %}
{% block message %}
<p>Are you sure you want to disconnect all {{ selected_objects|length }} of these {{ obj_type_plural }} on <strong>{{ device }}</strong>?</p>
<ul>
{% for obj in selected_objects %}
<li>{{ obj }}</li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -204,7 +204,7 @@
None
</div>
{% endif %}
{% if perms.dcim.add_service %}
{% if perms.ipam.add_service %}
<div class="panel-footer text-right">
<a href="{% url 'dcim:service_assign' device=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
@@ -424,12 +424,17 @@
<div class="panel-footer">
{% if interfaces and perms.dcim.change_interface %}
<button type="submit" name="_edit" formaction="{% url 'dcim:interface_bulk_edit' pk=device.pk %}" class="btn btn-warning btn-xs">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit selected
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button>
{% endif %}
{% if interfaces and perms.dcim.delete_interfaceconnection %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
{% if interfaces and perms.dcim.delete_interface %}
<button type="submit" name="_delete" formaction="{% url 'dcim:interface_bulk_delete' pk=device.pk %}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button>
{% endif %}
{% if perms.dcim.add_interface %}
@@ -479,9 +484,14 @@
</table>
{% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
<div class="panel-footer">
{% if cs_ports and perms.dcim.change_consoleport %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
{% if cs_ports and perms.dcim.delete_consoleserverport %}
<button type="submit" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button>
{% endif %}
{% if perms.dcim.add_consoleserverport %}
@@ -531,9 +541,14 @@
</table>
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
<div class="panel-footer">
{% if power_outlets and perms.dcim.change_powerport %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
</button>
{% endif %}
{% if power_outlets and perms.dcim.delete_poweroutlet %}
<button type="submit" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete selected
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete
</button>
{% endif %}
{% if perms.dcim.add_poweroutlet %}
@@ -576,7 +591,7 @@ function toggleConnection(elem, api_url) {
success: function() {
elem.parents('tr').removeClass('success').addClass('info');
elem.removeClass('connected btn-warning').addClass('btn-success');
elem.attr('title', 'Mark connected');
elem.attr('title', 'Mark installed');
elem.children('i').removeClass('glyphicon glyphicon-ban-circle').addClass('fa fa-plug')
}
});
@@ -595,7 +610,7 @@ function toggleConnection(elem, api_url) {
success: function() {
elem.parents('tr').removeClass('info').addClass('success');
elem.removeClass('btn-success').addClass('connected btn-warning');
elem.attr('title', 'Mark disconnected');
elem.attr('title', 'Mark planned');
elem.children('i').removeClass('fa fa-plug').addClass('glyphicon glyphicon-ban-circle')
}
});

View File

@@ -24,24 +24,24 @@
{% if perms.dcim.change_consoleport %}
{% if cp.cs_port %}
{% if cp.connection_status %}
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" data="{{ cp.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true" title="Mark planned"></i>
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" title="Mark planned" data="{{ cp.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</a>
{% else %}
<a href="#" class="btn btn-success btn-xs consoleport-toggle" data="{{ cp.pk }}">
<i class="fa fa-plug" aria-hidden="true" title="Mark connected"></i>
<a href="#" class="btn btn-success btn-xs consoleport-toggle" title="Mark installed" data="{{ cp.pk }}">
<i class="fa fa-plug" aria-hidden="true"></i>
</a>
{% endif %}
<a href="{% url 'dcim:consoleport_disconnect' pk=cp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
<a href="{% url 'dcim:consoleport_disconnect' pk=cp.pk %}" title="Delete connection" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a>
{% else %}
<a href="{% url 'dcim:consoleport_connect' pk=cp.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
<a href="{% url 'dcim:consoleport_connect' pk=cp.pk %}" title="Connect" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a>
{% endif %}
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit port"></i>
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" title="Edit port" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.dcim.delete_consoleport %}
@@ -50,8 +50,8 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:consoleport_delete' pk=cp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
<a href="{% url 'dcim:consoleport_delete' pk=cp.pk %}" title="Delete port" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>
{% endif %}
{% endif %}

View File

@@ -24,24 +24,24 @@
{% if perms.dcim.change_consoleserverport %}
{% if csp.connected_console %}
{% if csp.connected_console.connection_status %}
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" data="{{ csp.connected_console.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true" title="Mark planned"></i>
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" title="Mark planned" data="{{ csp.connected_console.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</a>
{% else %}
<a href="#" class="btn btn-success btn-xs consoleport-toggle" data="{{ csp.connected_console.pk }}">
<i class="fa fa-plug" aria-hidden="true" title="Mark connected"></i>
<a href="#" class="btn btn-success btn-xs consoleport-toggle" title="Mark installed" data="{{ csp.connected_console.pk }}">
<i class="fa fa-plug" aria-hidden="true"></i>
</a>
{% endif %}
<a href="{% url 'dcim:consoleserverport_disconnect' pk=csp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
<a href="{% url 'dcim:consoleserverport_disconnect' pk=csp.pk %}" title="Delete connection" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a>
{% else %}
<a href="{% url 'dcim:consoleserverport_connect' pk=csp.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
<a href="{% url 'dcim:consoleserverport_connect' pk=csp.pk %}" title="Connect" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a>
{% endif %}
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit port"></i>
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" title="Edit port" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.dcim.delete_consoleserverport %}
@@ -50,8 +50,8 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:consoleserverport_delete' pk=csp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
<a href="{% url 'dcim:consoleserverport_delete' pk=csp.pk %}" title="Delete port" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>
{% endif %}
{% endif %}

View File

@@ -45,7 +45,7 @@
<ul class="nav nav-tabs" style="margin-bottom: 20px">
<li role="presentation"{% if active_tab == 'info' %} class="active"{% endif %}><a href="{% url 'dcim:device' pk=device.pk %}">Info</a></li>
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}><a href="{% url 'dcim:device_inventory' pk=device.pk %}">Inventory</a></li>
{% if device.status %}
{% if device.status == 1 and device.platform.rpc_client and device.primary_ip %}
<li role="presentation"{% if active_tab == 'lldp-neighbors' %} class="active"{% endif %}><a href="{% url 'dcim:device_lldp_neighbors' pk=device.pk %}">LLDP Neighbors</a></li>
{% endif %}
</ul>

View File

@@ -1,4 +1,4 @@
<tr class="interface{% if iface.connection and not iface.connection.connection_status %} info{% endif %}">
<tr class="interface{% if iface.connection and iface.connection.connection_status %} success{% elif 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 }}" />
@@ -72,7 +72,7 @@
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</a>
{% else %}
<a href="#" class="btn btn-success btn-xs interface-toggle" data="{{ iface.connection.pk }}" title="Mark connected">
<a href="#" class="btn btn-success btn-xs interface-toggle" data="{{ iface.connection.pk }}" title="Mark installed">
<i class="fa fa-plug" aria-hidden="true"></i>
</a>
{% endif %}

View File

@@ -24,24 +24,24 @@
{% if perms.dcim.change_poweroutlet %}
{% if po.connected_port %}
{% if po.connected_port.connection_status %}
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" data="{{ po.connected_port.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true" title="Mark planned"></i>
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" title="Mark planned" data="{{ po.connected_port.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</a>
{% else %}
<a href="#" class="btn btn-success btn-xs consoleport-toggle" data="{{ po.connected_port.pk }}">
<i class="fa fa-plug" aria-hidden="true" title="Mark connected"></i>
<a href="#" class="btn btn-success btn-xs consoleport-toggle" title="Mark installed" data="{{ po.connected_port.pk }}">
<i class="fa fa-plug" aria-hidden="true"></i>
</a>
{% endif %}
<a href="{% url 'dcim:poweroutlet_disconnect' pk=po.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
<a href="{% url 'dcim:poweroutlet_disconnect' pk=po.pk %}" title="Delete connection" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a>
{% else %}
<a href="{% url 'dcim:poweroutlet_connect' pk=po.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
<a href="{% url 'dcim:poweroutlet_connect' pk=po.pk %}" title="Connect" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a>
{% endif %}
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit outlet"></i>
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" title="Edit outlet" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.dcim.delete_poweroutlet %}
@@ -50,8 +50,8 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:poweroutlet_delete' pk=po.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete outlet"></i>
<a href="{% url 'dcim:poweroutlet_delete' pk=po.pk %}" title="Delete outlet" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>
{% endif %}
{% endif %}

View File

@@ -24,24 +24,24 @@
{% if perms.dcim.change_powerport %}
{% if pp.power_outlet %}
{% if pp.connection_status %}
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" data="{{ pp.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true" title="Mark planned"></i>
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" title="Mark planned" data="{{ pp.pk }}">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</a>
{% else %}
<a href="#" class="btn btn-success btn-xs powerport-toggle" data="{{ pp.pk }}">
<i class="fa fa-plug" aria-hidden="true" title="Mark connected"></i>
<a href="#" class="btn btn-success btn-xs powerport-toggle" title="Mark installed" data="{{ pp.pk }}">
<i class="fa fa-plug" aria-hidden="true"></i>
</a>
{% endif %}
<a href="{% url 'dcim:powerport_disconnect' pk=pp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true" title="Delete connection"></i>
<a href="{% url 'dcim:powerport_disconnect' pk=pp.pk %}" title="Delete connection" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a>
{% else %}
<a href="{% url 'dcim:powerport_connect' pk=pp.pk %}" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true" title="Connect"></i>
<a href="{% url 'dcim:powerport_connect' pk=pp.pk %}" title="Connect" class="btn btn-success btn-xs">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a>
{% endif %}
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit port"></i>
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" title="Edit port" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.dcim.delete_powerport %}
@@ -50,8 +50,8 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button>
{% else %}
<a href="{% url 'dcim:powerport_delete' pk=pp.pk %}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete port"></i>
<a href="{% url 'dcim:powerport_delete' pk=pp.pk %}" title="Delete port" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a>
{% endif %}
{% endif %}

View File

@@ -57,6 +57,12 @@
<a href="{% url 'ipam:aggregate_list' %}?rir={{ aggregate.rir.slug }}">{{ aggregate.rir }}</a>
</td>
</tr>
<tr>
<td>Utilization</td>
<td>
{{ aggregate.get_utilization }}%
</td>
</tr>
<tr>
<td>Date Added</td>
<td>

View File

@@ -121,8 +121,8 @@
</td>
</tr>
<tr>
<td>IP Addresses</td>
<td><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">{{ ipaddress_count }}</a></td>
<td>Utilization</td>
<td><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">{{ ipaddress_count }} IP addresses</a> ({{ prefix.get_utilization }}%)</td>
</tr>
</table>
</div>

View File

@@ -8,13 +8,19 @@
{% render_field form.prefix %}
{% render_field form.status %}
{% render_field form.vrf %}
{% render_field form.site %}
{% render_field form.vlan %}
{% render_field form.role %}
{% render_field form.description %}
{% render_field form.is_pool %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Site/VLAN Assignment</strong></div>
<div class="panel-body">
{% render_field form.site %}
{% render_field form.vlan_group %}
{% render_field form.vlan %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body">

View File

@@ -19,6 +19,12 @@
{% endif %}
</h4>
{% include 'inc/created_updated.html' with obj=userkey %}
{% if not userkey.is_active %}
<div class="alert alert-warning" role="alert">
<i class="fa fa-warning"></i>
Your user key is inactive. Ask an administrator to enable it for you.
</div>
{% endif %}
<pre>{{ userkey.public_key }}</pre>
<hr />
{% if userkey.session_key %}

View File

@@ -41,6 +41,8 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['name', 'slug', 'group', 'description']
class Meta:
ordering = ['group', 'name']

View File

@@ -10,13 +10,13 @@ urlpatterns = [
# Tenant groups
url(r'^tenant-groups/$', views.TenantGroupListView.as_view(), name='tenantgroup_list'),
url(r'^tenant-groups/add/$', views.TenantGroupEditView.as_view(), name='tenantgroup_add'),
url(r'^tenant-groups/add/$', views.TenantGroupCreateView.as_view(), name='tenantgroup_add'),
url(r'^tenant-groups/delete/$', views.TenantGroupBulkDeleteView.as_view(), name='tenantgroup_bulk_delete'),
url(r'^tenant-groups/(?P<slug>[\w-]+)/edit/$', views.TenantGroupEditView.as_view(), name='tenantgroup_edit'),
# Tenants
url(r'^tenants/$', views.TenantListView.as_view(), name='tenant_list'),
url(r'^tenants/add/$', views.TenantEditView.as_view(), name='tenant_add'),
url(r'^tenants/add/$', views.TenantCreateView.as_view(), name='tenant_add'),
url(r'^tenants/import/$', views.TenantBulkImportView.as_view(), name='tenant_import'),
url(r'^tenants/edit/$', views.TenantBulkEditView.as_view(), name='tenant_bulk_edit'),
url(r'^tenants/delete/$', views.TenantBulkDeleteView.as_view(), name='tenant_bulk_delete'),

View File

@@ -26,8 +26,8 @@ class TenantGroupListView(ObjectListView):
template_name = 'tenancy/tenantgroup_list.html'
class TenantGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'tenancy.change_tenantgroup'
class TenantGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'tenancy.add_tenantgroup'
model = TenantGroup
form_class = forms.TenantGroupForm
@@ -35,6 +35,10 @@ class TenantGroupEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('tenancy:tenantgroup_list')
class TenantGroupEditView(TenantGroupCreateView):
permission_required = 'tenancy.change_tenantgroup'
class TenantGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'tenancy.delete_tenantgroup'
cls = TenantGroup
@@ -81,14 +85,18 @@ class TenantView(View):
})
class TenantEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'tenancy.change_tenant'
class TenantCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'tenancy.add_tenant'
model = Tenant
form_class = forms.TenantForm
template_name = 'tenancy/tenant_edit.html'
default_return_url = 'tenancy:tenant_list'
class TenantEditView(TenantCreateView):
permission_required = 'tenancy.change_tenant'
class TenantDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'tenancy.delete_tenant'
model = Tenant

View File

@@ -249,7 +249,7 @@ class CSVDataField(forms.CharField):
reader = csv.reader(value.splitlines())
# Consume and valdiate the first line of CSV data as column headers
headers = reader.next()
headers = next(reader)
for f in self.required_fields:
if f not in headers:
raise forms.ValidationError('Required column header "{}" not found.'.format(f))
@@ -478,8 +478,8 @@ class ChainedFieldsMixin(forms.BaseForm):
filters_dict = {}
for (db_field, parent_field) in field.chains:
if self.is_bound and self.data.get(parent_field):
filters_dict[db_field] = self.data[parent_field]
if self.is_bound and parent_field in self.data:
filters_dict[db_field] = self.data[parent_field] or None
elif self.initial.get(parent_field):
filters_dict[db_field] = self.initial[parent_field]
elif self.fields[parent_field].widget.attrs.get('nullable'):
@@ -489,6 +489,12 @@ class ChainedFieldsMixin(forms.BaseForm):
if filters_dict:
field.queryset = field.queryset.filter(**filters_dict)
elif not self.is_bound and self.instance and hasattr(self.instance, field_name):
obj = getattr(self.instance, field_name)
if obj is not None:
field.queryset = field.queryset.filter(pk=obj.pk)
else:
field.queryset = field.queryset.none()
elif not self.is_bound:
field.queryset = field.queryset.none()

View File

@@ -102,7 +102,9 @@ class ObjectListView(View):
.format(et.name))
# Fall back to built-in CSV export
elif 'export' in request.GET and hasattr(model, 'to_csv'):
output = '\n'.join([obj.to_csv() for obj in self.queryset])
headers = getattr(model, 'csv_headers', None)
output = ','.join(headers) + '\n' if headers else ''
output += '\n'.join([obj.to_csv() for obj in self.queryset])
response = HttpResponse(
output,
content_type='text/csv'
@@ -232,7 +234,7 @@ class ObjectDeleteView(GetReturnURLMixin, View):
"""
Delete a single object.
model: The model of the object being edited
model: The model of the object being deleted
template_name: The name of the template
default_return_url: Name of the URL to which the user is redirected after deleting the object
"""
@@ -288,7 +290,7 @@ class ObjectDeleteView(GetReturnURLMixin, View):
})
class BulkAddView(View):
class BulkCreateView(View):
"""
Create new objects in bulk.

View File

@@ -5,6 +5,25 @@
# Once the script completes, remember to restart the WSGI service (e.g.
# gunicorn or uWSGI).
# Determine which version of Python/pip to use. Default to v3 (if available)
# but allow the user to force v2.
PYTHON="python3"
PIP="pip3"
type $PYTHON >/dev/null 2>&1 && type $PIP >/dev/null 2>&1 || PYTHON="python" PIP="pip"
while getopts ":2" opt; do
case $opt in
2)
PYTHON="python"
PIP="pip"
echo "Forcing Python/pip v2"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit
;;
esac
done
# Optionally use sudo if not already root, and always prompt for password
# before running the command
PREFIX="sudo -k "
@@ -20,12 +39,6 @@ COMMAND="${PREFIX}find . -name \"*.pyc\" -delete"
echo "Cleaning up stale Python bytecode ($COMMAND)..."
eval $COMMAND
# Prefer python3/pip3
PYTHON="python3"
type $PYTHON >/dev/null 2>&1 || PYTHON="python"
PIP="pip3"
type $PIP >/dev/null 2>&1 || PIP="pip"
# Install any new Python packages
COMMAND="${PREFIX}${PIP} install -r requirements.txt --upgrade"
echo "Updating required Python packages ($COMMAND)..."