Compare commits

...

123 Commits

Author SHA1 Message Date
Jeremy Stretch
fa7b7288c9 Merge pull request #1398 from digitalocean/develop
Release v2.1.2
2017-08-04 10:54:29 -04:00
Jeremy Stretch
a7d5fb5006 Release v2.1.2 2017-08-04 10:52:06 -04:00
Jeremy Stretch
dadfcd4f81 Tweaked navigation menu styling 2017-08-03 17:49:22 -04:00
Jeremy Stretch
236229ea7e Closes #992: Allow the creation of multiple services per device with the same protocol and port 2017-08-03 15:40:52 -04:00
Jeremy Stretch
6f0f3cc115 Fixes #1390: Fixed IndexError when viewing available IPs within large IPv6 prefixes 2017-08-03 10:09:37 -04:00
Jeremy Stretch
3aa072b437 Fixes #1388: Fixed server error when searching globally for IPs/prefixes (rolled back #1379) 2017-08-03 09:26:28 -04:00
Jeremy Stretch
9cc03aaa9a Merge pull request #1387 from digitalocean/develop
Release v2.1.1
2017-08-02 14:22:30 -04:00
Jeremy Stretch
930b15ae09 Post-release version bump 2017-08-02 14:20:43 -04:00
Jeremy Stretch
1c1f4068cd Release v2.1.1 2017-08-02 14:20:20 -04:00
Jeremy Stretch
b6690e0012 Closes #1368: Render reservations in rack elevations view 2017-08-02 13:33:44 -04:00
Jeremy Stretch
52e7f670f3 Deleted obsolete template 2017-08-02 13:17:34 -04:00
Jeremy Stretch
12d643694f Cleaned up title blocks 2017-08-02 13:15:28 -04:00
Jeremy Stretch
3876a96b67 Tweaked IP address layout 2017-08-02 12:55:21 -04:00
Jeremy Stretch
e50f234ba3 Closes #1379: Allow searching devices by interface MAC address in global search 2017-08-02 11:54:53 -04:00
Jeremy Stretch
fd6df8e52a Fixes #1385: Connected device API endpoint no longer requires authentication if LOGIN_REQUIRED=False 2017-08-02 11:17:57 -04:00
Jeremy Stretch
28225c7cbd Merge pull request #1386 from bdlamprecht/patch-1
Updated ldap.md
2017-08-02 10:56:41 -04:00
bdlamprecht
cab2929c10 Updated ldap.md
Add mapping of `email` field to `mail` value in LDAP.  Also remove duplicate entries of such mapping information.
2017-08-02 08:45:00 -06:00
Jeremy Stretch
52a490bf5d Fixes #461: Display a validation error when attempting to assigning a new child device to a rack face/position 2017-07-28 10:05:51 -04:00
Jeremy Stretch
f2c16fbf3c Closes #893: Allow filtering by null values for NullCharacterFields (e.g. return only unnamed devices) 2017-07-27 09:53:41 -04:00
Jeremy Stretch
65a633f42d Closes #1374: Added NAPALM_ARGS and NAPALM_TIMEOUT configiuration parameters 2017-07-26 11:47:59 -04:00
Jeremy Stretch
d2c6d79589 Closes #1375: Renamed NETBOX_USERNAME and NETBOX_PASSWORD configuration parameters to NAPALM_USERNAME and NAPALM_PASSWORD 2017-07-26 11:24:19 -04:00
Jeremy Stretch
f4a873745f Post-release version bump 2017-07-25 11:26:50 -04:00
Jeremy Stretch
1bda56ea23 Merge pull request #1372 from digitalocean/develop
Release v2.1.0
2017-07-25 11:21:44 -04:00
Jeremy Stretch
c7e9d90321 Release v2.1.0 2017-07-25 11:19:33 -04:00
Jeremy Stretch
7476194bd1 PEP8 fix 2017-07-25 10:58:28 -04:00
Jeremy Stretch
1770c85689 Fixes #1371: Extend DeviceSerializer.parent_device to include standard fields 2017-07-25 10:56:23 -04:00
Jeremy Stretch
e364659c6e Tweaked NAPALM integration instructions 2017-07-25 10:17:28 -04:00
Jeremy Stretch
9e26198afe Added note about NAPALM integration 2017-07-25 10:09:44 -04:00
Jeremy Stretch
f9b6ddc230 Added "Migrating to Python3" to the docs index 2017-07-25 09:44:15 -04:00
vanderaaj
0991f94d06 How to migrate from Py2 to Py3 (#1355)
* How to migrate from Py2 to Py3

The commands done to migrate Ubuntu from Py2 to Py3.

* Update Migrating-to-Python3
2017-07-25 09:40:51 -04:00
Jeremy Stretch
32513083b1 Merge branch 'develop-2.1' into develop 2017-07-24 14:58:18 -04:00
Jeremy Stretch
336cdcddc5 PEP8 fix 2017-07-24 14:51:00 -04:00
Jeremy Stretch
4047c1a4e4 lsmodules() should only return native models 2017-07-24 14:34:01 -04:00
Jeremy Stretch
091cf390d2 Import constants from each app 2017-07-24 14:22:07 -04:00
Jeremy Stretch
05aaafc1cf Added docs for using the NetBox shell 2017-07-24 13:26:31 -04:00
Jeremy Stretch
5885b833cd Fixes #1362: Raise validation error when attempting to create an API key that's too short 2017-07-19 11:03:13 -04:00
Jeremy Stretch
106627da04 Fixes #1358: Correct VRF example values in IP/prefix import forms 2017-07-18 10:39:09 -04:00
Jeremy Stretch
d73ea54e08 Fixed table cell alignment for IP addresses 2017-07-17 13:55:20 -04:00
Jeremy Stretch
a45bfaf3da Hide/disable NAPALM tabs as appropriate 2017-07-17 13:29:11 -04:00
Jeremy Stretch
e85cc0d856 Removed legacy LLDP neighbors API endpoint 2017-07-17 13:21:38 -04:00
Jeremy Stretch
0f608f3a15 Added device config view 2017-07-17 13:19:25 -04:00
Jeremy Stretch
4ad5c6f864 Updated LLDP neighbors view to use NAPALM API 2017-07-17 13:05:11 -04:00
Jeremy Stretch
be47b6a6c0 Added device environmental status details 2017-07-17 12:58:13 -04:00
Jeremy Stretch
1f982c94ce Added an AJAX spinner 2017-07-17 11:41:39 -04:00
Jeremy Stretch
12472a2612 Live device status PoC 2017-07-14 16:07:28 -04:00
Jeremy Stretch
f6a8d32880 Initial work on NAPALM integration 2017-07-14 14:42:56 -04:00
Jeremy Stretch
bb2f86463e Upgraded jQuery to v3.2.1 2017-07-14 10:17:09 -04:00
Jeremy Stretch
e8dafc02f7 Merge branch 'develop' into develop-2.1
Conflicts:
	netbox/netbox/settings.py
2017-07-14 10:12:35 -04:00
Jeremy Stretch
0655834938 Post-release version bump 2017-07-14 10:11:04 -04:00
Jeremy Stretch
64a34ced72 Merge pull request #1346 from digitalocean/develop
Release v2.0.10
2017-07-14 10:09:16 -04:00
Jeremy Stretch
d0dc505220 Release v2.0.10 2017-07-14 10:07:21 -04:00
Jeremy Stretch
b2d3f3ff22 Tweaked page title 2017-07-14 10:01:59 -04:00
Jeremy Stretch
39730b6834 Optimized performance when editing/deleting objects in bulk 2017-07-13 17:39:28 -04:00
Jeremy Stretch
dd1991f2c6 Closes #838: Display details of all objects being edited/deleted in bulk 2017-07-13 16:31:47 -04:00
Jeremy Stretch
2f32e11f53 Fixes #1342: Allow designation of users and groups when creating/editing a secret role 2017-07-13 11:44:29 -04:00
Jeremy Stretch
280f55a875 Require django-tables2 v1.7+ 2017-07-13 11:39:59 -04:00
Jeremy Stretch
dc68be5abf Removed SearchTables; created DetailTables for models where needed 2017-07-12 16:42:45 -04:00
Jeremy Stretch
1ef90902bd Closes #1320: Remove checkbox from confirmation dialog 2017-07-12 14:53:52 -04:00
Jeremy Stretch
6f37e97c67 Fixes #1339: Fixed disappearing checkbox column under django-tables2 v1.7+ 2017-07-12 14:05:01 -04:00
Jeremy Stretch
e54c74d972 Fixes #1338: Allow importing prefixes with "container" status 2017-07-12 10:31:16 -04:00
Jeremy Stretch
af9fa85cc1 Fixes #1312: Catch error when attempting to activate a user key with an invalid private key 2017-07-12 10:06:13 -04:00
Jeremy Stretch
74828e1409 Fixes #1334: Fix server error when adding an interface to a device 2017-07-11 14:52:50 -04:00
Jeremy Stretch
dc77400ab1 Fixes #1333: Corrected label on is_console_server field of DeviceType bulk edit form 2017-07-11 14:36:59 -04:00
Jeremy Stretch
2d0638821d #1266: Exclude interfaces with existing connections or circuit terminations when creating a new connection 2017-07-10 12:44:16 -04:00
Jeremy Stretch
8a87d60f29 Closes #1269: Added circuit termination to interface serializer 2017-07-10 12:07:47 -04:00
Jeremy Stretch
530789b733 #1269: Reworked interface connection serialization 2017-07-10 11:52:36 -04:00
Jeremy Stretch
618d75566f Merge branch 'develop' into develop-2.1
Conflicts:
	netbox/netbox/settings.py
2017-07-10 09:48:20 -04:00
Jeremy Stretch
5f66893038 Post-release version bump 2017-07-10 09:44:34 -04:00
Jeremy Stretch
e05d379101 Merge pull request #1327 from digitalocean/develop
Release v2.0.9
2017-07-10 09:43:59 -04:00
Jeremy Stretch
41ea433e7c Release v2.0.9 2017-07-10 09:42:07 -04:00
Jeremy Stretch
bfd7881b7b Fixes #1325: Retain interface attachment when editing a circuit termination 2017-07-10 09:38:59 -04:00
Jeremy Stretch
1f9806a480 Fixes #1285: Enforce model validation when creating/editing objects via the API 2017-07-06 17:37:24 -04:00
Jeremy Stretch
5b43a108bc Merge branch 'develop' into develop-2.1
Conflicts:
	netbox/ipam/models.py
	netbox/netbox/settings.py
	netbox/templates/dcim/inc/interface.html
2017-07-06 13:27:13 -04:00
Jeremy Stretch
b253c8cc95 Fixes #1319: Fixed server error when attempting to create console/power connections 2017-07-06 13:20:53 -04:00
Jeremy Stretch
0fc9ed852e Fixed typo in example 2017-07-06 13:14:10 -04:00
Jeremy Stretch
175c1f2720 Post-release version bump 2017-07-05 14:36:25 -04:00
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
30d1605007 Closes #1246: Added ability to auto-create the next available IP address within a prefix 2017-06-30 16:51:31 -04: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
a23da9f867 PEP8 fixes 2017-06-28 16:25:36 -04:00
Jeremy Stretch
d5bb37b552 #1246: Initial work on an API endpoint to retrieve available IPs for a prefix 2017-06-28 16:23:17 -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
5940feb64b Closes #1121: Added asset_tag and description fields to inventory items 2017-06-23 17:05:37 -04:00
Jeremy Stretch
229e6809d8 Closes #1041: Added enabled and MTU fields to the interface model 2017-06-23 14:04:15 -04:00
Jeremy Stretch
87e5687d03 Closes #1203: Implemented query filters for all models 2017-06-19 16:10:18 -04:00
Jeremy Stretch
5aba1d9aec Fixes #1288: Corrected permission name for deleting image attachments 2017-06-19 09:20:03 -04:00
Jeremy Stretch
68ebe85a98 Closes #1218: Added IEEE 802.11 wireless interface types 2017-06-16 17:52:09 -04:00
Jeremy Stretch
789ac5dfd4 Combined mgmt and non-mgmt interfaces into same list on device and device type views 2017-06-16 17:13:33 -04:00
Jeremy Stretch
ceb8fee0cc Moved constant definitions from models.py to constants.py 2017-06-16 16:01:44 -04:00
Jeremy Stretch
421270f4a6 Renamed IP address status 'virtual' to 'VIP' 2017-06-16 15:37:46 -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
ba8f48af65 Merge branch 'develop' into develop-2.1
Conflicts:
	netbox/netbox/settings.py
2017-06-15 14:26:02 -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
f427c00d94 Closes #819: Implemented IP address functional roles 2017-06-14 16:11:13 -04:00
Jeremy Stretch
8bcd8c404d Closes #1141: Include VRF name and RD in form selections 2017-06-14 15:00:27 -04:00
Jeremy Stretch
4d7f9c42c8 Version bump for v2.1 2017-06-14 14:55:59 -04:00
Jeremy Stretch
29a71fd903 #1265: Improved livesearch UI Javascript 2017-06-14 14:50:12 -04:00
Jeremy Stretch
cd263484c3 Fixes #1079: Order interfaces naturally via API 2017-06-14 14:34:14 -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
172 changed files with 3007 additions and 1932 deletions

View File

@@ -135,11 +135,53 @@ An API consumer can request an arbitrary number of objects by appending the "lim
--- ---
## NETBOX_USERNAME ## NAPALM_USERNAME
## NETBOX_PASSWORD ## NAPALM_PASSWORD
If provided, NetBox will use these credentials to authenticate against devices when collecting data. NetBox will use these credentials when authenticating to remote devices via the [NAPALM library](https://napalm-automation.net/), if installed. Both parameters are optional.
Note: If SSH public key authentication has been set up for the system account under which NetBox runs, these parameters are not needed.
---
## NAPALM_ARGS
A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](http://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example:
```
NAPALM_ARGS = {
'api_key': '472071a93b60a1bd1fafb401d9f8ef41',
'port': 2222,
}
```
Note: Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument:
```
NAPALM_USERNAME = 'username'
NAPALM_PASSWORD = 'MySecretPassword'
NAPALM_ARGS = {
'secret': NAPALM_PASSWORD,
# Include any additional args here
}
```
---
## NAPALM_TIMEOUT
Default: 30 seconds
The amount of time (in seconds) to wait for NAPALM to connect to a device.
---
## NETBOX_USERNAME (Deprecated)
## NETBOX_PASSWORD (Deprecated)
These settings have been deprecated and will be removed in NetBox v2.2. Please use `NAPALM_USERNAME` and `NAPALM_PASSWORD` instead.
--- ---

View File

@@ -119,7 +119,7 @@ Each line of the **device patterns** field represents a hierarchical layer withi
``` ```
core-switch-[abcd] core-switch-[abcd]
dist-switch\d dist-switch\d
access-switch\d+,oob-switch\d+ access-switch\d+;oob-switch\d+
``` ```
Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those. Note that you can combine multiple regexes onto one line using semicolons. The order in which regexes are listed on a line is significant: devices matching the first regex will be rendered first, and subsequent groups will be rendered to the right of those.

View File

@@ -72,7 +72,8 @@ AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com"
# You can map user attributes to Django attributes as so. # You can map user attributes to Django attributes as so.
AUTH_LDAP_USER_ATTR_MAP = { AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName", "first_name": "givenName",
"last_name": "sn" "last_name": "sn",
"email": "mail"
} }
``` ```
@@ -108,12 +109,3 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
* `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in. * `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
* `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_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. * `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
It is also possible map user attributes to Django attributes:
```python
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
}
```

View File

@@ -0,0 +1,33 @@
# Migration
Remove Python 2 packages
```no-highlight
# apt-get remove --purge -y python-dev python-pip
```
Install Python 3 packages
```no-highlight
# apt-get install -y python3 python3-dev python3-pip
```
Install Python Packages
```no-highlight
# cd /opt/netbox
# pip3 install -r requirements.txt
```
Gunicorn Update
```no-highlight
# pip uninstall gunicorn
# pip3 install gunicorn
```
Re-install LDAP Module (optional if using LDAP for auth)
```no-highlight
sudo pip3 install django-auth-ldap
```

View File

@@ -1,12 +1,11 @@
# Installation # Installation
**Debian/Ubuntu** **Ubuntu**
Python 3: Python 3:
```no-highlight ```no-highlight
# apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev # 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: 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 # 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: 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: If `git` is not already installed, install it:
**Debian/Ubuntu** **Ubuntu**
```no-highlight ```no-highlight
# apt-get install -y git # apt-get install -y git
``` ```
**CentOS/RHEL** **CentOS**
```no-highlight ```no-highlight
# yum install -y git # yum install -y git
@@ -98,6 +97,14 @@ Python 2:
# pip install -r requirements.txt # pip install -r requirements.txt
``` ```
### NAPALM Automation
As of v2.1.0, NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API. Installation of NAPALM is optional. To enable it, install the `napalm` package using pip or pip3:
```no-highlight
# pip install napalm
```
# Configuration # Configuration
Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`. Move into the NetBox configuration directory and make a copy of `configuration.example.py` named `configuration.py`.
@@ -150,11 +157,14 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a
# Run Database Migrations # 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 ```no-highlight
# cd /opt/netbox/netbox/ # cd /opt/netbox/netbox/
# ./manage.py migrate # python manage.py migrate
Operations to perform: Operations to perform:
Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users
Running migrations: Running migrations:
@@ -172,7 +182,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: 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 ```no-highlight
# ./manage.py createsuperuser # python manage.py createsuperuser
Username: admin Username: admin
Email address: admin@example.com Email address: admin@example.com
Password: Password:
@@ -183,7 +193,7 @@ Superuser created successfully.
# Collect Static Files # Collect Static Files
```no-highlight ```no-highlight
# ./manage.py collectstatic --no-input # python manage.py collectstatic --no-input
You have requested to collect static files at the destination You have requested to collect static files at the destination
location as specified in your settings: location as specified in your settings:
@@ -204,7 +214,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. 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 ```no-highlight
# ./manage.py loaddata initial_data # python manage.py loaddata initial_data
Installed 43 object(s) from 4 fixture(s) Installed 43 object(s) from 4 fixture(s)
``` ```
@@ -213,7 +223,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: At this point, NetBox should be able to run. We can verify this by starting a development instance:
```no-highlight ```no-highlight
# ./manage.py runserver 0.0.0.0:8000 --insecure # python manage.py runserver 0.0.0.0:8000 --insecure
Performing system checks... Performing system checks...
System check identified no issues (0 silenced). 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).) 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 # Installation
**Debian/Ubuntu** **Ubuntu**
```no-highlight ```no-highlight
# apt-get update # apt-get update
# apt-get install -y postgresql libpq-dev # apt-get install -y postgresql libpq-dev
``` ```
**CentOS/RHEL** **CentOS**
```no-highlight ```no-highlight
# yum install -y postgresql postgresql-server postgresql-devel # yum install -y postgresql postgresql-server postgresql-devel

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. 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 !!! 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 ```no-highlight
# apt-get install -y gunicorn supervisor # apt-get install -y gunicorn supervisor

194
docs/shell/intro.md Normal file
View File

@@ -0,0 +1,194 @@
NetBox includes a Python shell withing which objects can be directly queried, created, modified, and deleted. To enter the shell, run the following command:
```
./manage.py nbshell
```
This will launch a customized version of [the built-in Django shell](https://docs.djangoproject.com/en/dev/ref/django-admin/#shell) with all relevant NetBox models pre-loaded. (If desired, the stock Django shell is also available by executing `./manage.py shell`.)
```
$ ./manage.py nbshell
### NetBox interactive shell (jstretch-laptop)
### Python 2.7.6 | Django 1.11.3 | NetBox 2.1.0-dev
### lsmodels() will show available models. Use help(<model>) for more info.
```
The function `lsmodels()` will print a list of all available NetBox models:
```
>>> lsmodels()
DCIM:
ConsolePort
ConsolePortTemplate
ConsoleServerPort
ConsoleServerPortTemplate
Device
...
```
## Querying Objects
Objects are retrieved by forming a [Django queryset](https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-objects). The base queryset for an object takes the form `<model>.objects.all()`, which will return a (truncated) list of all objects of that type.
```
>>> Device.objects.all()
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>, <Device: TestDevice4>, <Device: TestDevice5>, '...(remaining elements truncated)...']>
```
Use a `for` loop to cycle through all objects in the list:
```
>>> for device in Device.objects.all():
... print(device.name, device.device_type)
...
(u'TestDevice1', <DeviceType: PacketThingy 9000>)
(u'TestDevice2', <DeviceType: PacketThingy 9000>)
(u'TestDevice3', <DeviceType: PacketThingy 9000>)
(u'TestDevice4', <DeviceType: PacketThingy 9000>)
(u'TestDevice5', <DeviceType: PacketThingy 9000>)
...
```
To count all objects matching the query, replace `all()` with `count()`:
```
>>> Device.objects.count()
1274
```
To retrieve a particular object (typically by its primary key or other unique field), use `get()`:
```
>>> Site.objects.get(pk=7)
<Site: Test Lab>
```
### Filtering Querysets
In most cases, you want to retrieve only a specific subset of objects. To filter a queryset, replace `all()` with `filter()` and pass one or more keyword arguments. For example:
```
>>> Device.objects.filter(status=STATUS_ACTIVE)
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>, <Device: TestDevice8>, <Device: TestDevice9>, '...(remaining elements truncated)...']>
```
Querysets support slicing to return a specific range of objects.
```
>>> Device.objects.filter(status=STATUS_ACTIVE)[:3]
<QuerySet [<Device: TestDevice1>, <Device: TestDevice2>, <Device: TestDevice3>]>
```
The `count()` method can be appended to the queryset to return a count of objects rather than the full list.
```
>>> Device.objects.filter(status=STATUS_ACTIVE).count()
982
```
Relationships with other models can be traversed by concatenting field names with a double-underscore. For example, the following will return all devices assigned to the tenant named "Pied Piper."
```
>>> Device.objects.filter(tenant__name='Pied Piper')
```
This approach can span multiple levels of relations. For example, the following will return all IP addresses assigned to a device in North America:
```
>>> IPAddress.objects.filter(interface__device__site__region__slug='north-america')
```
!!! note
While the above query is functional, it is very inefficient. There are ways to optimize such requests, however they are out of the scope of this document. For more information, see the [Django queryset method reference](https://docs.djangoproject.com/en/dev/ref/models/querysets/) documentation.
Reverse relationships can be traversed as well. For example, the following will find all devices with an interface named "em0":
```
>>> Device.objects.filter(interfaces__name='em0')
```
Character fields can be filtered against partial matches using the `contains` or `icontains` field lookup (the later of which is case-insensitive).
```
>>> Device.objects.filter(name__icontains='testdevice')
```
Similarly, numeric fields can be filtered by values less than, greater than, and/or equal to a given value.
```
>>> VLAN.objects.filter(vid__gt=2000)
```
Multiple filters can be combined to further refine a queryset.
```
>>> VLAN.objects.filter(vid__gt=2000, name__icontains='engineering')
```
To return the inverse of a filtered queryset, use `exclude()` instead of `filter()`.
```
>>> Device.objects.count()
4479
>>> Device.objects.filter(status=STATUS_ACTIVE).count()
4133
>>> Device.objects.exclude(status=STATUS_ACTIVE).count()
346
```
!!! info
The examples above are intended only to provide a cursory introduction to queryset filtering. For an exhaustive list of the available filters, please consult the [Django queryset API docs](https://docs.djangoproject.com/en/dev/ref/models/querysets/).
## Creating and Updating Objects
New objects can be created by instantiating the desired model, defining values for all required attributes, and calling `save()` on the instance.
```
>>> lab1 = Site.objects.get(pk=7)
>>> myvlan = VLAN(vid=123, name='MyNewVLAN', site=lab1)
>>> myvlan.save()
```
Alternatively, the above can be performed as a single operation:
```
>>> VLAN(vid=123, name='MyNewVLAN', site=Site.objects.get(pk=7)).save()
```
To modify an object, retrieve it, update the desired field(s), and call `save()` again.
```
>>> vlan = VLAN.objects.get(pk=1280)
>>> vlan.name
u'MyNewVLAN'
>>> vlan.name = 'BetterName'
>>> vlan.save()
>>> VLAN.objects.get(pk=1280).name
u'BetterName'
```
!!! warning
The Django ORM provides methods to create/edit many objects at once, namely `bulk_create()` and `update()`. These are best avoided in most cases as they bypass a model's built-in validation and can easily lead to database corruption if not used carefully.
## Deleting Objects
To delete an object, simply call `delete()` on its instance. This will return a dictionary of all objects (including related objects) which have been deleted as a result of this operation.
```
>>> vlan
<VLAN: 123 (BetterName)>
>>> vlan.delete()
(1, {u'extras.CustomFieldValue': 0, u'ipam.VLAN': 1})
```
To delete multiple objects at once, call `delete()` on a filtered queryset. It's a good idea to always sanity-check the count of selected objects _before_ deleting them.
```
>>> Device.objects.filter(name__icontains='test').count()
27
>>> Device.objects.filter(name__icontains='test').delete()
(35, {u'extras.CustomFieldValue': 0, u'dcim.DeviceBay': 0, u'secrets.Secret': 0, u'dcim.InterfaceConnection': 4, u'extras.ImageAttachment': 0, u'dcim.Device': 27, u'dcim.Interface': 4, u'dcim.ConsolePort': 0, u'dcim.PowerPort': 0})
```
!!! warning
Deletions are immediate and irreversible. Always think very carefully before calling `delete()` on an instance or queryset.

View File

@@ -8,6 +8,7 @@ pages:
- 'Web Server': 'installation/web-server.md' - 'Web Server': 'installation/web-server.md'
- 'LDAP (Optional)': 'installation/ldap.md' - 'LDAP (Optional)': 'installation/ldap.md'
- 'Upgrading': 'installation/upgrading.md' - 'Upgrading': 'installation/upgrading.md'
- 'Migrating to Python3': 'installation/migrating-to-python3.md'
- 'Configuration': - 'Configuration':
- 'Mandatory Settings': 'configuration/mandatory-settings.md' - 'Mandatory Settings': 'configuration/mandatory-settings.md'
- 'Optional Settings': 'configuration/optional-settings.md' - 'Optional Settings': 'configuration/optional-settings.md'
@@ -23,6 +24,8 @@ pages:
- 'Authentication': 'api/authentication.md' - 'Authentication': 'api/authentication.md'
- 'Working with Secrets': 'api/working-with-secrets.md' - 'Working with Secrets': 'api/working-with-secrets.md'
- 'Examples': 'api/examples.md' - 'Examples': 'api/examples.md'
- 'Shell':
- 'Introduction': 'shell/intro.md'
markdown_extensions: markdown_extensions:
- admonition: - admonition:

View File

@@ -6,6 +6,7 @@ from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ModelValidationMixin
# #
@@ -44,7 +45,7 @@ class WritableProviderSerializer(CustomFieldModelSerializer):
# Circuit types # Circuit types
# #
class CircuitTypeSerializer(serializers.ModelSerializer): class CircuitTypeSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = CircuitType model = CircuitType
@@ -110,7 +111,7 @@ class CircuitTerminationSerializer(serializers.ModelSerializer):
] ]
class WritableCircuitTerminationSerializer(serializers.ModelSerializer): class WritableCircuitTerminationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination

View File

@@ -43,6 +43,7 @@ class ProviderViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
class CircuitTypeViewSet(ModelViewSet): class CircuitTypeViewSet(ModelViewSet):
queryset = CircuitType.objects.all() queryset = CircuitType.objects.all()
serializer_class = serializers.CircuitTypeSerializer serializer_class = serializers.CircuitTypeSerializer
filter_class = filters.CircuitTypeFilter
# #

View File

@@ -0,0 +1,10 @@
from __future__ import unicode_literals
# CircuitTermination sides
TERM_SIDE_A = 'A'
TERM_SIDE_Z = 'Z'
TERM_SIDE_CHOICES = (
(TERM_SIDE_A, 'A'),
(TERM_SIDE_Z, 'Z'),
)

View File

@@ -31,7 +31,7 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Provider model = Provider
fields = ['name', 'account', 'asn'] fields = ['name', 'slug', 'asn', 'account']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -39,10 +39,19 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter( return queryset.filter(
Q(name__icontains=value) | Q(name__icontains=value) |
Q(account__icontains=value) | Q(account__icontains=value) |
Q(noc_contact__icontains=value) |
Q(admin_contact__icontains=value) |
Q(comments__icontains=value) Q(comments__icontains=value)
) )
class CircuitTypeFilter(django_filters.FilterSet):
class Meta:
model = CircuitType
fields = ['name', 'slug']
class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet): class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
@@ -50,7 +59,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
provider_id = django_filters.ModelMultipleChoiceFilter( provider_id = django_filters.ModelMultipleChoiceFilter(
name='provider',
queryset=Provider.objects.all(), queryset=Provider.objects.all(),
label='Provider (ID)', label='Provider (ID)',
) )
@@ -61,7 +69,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Provider (slug)', label='Provider (slug)',
) )
type_id = django_filters.ModelMultipleChoiceFilter( type_id = django_filters.ModelMultipleChoiceFilter(
name='type',
queryset=CircuitType.objects.all(), queryset=CircuitType.objects.all(),
label='Circuit type (ID)', label='Circuit type (ID)',
) )
@@ -72,7 +79,6 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Circuit type (slug)', label='Circuit type (slug)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
@@ -96,7 +102,7 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['install_date'] fields = ['cid', 'install_date', 'commit_rate']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -111,12 +117,34 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
class CircuitTerminationFilter(django_filters.FilterSet): class CircuitTerminationFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
)
circuit_id = django_filters.ModelMultipleChoiceFilter( circuit_id = django_filters.ModelMultipleChoiceFilter(
name='circuit',
queryset=Circuit.objects.all(), queryset=Circuit.objects.all(),
label='Circuit', label='Circuit',
) )
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = ['term_side', 'site'] fields = ['term_side', 'port_speed', 'upstream_speed', 'xconnect_id']
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(circuit__cid__icontains=value) |
Q(xconnect_id__icontains=value) |
Q(pp_info__icontains=value)
).distinct()

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django import forms from django import forms
from django.db.models import Count from django.db.models import Count
from dcim.models import Site, Device, Interface, Rack, VIRTUAL_IFACE_TYPES from dcim.models import Site, Device, Interface, Rack
from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
@@ -210,7 +210,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
) )
) )
interface = ChainedModelChoiceField( interface = ChainedModelChoiceField(
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related( queryset=Interface.objects.connectable().select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b' 'circuit_termination', 'connected_as_a', 'connected_as_b'
), ),
chains=( chains=(
@@ -244,7 +244,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
# Initialize helper selectors # Initialize helper selectors
instance = kwargs.get('instance') instance = kwargs.get('instance')
if instance and instance.interface is not None: if instance and instance.interface is not None:
initial = kwargs.get('initial', {}) initial = kwargs.get('initial', {}).copy()
initial['rack'] = instance.interface.device.rack initial['rack'] = instance.interface.device.rack
initial['device'] = instance.interface.device initial['device'] = instance.interface.device
kwargs['initial'] = initial kwargs['initial'] = initial
@@ -252,6 +252,11 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
super(CircuitTerminationForm, self).__init__(*args, **kwargs) super(CircuitTerminationForm, self).__init__(*args, **kwargs)
# Mark connected interfaces as disabled # Mark connected interfaces as disabled
self.fields['interface'].choices = [ self.fields['interface'].choices = []
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface'].queryset for iface in self.fields['interface'].queryset:
] self.fields['interface'].choices.append(
(iface.id, {
'label': iface.name,
'disabled': iface.is_connected and iface.pk != self.initial.get('interface'),
})
)

View File

@@ -10,14 +10,7 @@ from extras.models import CustomFieldModel, CustomFieldValue
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.utils import csv_format from utilities.utils import csv_format
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from .constants import *
TERM_SIDE_A = 'A'
TERM_SIDE_Z = 'Z'
TERM_SIDE_CHOICES = (
(TERM_SIDE_A, 'A'),
(TERM_SIDE_Z, 'Z'),
)
def humanize_speed(speed): def humanize_speed(speed):

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from utilities.tables import BaseTable, SearchTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import Circuit, CircuitType, Provider from .models import Circuit, CircuitType, Provider
@@ -21,19 +21,18 @@ CIRCUITTYPE_ACTIONS = """
class ProviderTable(BaseTable): class ProviderTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
name = tables.LinkColumn() name = tables.LinkColumn()
circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Provider model = Provider
fields = ('pk', 'name', 'asn', 'account', 'circuit_count') fields = ('pk', 'name', 'asn', 'account',)
class ProviderSearchTable(SearchTable): class ProviderDetailTable(ProviderTable):
name = tables.LinkColumn() circuit_count = tables.Column(accessor=Accessor('count_circuits'), verbose_name='Circuits')
class Meta(SearchTable.Meta): class Meta(ProviderTable.Meta):
model = Provider model = Provider
fields = ('name', 'asn', 'account') fields = ('pk', 'name', 'asn', 'account', 'circuit_count')
# #
@@ -74,19 +73,3 @@ class CircuitTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Circuit model = Circuit
fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description') fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
class CircuitSearchTable(SearchTable):
cid = tables.LinkColumn(verbose_name='ID')
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
a_side = tables.LinkColumn(
'dcim:site', accessor=Accessor('termination_a.site'), args=[Accessor('termination_a.site.slug')]
)
z_side = tables.LinkColumn(
'dcim:site', accessor=Accessor('termination_z.site'), args=[Accessor('termination_z.site.slug')]
)
class Meta(SearchTable.Meta):
model = Circuit
fields = ('cid', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')

View File

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

View File

@@ -26,7 +26,7 @@ class ProviderListView(ObjectListView):
queryset = Provider.objects.annotate(count_circuits=Count('circuits')) queryset = Provider.objects.annotate(count_circuits=Count('circuits'))
filter = filters.ProviderFilter filter = filters.ProviderFilter
filter_form = forms.ProviderFilterForm filter_form = forms.ProviderFilterForm
table = tables.ProviderTable table = tables.ProviderDetailTable
template_name = 'circuits/provider_list.html' template_name = 'circuits/provider_list.html'
@@ -49,14 +49,18 @@ class ProviderView(View):
}) })
class ProviderEditView(PermissionRequiredMixin, ObjectEditView): class ProviderCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_provider' permission_required = 'circuits.add_provider'
model = Provider model = Provider
form_class = forms.ProviderForm form_class = forms.ProviderForm
template_name = 'circuits/provider_edit.html' template_name = 'circuits/provider_edit.html'
default_return_url = 'circuits:provider_list' default_return_url = 'circuits:provider_list'
class ProviderEditView(ProviderCreateView):
permission_required = 'circuits.change_provider'
class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView): class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'circuits.delete_provider' permission_required = 'circuits.delete_provider'
model = Provider model = Provider
@@ -74,8 +78,8 @@ class ProviderBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_provider' permission_required = 'circuits.change_provider'
cls = Provider cls = Provider
filter = filters.ProviderFilter filter = filters.ProviderFilter
table = tables.ProviderTable
form = forms.ProviderBulkEditForm form = forms.ProviderBulkEditForm
template_name = 'circuits/provider_bulk_edit.html'
default_return_url = 'circuits:provider_list' default_return_url = 'circuits:provider_list'
@@ -83,6 +87,7 @@ class ProviderBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_provider' permission_required = 'circuits.delete_provider'
cls = Provider cls = Provider
filter = filters.ProviderFilter filter = filters.ProviderFilter
table = tables.ProviderTable
default_return_url = 'circuits:provider_list' default_return_url = 'circuits:provider_list'
@@ -96,8 +101,8 @@ class CircuitTypeListView(ObjectListView):
template_name = 'circuits/circuittype_list.html' template_name = 'circuits/circuittype_list.html'
class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView): class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuittype' permission_required = 'circuits.add_circuittype'
model = CircuitType model = CircuitType
form_class = forms.CircuitTypeForm form_class = forms.CircuitTypeForm
@@ -105,9 +110,15 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('circuits:circuittype_list') return reverse('circuits:circuittype_list')
class CircuitTypeEditView(CircuitTypeCreateView):
permission_required = 'circuits.change_circuittype'
class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuittype' permission_required = 'circuits.delete_circuittype'
cls = CircuitType cls = CircuitType
queryset = CircuitType.objects.annotate(circuit_count=Count('circuits'))
table = tables.CircuitTypeTable
default_return_url = 'circuits:circuittype_list' default_return_url = 'circuits:circuittype_list'
@@ -146,14 +157,18 @@ class CircuitView(View):
}) })
class CircuitEditView(PermissionRequiredMixin, ObjectEditView): class CircuitCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuit' permission_required = 'circuits.add_circuit'
model = Circuit model = Circuit
form_class = forms.CircuitForm form_class = forms.CircuitForm
template_name = 'circuits/circuit_edit.html' template_name = 'circuits/circuit_edit.html'
default_return_url = 'circuits:circuit_list' default_return_url = 'circuits:circuit_list'
class CircuitEditView(CircuitCreateView):
permission_required = 'circuits.change_circuit'
class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView): class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'circuits.delete_circuit' permission_required = 'circuits.delete_circuit'
model = Circuit model = Circuit
@@ -170,16 +185,19 @@ class CircuitBulkImportView(PermissionRequiredMixin, BulkImportView):
class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView): class CircuitBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'circuits.change_circuit' permission_required = 'circuits.change_circuit'
cls = Circuit cls = Circuit
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
filter = filters.CircuitFilter filter = filters.CircuitFilter
table = tables.CircuitTable
form = forms.CircuitBulkEditForm form = forms.CircuitBulkEditForm
template_name = 'circuits/circuit_bulk_edit.html'
default_return_url = 'circuits:circuit_list' default_return_url = 'circuits:circuit_list'
class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class CircuitBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'circuits.delete_circuit' permission_required = 'circuits.delete_circuit'
cls = Circuit cls = Circuit
queryset = Circuit.objects.select_related('provider', 'type', 'tenant').prefetch_related('terminations__site')
filter = filters.CircuitFilter filter = filters.CircuitFilter
table = tables.CircuitTable
default_return_url = 'circuits:circuit_list' default_return_url = 'circuits:circuit_list'
@@ -232,8 +250,8 @@ def circuit_terminations_swap(request, pk):
# Circuit terminations # Circuit terminations
# #
class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView): class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'circuits.change_circuittermination' permission_required = 'circuits.add_circuittermination'
model = CircuitTermination model = CircuitTermination
form_class = forms.CircuitTerminationForm form_class = forms.CircuitTerminationForm
template_name = 'circuits/circuittermination_edit.html' template_name = 'circuits/circuittermination_edit.html'
@@ -247,6 +265,10 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView):
return obj.circuit.get_absolute_url() return obj.circuit.get_absolute_url()
class CircuitTerminationEditView(CircuitTerminationCreateView):
permission_required = 'circuits.change_circuittermination'
class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView): class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'circuits.delete_circuittermination' permission_required = 'circuits.delete_circuittermination'
model = CircuitTermination model = CircuitTermination

View File

@@ -1,19 +1,21 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
from ipam.models import IPAddress from ipam.models import IPAddress
from circuits.models import Circuit, CircuitTermination
from dcim.models import ( from dcim.models import (
CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface, DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES, RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
) )
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
# #
@@ -36,7 +38,7 @@ class RegionSerializer(serializers.ModelSerializer):
fields = ['id', 'name', 'slug', 'parent'] fields = ['id', 'name', 'slug', 'parent']
class WritableRegionSerializer(serializers.ModelSerializer): class WritableRegionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Region model = Region
@@ -98,7 +100,7 @@ class NestedRackGroupSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name', 'slug'] fields = ['id', 'url', 'name', 'slug']
class WritableRackGroupSerializer(serializers.ModelSerializer): class WritableRackGroupSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = RackGroup model = RackGroup
@@ -109,7 +111,7 @@ class WritableRackGroupSerializer(serializers.ModelSerializer):
# Rack roles # Rack roles
# #
class RackRoleSerializer(serializers.ModelSerializer): class RackRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = RackRole model = RackRole
@@ -174,6 +176,9 @@ class WritableRackSerializer(CustomFieldModelSerializer):
validator.set_context(self) validator.set_context(self)
validator(data) validator(data)
# Enforce model validation
super(WritableRackSerializer, self).validate(data)
return data return data
@@ -211,7 +216,7 @@ class RackReservationSerializer(serializers.ModelSerializer):
fields = ['id', 'rack', 'units', 'created', 'user', 'description'] fields = ['id', 'rack', 'units', 'created', 'user', 'description']
class WritableRackReservationSerializer(serializers.ModelSerializer): class WritableRackReservationSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = RackReservation model = RackReservation
@@ -222,7 +227,7 @@ class WritableRackReservationSerializer(serializers.ModelSerializer):
# Manufacturers # Manufacturers
# #
class ManufacturerSerializer(serializers.ModelSerializer): class ManufacturerSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Manufacturer model = Manufacturer
@@ -287,7 +292,7 @@ class ConsolePortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritableConsolePortTemplateSerializer(serializers.ModelSerializer): class WritableConsolePortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
@@ -306,7 +311,7 @@ class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritableConsoleServerPortTemplateSerializer(serializers.ModelSerializer): class WritableConsoleServerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
@@ -325,7 +330,7 @@ class PowerPortTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritablePowerPortTemplateSerializer(serializers.ModelSerializer): class WritablePowerPortTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
@@ -344,7 +349,7 @@ class PowerOutletTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritablePowerOutletTemplateSerializer(serializers.ModelSerializer): class WritablePowerOutletTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
@@ -364,7 +369,7 @@ class InterfaceTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only'] fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
class WritableInterfaceTemplateSerializer(serializers.ModelSerializer): class WritableInterfaceTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
@@ -383,7 +388,7 @@ class DeviceBayTemplateSerializer(serializers.ModelSerializer):
fields = ['id', 'device_type', 'name'] fields = ['id', 'device_type', 'name']
class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer): class WritableDeviceBayTemplateSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
@@ -394,7 +399,7 @@ class WritableDeviceBayTemplateSerializer(serializers.ModelSerializer):
# Device roles # Device roles
# #
class DeviceRoleSerializer(serializers.ModelSerializer): class DeviceRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
@@ -413,11 +418,11 @@ class NestedDeviceRoleSerializer(serializers.ModelSerializer):
# Platforms # Platforms
# #
class PlatformSerializer(serializers.ModelSerializer): class PlatformSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Platform model = Platform
fields = ['id', 'name', 'slug', 'rpc_client'] fields = ['id', 'name', 'slug', 'napalm_driver', 'rpc_client']
class NestedPlatformSerializer(serializers.ModelSerializer): class NestedPlatformSerializer(serializers.ModelSerializer):
@@ -468,14 +473,10 @@ class DeviceSerializer(CustomFieldModelSerializer):
device_bay = obj.parent_bay device_bay = obj.parent_bay
except DeviceBay.DoesNotExist: except DeviceBay.DoesNotExist:
return None return None
return { context = {'request': self.context['request']}
'id': device_bay.device.pk, data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
'name': device_bay.device.name, data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
'device_bay': { return data
'id': device_bay.pk,
'name': device_bay.name,
}
}
class WritableDeviceSerializer(CustomFieldModelSerializer): class WritableDeviceSerializer(CustomFieldModelSerializer):
@@ -496,6 +497,9 @@ class WritableDeviceSerializer(CustomFieldModelSerializer):
validator.set_context(self) validator.set_context(self)
validator(data) validator(data)
# Enforce model validation
super(WritableDeviceSerializer, self).validate(data)
return data return data
@@ -512,7 +516,7 @@ class ConsoleServerPortSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_console'] read_only_fields = ['connected_console']
class WritableConsoleServerPortSerializer(serializers.ModelSerializer): class WritableConsoleServerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
@@ -532,7 +536,7 @@ class ConsolePortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'cs_port', 'connection_status'] fields = ['id', 'device', 'name', 'cs_port', 'connection_status']
class WritableConsolePortSerializer(serializers.ModelSerializer): class WritableConsolePortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = ConsolePort model = ConsolePort
@@ -552,7 +556,7 @@ class PowerOutletSerializer(serializers.ModelSerializer):
read_only_fields = ['connected_port'] read_only_fields = ['connected_port']
class WritablePowerOutletSerializer(serializers.ModelSerializer): class WritablePowerOutletSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
@@ -572,7 +576,7 @@ class PowerPortSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'power_outlet', 'connection_status'] fields = ['id', 'device', 'name', 'power_outlet', 'connection_status']
class WritablePowerPortSerializer(serializers.ModelSerializer): class WritablePowerPortSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = PowerPort model = PowerPort
@@ -591,28 +595,58 @@ class NestedInterfaceSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'name'] fields = ['id', 'url', 'name']
class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
class Meta:
model = Circuit
fields = ['id', 'url', 'cid']
class InterfaceCircuitTerminationSerializer(serializers.ModelSerializer):
circuit = InterfaceNestedCircuitSerializer()
class Meta:
model = CircuitTermination
fields = [
'id', 'circuit', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
]
class InterfaceSerializer(serializers.ModelSerializer): class InterfaceSerializer(serializers.ModelSerializer):
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES) form_factor = ChoiceFieldSerializer(choices=IFACE_FF_CHOICES)
lag = NestedInterfaceSerializer() lag = NestedInterfaceSerializer()
connection = serializers.SerializerMethodField(read_only=True) is_connected = serializers.SerializerMethodField(read_only=True)
connected_interface = serializers.SerializerMethodField(read_only=True) interface_connection = serializers.SerializerMethodField(read_only=True)
circuit_termination = InterfaceCircuitTerminationSerializer()
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = [
'id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description', 'connection', 'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
'connected_interface', 'is_connected', 'interface_connection', 'circuit_termination',
] ]
def get_connection(self, obj): def get_is_connected(self, obj):
"""
Return True if the interface has a connected interface or circuit termination.
"""
if obj.connection: if obj.connection:
return NestedInterfaceConnectionSerializer(obj.connection, context=self.context).data return True
return None try:
circuit_termination = obj.circuit_termination
return True
except CircuitTermination.DoesNotExist:
pass
return False
def get_connected_interface(self, obj): def get_interface_connection(self, obj):
if obj.connected_interface: if obj.connection:
return PeerInterfaceSerializer(obj.connected_interface, context=self.context).data return OrderedDict((
('interface', PeerInterfaceSerializer(obj.connected_interface, context=self.context).data),
('status', obj.connection.connection_status),
))
return None return None
@@ -624,14 +658,19 @@ class PeerInterfaceSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Interface model = Interface
fields = ['id', 'url', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description'] fields = [
'id', 'url', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
'description',
]
class WritableInterfaceSerializer(serializers.ModelSerializer): class WritableInterfaceSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Interface model = Interface
fields = ['id', 'device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description'] fields = [
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description',
]
# #
@@ -647,7 +686,15 @@ class DeviceBaySerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'installed_device'] fields = ['id', 'device', 'name', 'installed_device']
class WritableDeviceBaySerializer(serializers.ModelSerializer): class NestedDeviceBaySerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
class Meta:
model = DeviceBay
fields = ['id', 'url', 'name']
class WritableDeviceBaySerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = DeviceBay model = DeviceBay
@@ -664,14 +711,20 @@ class InventoryItemSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] fields = [
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description',
]
class WritableInventoryItemSerializer(serializers.ModelSerializer): class WritableInventoryItemSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] fields = [
'id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered',
'description',
]
# #
@@ -696,7 +749,7 @@ class NestedInterfaceConnectionSerializer(serializers.ModelSerializer):
fields = ['id', 'url', 'connection_status'] fields = ['id', 'url', 'connection_status']
class WritableInterfaceConnectionSerializer(serializers.ModelSerializer): class WritableInterfaceConnectionSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = InterfaceConnection model = InterfaceConnection

View File

@@ -1,12 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework.decorators import detail_route from rest_framework.decorators import detail_route
from rest_framework.mixins import ListModelMixin from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
from django.conf import settings from django.conf import settings
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from dcim.models import ( from dcim.models import (
@@ -19,7 +20,7 @@ from dcim import filters
from extras.api.serializers import RenderedGraphSerializer from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.api import ServiceUnavailable, WritableSerializerMixin from utilities.api import IsAuthenticatedOrLoginNotRequired, ServiceUnavailable, WritableSerializerMixin
from .exceptions import MissingFilterException from .exceptions import MissingFilterException
from . import serializers from . import serializers
@@ -32,6 +33,7 @@ class RegionViewSet(WritableSerializerMixin, ModelViewSet):
queryset = Region.objects.all() queryset = Region.objects.all()
serializer_class = serializers.RegionSerializer serializer_class = serializers.RegionSerializer
write_serializer_class = serializers.WritableRegionSerializer write_serializer_class = serializers.WritableRegionSerializer
filter_class = filters.RegionFilter
# #
@@ -73,6 +75,7 @@ class RackGroupViewSet(WritableSerializerMixin, ModelViewSet):
class RackRoleViewSet(ModelViewSet): class RackRoleViewSet(ModelViewSet):
queryset = RackRole.objects.all() queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer serializer_class = serializers.RackRoleSerializer
filter_class = filters.RackRoleFilter
# #
@@ -128,6 +131,7 @@ class RackReservationViewSet(WritableSerializerMixin, ModelViewSet):
class ManufacturerViewSet(ModelViewSet): class ManufacturerViewSet(ModelViewSet):
queryset = Manufacturer.objects.all() queryset = Manufacturer.objects.all()
serializer_class = serializers.ManufacturerSerializer serializer_class = serializers.ManufacturerSerializer
filter_class = filters.ManufacturerFilter
# #
@@ -194,6 +198,7 @@ class DeviceBayTemplateViewSet(WritableSerializerMixin, ModelViewSet):
class DeviceRoleViewSet(ModelViewSet): class DeviceRoleViewSet(ModelViewSet):
queryset = DeviceRole.objects.all() queryset = DeviceRole.objects.all()
serializer_class = serializers.DeviceRoleSerializer serializer_class = serializers.DeviceRoleSerializer
filter_class = filters.DeviceRoleFilter
# #
@@ -203,6 +208,7 @@ class DeviceRoleViewSet(ModelViewSet):
class PlatformViewSet(ModelViewSet): class PlatformViewSet(ModelViewSet):
queryset = Platform.objects.all() queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer serializer_class = serializers.PlatformSerializer
filter_class = filters.PlatformFilter
# #
@@ -219,27 +225,66 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
write_serializer_class = serializers.WritableDeviceSerializer write_serializer_class = serializers.WritableDeviceSerializer
filter_class = filters.DeviceFilter filter_class = filters.DeviceFilter
@detail_route(url_path='lldp-neighbors') @detail_route(url_path='napalm')
def lldp_neighbors(self, request, pk): def napalm(self, request, pk):
""" """
Retrieve live LLDP neighbors of a device Execute a NAPALM method on a Device
""" """
device = get_object_or_404(Device, pk=pk) device = get_object_or_404(Device, pk=pk)
if not device.primary_ip: if not device.primary_ip:
raise ServiceUnavailable("No IP configured for this device.") raise ServiceUnavailable("This device does not have a primary IP address configured.")
if device.platform is None:
raise ServiceUnavailable("No platform is configured for this device.")
if not device.platform.napalm_driver:
raise ServiceUnavailable("No NAPALM driver is configured for this device's platform ().".format(
device.platform
))
RPC = device.get_rpc_client() # Check that NAPALM is installed and verify the configured driver
if not RPC:
raise ServiceUnavailable("No RPC client available for this platform ({}).".format(device.platform))
# Connect to device and retrieve inventory info
try: try:
with RPC(device, username=settings.NETBOX_USERNAME, password=settings.NETBOX_PASSWORD) as rpc_client: import napalm
lldp_neighbors = rpc_client.get_lldp_neighbors() from napalm_base.exceptions import ConnectAuthError, ModuleImportError
except: except ImportError:
raise ServiceUnavailable("Error connecting to the remote device.") raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
try:
driver = napalm.get_network_driver(device.platform.napalm_driver)
except ModuleImportError:
raise ServiceUnavailable("NAPALM driver for platform {} not found: {}.".format(
device.platform, device.platform.napalm_driver
))
return Response(lldp_neighbors) # Verify user permission
if not request.user.has_perm('dcim.napalm_read'):
return HttpResponseForbidden()
# Validate requested NAPALM methods
napalm_methods = request.GET.getlist('method')
for method in napalm_methods:
if not hasattr(driver, method):
return HttpResponseBadRequest("Unknown NAPALM method: {}".format(method))
elif not method.startswith('get_'):
return HttpResponseBadRequest("Unsupported NAPALM method: {}".format(method))
# Connect to the device and execute the requested methods
# TODO: Improve error handling
response = OrderedDict([(m, None) for m in napalm_methods])
ip_address = str(device.primary_ip.address.ip)
d = driver(
hostname=ip_address,
username=settings.NAPALM_USERNAME,
password=settings.NAPALM_PASSWORD,
timeout=settings.NAPALM_TIMEOUT,
optional_args=settings.NAPALM_ARGS
)
try:
d.open()
for method in napalm_methods:
response[method] = getattr(d, method)()
except Exception as e:
raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e))
d.close()
return Response(response)
# #
@@ -341,7 +386,7 @@ class ConnectedDeviceViewSet(ViewSet):
* `peer-device`: The name of the peer device * `peer-device`: The name of the peer device
* `peer-interface`: The name of the peer interface * `peer-interface`: The name of the peer interface
""" """
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticatedOrLoginNotRequired]
def get_view_name(self): def get_view_name(self):
return "Connected Device Locator" return "Connected Device Locator"

230
netbox/dcim/constants.py Normal file
View File

@@ -0,0 +1,230 @@
from __future__ import unicode_literals
# Rack types
RACK_TYPE_2POST = 100
RACK_TYPE_4POST = 200
RACK_TYPE_CABINET = 300
RACK_TYPE_WALLFRAME = 1000
RACK_TYPE_WALLCABINET = 1100
RACK_TYPE_CHOICES = (
(RACK_TYPE_2POST, '2-post frame'),
(RACK_TYPE_4POST, '4-post frame'),
(RACK_TYPE_CABINET, '4-post cabinet'),
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
)
# Rack widths
RACK_WIDTH_19IN = 19
RACK_WIDTH_23IN = 23
RACK_WIDTH_CHOICES = (
(RACK_WIDTH_19IN, '19 inches'),
(RACK_WIDTH_23IN, '23 inches'),
)
# Rack faces
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
RACK_FACE_CHOICES = [
[RACK_FACE_FRONT, 'Front'],
[RACK_FACE_REAR, 'Rear'],
]
# Parent/child device roles
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
SUBDEVICE_ROLE_CHOICES = (
(None, 'None'),
(SUBDEVICE_ROLE_PARENT, 'Parent'),
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
# Interface ordering schemes (for device types)
IFACE_ORDERING_POSITION = 1
IFACE_ORDERING_NAME = 2
IFACE_ORDERING_CHOICES = [
[IFACE_ORDERING_POSITION, 'Slot/position'],
[IFACE_ORDERING_NAME, 'Name (alphabetically)']
]
# Interface form factors
# Virtual
IFACE_FF_VIRTUAL = 0
IFACE_FF_LAG = 200
# Ethernet
IFACE_FF_100ME_FIXED = 800
IFACE_FF_1GE_FIXED = 1000
IFACE_FF_1GE_GBIC = 1050
IFACE_FF_1GE_SFP = 1100
IFACE_FF_10GE_FIXED = 1150
IFACE_FF_10GE_SFP_PLUS = 1200
IFACE_FF_10GE_XFP = 1300
IFACE_FF_10GE_XENPAK = 1310
IFACE_FF_10GE_X2 = 1320
IFACE_FF_25GE_SFP28 = 1350
IFACE_FF_40GE_QSFP_PLUS = 1400
IFACE_FF_100GE_CFP = 1500
IFACE_FF_100GE_QSFP28 = 1600
# Wireless
IFACE_FF_80211A = 2600
IFACE_FF_80211G = 2610
IFACE_FF_80211N = 2620
IFACE_FF_80211AC = 2630
IFACE_FF_80211AD = 2640
# Fibrechannel
IFACE_FF_1GFC_SFP = 3010
IFACE_FF_2GFC_SFP = 3020
IFACE_FF_4GFC_SFP = 3040
IFACE_FF_8GFC_SFP_PLUS = 3080
IFACE_FF_16GFC_SFP_PLUS = 3160
# Serial
IFACE_FF_T1 = 4000
IFACE_FF_E1 = 4010
IFACE_FF_T3 = 4040
IFACE_FF_E3 = 4050
# Stacking
IFACE_FF_STACKWISE = 5000
IFACE_FF_STACKWISE_PLUS = 5050
IFACE_FF_FLEXSTACK = 5100
IFACE_FF_FLEXSTACK_PLUS = 5150
IFACE_FF_JUNIPER_VCP = 5200
# Other
IFACE_FF_OTHER = 32767
IFACE_FF_CHOICES = [
[
'Virtual interfaces',
[
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
]
],
[
'Ethernet (fixed)',
[
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
]
],
[
'Ethernet (modular)',
[
[IFACE_FF_1GE_GBIC, 'GBIC (1GE)'],
[IFACE_FF_1GE_SFP, 'SFP (1GE)'],
[IFACE_FF_10GE_SFP_PLUS, 'SFP+ (10GE)'],
[IFACE_FF_10GE_XFP, 'XFP (10GE)'],
[IFACE_FF_10GE_XENPAK, 'XENPAK (10GE)'],
[IFACE_FF_10GE_X2, 'X2 (10GE)'],
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
]
],
[
'Wireless',
[
[IFACE_FF_80211A, 'IEEE 802.11a'],
[IFACE_FF_80211G, 'IEEE 802.11b/g'],
[IFACE_FF_80211N, 'IEEE 802.11n'],
[IFACE_FF_80211AC, 'IEEE 802.11ac'],
[IFACE_FF_80211AD, 'IEEE 802.11ad'],
]
],
[
'FibreChannel',
[
[IFACE_FF_1GFC_SFP, 'SFP (1GFC)'],
[IFACE_FF_2GFC_SFP, 'SFP (2GFC)'],
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
]
],
[
'Serial',
[
[IFACE_FF_T1, 'T1 (1.544 Mbps)'],
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
[IFACE_FF_T3, 'T3 (45 Mbps)'],
[IFACE_FF_E3, 'E3 (34 Mbps)'],
]
],
[
'Stacking',
[
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
]
],
[
'Other',
[
[IFACE_FF_OTHER, 'Other'],
]
],
]
VIRTUAL_IFACE_TYPES = [
IFACE_FF_VIRTUAL,
IFACE_FF_LAG,
]
WIRELESS_IFACE_TYPES = [
IFACE_FF_80211A,
IFACE_FF_80211G,
IFACE_FF_80211N,
IFACE_FF_80211AC,
IFACE_FF_80211AD,
]
NONCONNECTABLE_IFACE_TYPES = VIRTUAL_IFACE_TYPES + WIRELESS_IFACE_TYPES
# Device statuses
STATUS_OFFLINE = 0
STATUS_ACTIVE = 1
STATUS_PLANNED = 2
STATUS_STAGED = 3
STATUS_FAILED = 4
STATUS_INVENTORY = 5
STATUS_CHOICES = [
[STATUS_ACTIVE, 'Active'],
[STATUS_OFFLINE, 'Offline'],
[STATUS_PLANNED, 'Planned'],
[STATUS_STAGED, 'Staged'],
[STATUS_FAILED, 'Failed'],
[STATUS_INVENTORY, 'Inventory'],
]
# Bootstrap CSS classes for device stasuses
DEVICE_STATUS_CLASSES = {
0: 'warning',
1: 'success',
2: 'info',
3: 'primary',
4: 'danger',
5: 'default',
}
# Console/power/interface connection statuses
CONNECTION_STATUS_PLANNED = False
CONNECTION_STATUS_CONNECTED = True
CONNECTION_STATUS_CHOICES = [
[CONNECTION_STATUS_PLANNED, 'Planned'],
[CONNECTION_STATUS_CONNECTED, 'Connected'],
]
# Platform -> RPC client mappings
RPC_CLIENT_JUNIPER_JUNOS = 'juniper-junos'
RPC_CLIENT_CISCO_IOS = 'cisco-ios'
RPC_CLIENT_OPENGEAR = 'opengear'
RPC_CLIENT_CHOICES = [
[RPC_CLIENT_JUNIPER_JUNOS, 'Juniper Junos (NETCONF)'],
[RPC_CLIENT_CISCO_IOS, 'Cisco IOS (SSH)'],
[RPC_CLIENT_OPENGEAR, 'Opengear (SSH)'],
]

View File

@@ -1,21 +1,40 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import django_filters import django_filters
from netaddr import EUI
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from extras.filters import CustomFieldFilterSet from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection, DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site, VIRTUAL_IFACE_TYPES, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site,
VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
) )
class RegionFilter(django_filters.FilterSet):
parent_id = NullableModelMultipleChoiceFilter(
queryset=Region.objects.all(),
label='Parent region (ID)',
)
parent = NullableModelMultipleChoiceFilter(
queryset=Region.objects.all(),
to_field_name='slug',
label='Parent region (slug)',
)
class Meta:
model = Region
fields = ['name', 'slug']
class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet): class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
@@ -23,23 +42,19 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
region_id = NullableModelMultipleChoiceFilter( region_id = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(), queryset=Region.objects.all(),
label='Region (ID)', label='Region (ID)',
) )
region = NullableModelMultipleChoiceFilter( region = NullableModelMultipleChoiceFilter(
name='region',
queryset=Region.objects.all(), queryset=Region.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Region (slug)', label='Region (slug)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
tenant = NullableModelMultipleChoiceFilter( tenant = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Tenant (slug)', label='Tenant (slug)',
@@ -47,7 +62,7 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Site model = Site
fields = ['q', 'name', 'facility', 'asn'] fields = ['q', 'name', 'slug', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -57,6 +72,9 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
Q(facility__icontains=value) | Q(facility__icontains=value) |
Q(physical_address__icontains=value) | Q(physical_address__icontains=value) |
Q(shipping_address__icontains=value) | Q(shipping_address__icontains=value) |
Q(contact_name__icontains=value) |
Q(contact_phone__icontains=value) |
Q(contact_email__icontains=value) |
Q(comments__icontains=value) Q(comments__icontains=value)
) )
try: try:
@@ -68,7 +86,6 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
class RackGroupFilter(django_filters.FilterSet): class RackGroupFilter(django_filters.FilterSet):
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
@@ -81,7 +98,14 @@ class RackGroupFilter(django_filters.FilterSet):
class Meta: class Meta:
model = RackGroup model = RackGroup
fields = ['name'] fields = ['site_id', 'name', 'slug']
class RackRoleFilter(django_filters.FilterSet):
class Meta:
model = RackRole
fields = ['name', 'slug', 'color']
class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -90,8 +114,8 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
facility_id = NullableCharFieldFilter()
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
@@ -102,7 +126,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)', label='Site (slug)',
) )
group_id = NullableModelMultipleChoiceFilter( group_id = NullableModelMultipleChoiceFilter(
name='group',
queryset=RackGroup.objects.all(), queryset=RackGroup.objects.all(),
label='Group (ID)', label='Group (ID)',
) )
@@ -113,7 +136,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Group', label='Group',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
@@ -124,7 +146,6 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)', label='Tenant (slug)',
) )
role_id = NullableModelMultipleChoiceFilter( role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=RackRole.objects.all(), queryset=RackRole.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
@@ -137,7 +158,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Rack model = Rack
fields = ['u_height'] fields = ['type', 'width', 'u_height', 'desc_units']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -155,6 +176,10 @@ class RackReservationFilter(django_filters.FilterSet):
method='search', method='search',
label='Search', label='Search',
) )
rack_id = django_filters.ModelMultipleChoiceFilter(
queryset=Rack.objects.all(),
label='Rack (ID)',
)
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
name='rack__site', name='rack__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
@@ -177,15 +202,20 @@ class RackReservationFilter(django_filters.FilterSet):
to_field_name='slug', to_field_name='slug',
label='Group', label='Group',
) )
rack_id = django_filters.ModelMultipleChoiceFilter( user_id = django_filters.ModelMultipleChoiceFilter(
name='rack', queryset=User.objects.all(),
queryset=Rack.objects.all(), label='User (ID)',
label='Rack (ID)', )
user = django_filters.ModelMultipleChoiceFilter(
name='user',
queryset=User.objects.all(),
to_field_name='username',
label='User (name)',
) )
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['rack', 'user'] fields = ['created']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -198,6 +228,13 @@ class RackReservationFilter(django_filters.FilterSet):
) )
class ManufacturerFilter(django_filters.FilterSet):
class Meta:
model = Manufacturer
fields = ['name', 'slug']
class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet): class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
@@ -205,7 +242,6 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
manufacturer_id = django_filters.ModelMultipleChoiceFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='manufacturer',
queryset=Manufacturer.objects.all(), queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)', label='Manufacturer (ID)',
) )
@@ -219,7 +255,8 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = DeviceType model = DeviceType
fields = [ fields = [
'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device', 'subdevice_role',
] ]
def search(self, queryset, name, value): def search(self, queryset, name, value):
@@ -235,16 +272,9 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceTypeComponentFilterSet(django_filters.FilterSet): class DeviceTypeComponentFilterSet(django_filters.FilterSet):
devicetype_id = django_filters.ModelMultipleChoiceFilter( devicetype_id = django_filters.ModelMultipleChoiceFilter(
name='device_type',
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
label='Device type (ID)', label='Device type (ID)',
) )
devicetype = django_filters.ModelMultipleChoiceFilter(
name='device_type',
queryset=DeviceType.objects.all(),
to_field_name='name',
label='Device type (name)',
)
class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet): class ConsolePortTemplateFilter(DeviceTypeComponentFilterSet):
@@ -279,7 +309,7 @@ class InterfaceTemplateFilter(DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = ['name', 'form_factor'] fields = ['name', 'form_factor', 'mgmt_only']
class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet): class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
@@ -289,18 +319,75 @@ class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
fields = ['name'] fields = ['name']
class DeviceRoleFilter(django_filters.FilterSet):
class Meta:
model = DeviceRole
fields = ['name', 'slug', 'color']
class PlatformFilter(django_filters.FilterSet):
class Meta:
model = Platform
fields = ['name', 'slug']
class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet): class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
) )
mac_address = django_filters.CharFilter( manufacturer_id = django_filters.ModelMultipleChoiceFilter(
method='_mac_address', name='device_type__manufacturer',
label='MAC address', queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
) )
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='device_type__manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
device_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(),
label='Device type (ID)',
)
role_id = django_filters.ModelMultipleChoiceFilter(
name='device_role_id',
queryset=DeviceRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='device_role__slug',
queryset=DeviceRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
platform_id = NullableModelMultipleChoiceFilter(
queryset=Platform.objects.all(),
label='Platform (ID)',
)
platform = NullableModelMultipleChoiceFilter(
name='platform',
queryset=Platform.objects.all(),
to_field_name='slug',
label='Platform (slug)',
)
name = NullableCharFieldFilter()
asset_tag = NullableCharFieldFilter()
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
@@ -320,60 +407,18 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
label='Rack (ID)', label='Rack (ID)',
) )
role_id = django_filters.ModelMultipleChoiceFilter(
name='device_role',
queryset=DeviceRole.objects.all(),
label='Role (ID)',
)
role = django_filters.ModelMultipleChoiceFilter(
name='device_role__slug',
queryset=DeviceRole.objects.all(),
to_field_name='slug',
label='Role (slug)',
)
tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
label='Tenant (ID)',
)
tenant = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(),
to_field_name='slug',
label='Tenant (slug)',
)
device_type_id = django_filters.ModelMultipleChoiceFilter(
name='device_type',
queryset=DeviceType.objects.all(),
label='Device type (ID)',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
name='device_type__manufacturer',
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='device_type__manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
model = django_filters.ModelMultipleChoiceFilter( model = django_filters.ModelMultipleChoiceFilter(
name='device_type__slug', name='device_type__slug',
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
to_field_name='slug', to_field_name='slug',
label='Device model (slug)', label='Device model (slug)',
) )
platform_id = NullableModelMultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
name='platform', choices=STATUS_CHOICES
queryset=Platform.objects.all(),
label='Platform (ID)',
) )
platform = NullableModelMultipleChoiceFilter( is_full_depth = django_filters.BooleanFilter(
name='platform', name='device_type__is_full_depth',
queryset=Platform.objects.all(), label='Is full depth',
to_field_name='slug',
label='Platform (slug)',
) )
is_console_server = django_filters.BooleanFilter( is_console_server = django_filters.BooleanFilter(
name='device_type__is_console_server', name='device_type__is_console_server',
@@ -387,17 +432,18 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
name='device_type__is_network_device', name='device_type__is_network_device',
label='Is a network device', label='Is a network device',
) )
mac_address = django_filters.CharFilter(
method='_mac_address',
label='MAC address',
)
has_primary_ip = django_filters.BooleanFilter( has_primary_ip = django_filters.BooleanFilter(
method='_has_primary_ip', method='_has_primary_ip',
label='Has a primary IP', label='Has a primary IP',
) )
status = django_filters.MultipleChoiceFilter(
choices=STATUS_CHOICES
)
class Meta: class Meta:
model = Device model = Device
fields = ['name', 'serial', 'asset_tag'] fields = ['serial']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -415,7 +461,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
if not value: if not value:
return queryset return queryset
try: try:
return queryset.filter(interfaces__mac_address=value).distinct() mac = EUI(value.strip())
return queryset.filter(interfaces__mac_address=mac).distinct()
except AddrFormatError: except AddrFormatError:
return queryset.none() return queryset.none()
@@ -433,13 +480,11 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
class DeviceComponentFilterSet(django_filters.FilterSet): class DeviceComponentFilterSet(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelChoiceFilter(
name='device',
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device (ID)', label='Device (ID)',
) )
device = django_filters.ModelMultipleChoiceFilter( device = django_filters.ModelChoiceFilter(
name='device__name',
queryset=Device.objects.all(), queryset=Device.objects.all(),
to_field_name='name', to_field_name='name',
label='Device (name)', label='Device (name)',
@@ -474,7 +519,21 @@ class PowerOutletFilter(DeviceComponentFilterSet):
fields = ['name'] fields = ['name']
class InterfaceFilter(DeviceComponentFilterSet): class InterfaceFilter(django_filters.FilterSet):
"""
Not using DeviceComponentFilterSet for Interfaces because we need to glean the ordering logic from the parent
Device's DeviceType.
"""
device = django_filters.CharFilter(
method='filter_device',
name='name',
label='Device',
)
device_id = django_filters.NumberFilter(
method='filter_device',
name='pk',
label='Device (ID)',
)
type = django_filters.CharFilter( type = django_filters.CharFilter(
method='filter_type', method='filter_type',
label='Interface type', label='Interface type',
@@ -491,24 +550,32 @@ class InterfaceFilter(DeviceComponentFilterSet):
class Meta: class Meta:
model = Interface model = Interface
fields = ['name', 'form_factor'] fields = ['name', 'form_factor', 'enabled', 'mtu', 'mgmt_only']
def filter_device(self, queryset, name, value):
try:
device = Device.objects.select_related('device_type').get(**{name: value})
ordering = device.device_type.interface_ordering
return queryset.filter(device=device).order_naturally(ordering)
except Device.DoesNotExist:
return queryset.none()
def filter_type(self, queryset, name, value): def filter_type(self, queryset, name, value):
value = value.strip().lower() value = value.strip().lower()
if value == 'physical': return {
return queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES) 'physical': queryset.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES),
elif value == 'virtual': 'virtual': queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES),
return queryset.filter(form_factor__in=VIRTUAL_IFACE_TYPES) 'wireless': queryset.filter(form_factor__in=WIRELESS_IFACE_TYPES),
elif value == 'lag': 'lag': queryset.filter(form_factor=IFACE_FF_LAG),
return queryset.filter(form_factor=IFACE_FF_LAG) }.get(value, queryset.none())
return queryset
def _mac_address(self, queryset, name, value): def _mac_address(self, queryset, name, value):
value = value.strip() value = value.strip()
if not value: if not value:
return queryset return queryset
try: try:
return queryset.filter(mac_address=value) mac = EUI(value.strip())
return queryset.filter(mac_address=mac)
except AddrFormatError: except AddrFormatError:
return queryset.none() return queryset.none()
@@ -521,10 +588,25 @@ class DeviceBayFilter(DeviceComponentFilterSet):
class InventoryItemFilter(DeviceComponentFilterSet): class InventoryItemFilter(DeviceComponentFilterSet):
parent_id = NullableModelMultipleChoiceFilter(
queryset=InventoryItem.objects.all(),
label='Parent inventory item (ID)',
)
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
queryset=Manufacturer.objects.all(),
label='Manufacturer (ID)',
)
manufacturer = django_filters.ModelMultipleChoiceFilter(
name='manufacturer__slug',
queryset=Manufacturer.objects.all(),
to_field_name='slug',
label='Manufacturer (slug)',
)
asset_tag = NullableCharFieldFilter()
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['name'] fields = ['name', 'part_id', 'serial', 'discovered']
class ConsoleConnectionFilter(django_filters.FilterSet): class ConsoleConnectionFilter(django_filters.FilterSet):

View File

@@ -13,8 +13,8 @@ from tenancy.forms import TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import ( from utilities.forms import (
APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVChoiceField, ExpandableNameField, FilterChoiceField, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField,
FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField,
FilterTreeNodeMultipleChoiceField, FilterTreeNodeMultipleChoiceField,
) )
from .formfields import MACAddressFormField from .formfields import MACAddressFormField
@@ -24,7 +24,7 @@ from .models import (
IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer,
InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES,
RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, VIRTUAL_IFACE_TYPES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
) )
@@ -402,7 +402,9 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
u_height = forms.IntegerField(min_value=1, required=False) u_height = forms.IntegerField(min_value=1, required=False)
is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth') is_full_depth = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth')
interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False) interface_ordering = forms.ChoiceField(choices=add_blank_choice(IFACE_ORDERING_CHOICES), required=False)
is_console_server = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is full depth') is_console_server = forms.NullBooleanField(
required=False, widget=BulkEditNullBooleanSelect, label='Is a console server'
)
is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU') is_pdu = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a PDU')
is_network_device = forms.NullBooleanField( is_network_device = forms.NullBooleanField(
required=False, widget=BulkEditNullBooleanSelect, label='Is a network device' required=False, widget=BulkEditNullBooleanSelect, label='Is a network device'
@@ -556,7 +558,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = Platform model = Platform
fields = ['name', 'slug', 'rpc_client'] fields = ['name', 'slug', 'napalm_driver', 'rpc_client']
# #
@@ -630,7 +632,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
instance = kwargs.get('instance') instance = kwargs.get('instance')
# Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field # Using hasattr() instead of "is not None" to avoid RelatedObjectDoesNotExist on required field
if instance and hasattr(instance, 'device_type'): if instance and hasattr(instance, 'device_type'):
initial = kwargs.get('initial', {}) initial = kwargs.get('initial', {}).copy()
initial['manufacturer'] = instance.device_type.manufacturer initial['manufacturer'] = instance.device_type.manufacturer
kwargs['initial'] = initial kwargs['initial'] = initial
@@ -1174,6 +1176,10 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.
} }
class ConsoleServerPortBulkDisconnectForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput)
# #
# Power ports # Power ports
# #
@@ -1431,6 +1437,10 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
} }
class PowerOutletBulkDisconnectForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput)
# #
# Interfaces # Interfaces
# #
@@ -1439,7 +1449,7 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = Interface model = Interface
fields = ['device', 'name', 'form_factor', 'lag', 'mac_address', 'mgmt_only', 'description'] fields = ['device', 'name', 'form_factor', 'enabled', 'lag', 'mac_address', 'mtu', 'mgmt_only', 'description']
widgets = { widgets = {
'device': forms.HiddenInput(), 'device': forms.HiddenInput(),
} }
@@ -1461,12 +1471,19 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
class InterfaceCreateForm(DeviceComponentForm): class InterfaceCreateForm(DeviceComponentForm):
name_pattern = ExpandableNameField(label='Name') name_pattern = ExpandableNameField(label='Name')
form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES) form_factor = forms.ChoiceField(choices=IFACE_FF_CHOICES)
enabled = forms.BooleanField(required=False)
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG') lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
mac_address = MACAddressFormField(required=False, label='MAC Address') mac_address = MACAddressFormField(required=False, label='MAC Address')
mgmt_only = forms.BooleanField(required=False, label='OOB Management') mgmt_only = forms.BooleanField(required=False, label='OOB Management')
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Set interfaces enabled by default
kwargs['initial'] = kwargs.get('initial', {}).copy()
kwargs['initial'].update({'enabled': True})
super(InterfaceCreateForm, self).__init__(*args, **kwargs) super(InterfaceCreateForm, self).__init__(*args, **kwargs)
# Limit LAG choices to interfaces belonging to this device # Limit LAG choices to interfaces belonging to this device
@@ -1481,13 +1498,15 @@ class InterfaceCreateForm(DeviceComponentForm):
class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput) pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput) device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput)
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False) form_factor = forms.ChoiceField(choices=add_blank_choice(IFACE_FF_CHOICES), required=False)
enabled = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect)
lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG')
mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU')
mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only') mgmt_only = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Management only')
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
class Meta: class Meta:
nullable_fields = ['lag', 'description'] nullable_fields = ['lag', 'mtu', 'description']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs) super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
@@ -1508,6 +1527,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
self.fields['lag'].choices = [] self.fields['lag'].choices = []
class InterfaceBulkDisconnectForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput)
# #
# Interface connections # Interface connections
# #
@@ -1562,7 +1585,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
) )
) )
interface_b = ChainedModelChoiceField( interface_b = ChainedModelChoiceField(
queryset=Interface.objects.exclude(form_factor__in=VIRTUAL_IFACE_TYPES).select_related( queryset=Interface.objects.connectable().select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b' 'circuit_termination', 'connected_as_a', 'connected_as_b'
), ),
chains=( chains=(
@@ -1571,7 +1594,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
label='Interface', label='Interface',
widget=APISelect( widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical', api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical',
disabled_indicator='connection' disabled_indicator='is_connected'
) )
) )
@@ -1584,9 +1607,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
super(InterfaceConnectionForm, self).__init__(*args, **kwargs) super(InterfaceConnectionForm, self).__init__(*args, **kwargs)
# Initialize interface A choices # Initialize interface A choices
device_a_interfaces = Interface.objects.order_naturally().filter(device=device_a).exclude( device_a_interfaces = Interface.objects.connectable().order_naturally().filter(device=device_a).select_related(
form_factor__in=VIRTUAL_IFACE_TYPES
).select_related(
'circuit_termination', 'connected_as_a', 'connected_as_b' 'circuit_termination', 'connected_as_a', 'connected_as_b'
) )
self.fields['interface_a'].choices = [ self.fields['interface_a'].choices = [
@@ -1594,6 +1615,7 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor
] ]
# Mark connected interfaces as disabled # Mark connected interfaces as disabled
if self.data.get('device_b'):
self.fields['interface_b'].choices = [ self.fields['interface_b'].choices = [
(iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset
] ]
@@ -1674,8 +1696,7 @@ class InterfaceConnectionCSVForm(forms.ModelForm):
return interface return interface
class InterfaceConnectionDeletionForm(BootstrapMixin, forms.Form): class InterfaceConnectionDeletionForm(ConfirmationForm):
confirm = forms.BooleanField(required=True)
# Used for HTTP redirect upon successful deletion # Used for HTTP redirect upon successful deletion
device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False) device = forms.ModelChoiceField(queryset=Device.objects.all(), widget=forms.HiddenInput(), required=False)
@@ -1745,4 +1766,4 @@ class InventoryItemForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['name', 'manufacturer', 'part_id', 'serial'] fields = ['name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description']

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-16 21:38
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0037_unicode_literals'),
]
operations = [
migrations.AlterField(
model_name='interface',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
migrations.AlterField(
model_name='interfacetemplate',
name='form_factor',
field=models.PositiveSmallIntegerField(choices=[['Virtual interfaces', [[0, 'Virtual'], [200, 'Link Aggregation Group (LAG)']]], ['Ethernet (fixed)', [[800, '100BASE-TX (10/100ME)'], [1000, '1000BASE-T (1GE)'], [1150, '10GBASE-T (10GE)']]], ['Ethernet (modular)', [[1050, 'GBIC (1GE)'], [1100, 'SFP (1GE)'], [1200, 'SFP+ (10GE)'], [1300, 'XFP (10GE)'], [1310, 'XENPAK (10GE)'], [1320, 'X2 (10GE)'], [1350, 'SFP28 (25GE)'], [1400, 'QSFP+ (40GE)'], [1500, 'CFP (100GE)'], [1600, 'QSFP28 (100GE)']]], ['Wireless', [[2600, 'IEEE 802.11a'], [2610, 'IEEE 802.11b/g'], [2620, 'IEEE 802.11n'], [2630, 'IEEE 802.11ac'], [2640, 'IEEE 802.11ad']]], ['FibreChannel', [[3010, 'SFP (1GFC)'], [3020, 'SFP (2GFC)'], [3040, 'SFP (4GFC)'], [3080, 'SFP+ (8GFC)'], [3160, 'SFP+ (16GFC)']]], ['Serial', [[4000, 'T1 (1.544 Mbps)'], [4010, 'E1 (2.048 Mbps)'], [4040, 'T3 (45 Mbps)'], [4050, 'E3 (34 Mbps)']]], ['Stacking', [[5000, 'Cisco StackWise'], [5050, 'Cisco StackWise Plus'], [5100, 'Cisco FlexStack'], [5150, 'Cisco FlexStack Plus'], [5200, 'Juniper VCP']]], ['Other', [[32767, 'Other']]]], default=1200),
),
]

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-23 17:05
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0038_wireless_interfaces'),
]
operations = [
migrations.AddField(
model_name='interface',
name='enabled',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='interface',
name='mtu',
field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU'),
),
]

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-06-23 20:44
from __future__ import unicode_literals
from django.db import migrations, models
import utilities.fields
class Migration(migrations.Migration):
dependencies = [
('dcim', '0039_interface_add_enabled_mtu'),
]
operations = [
migrations.AddField(
model_name='inventoryitem',
name='asset_tag',
field=utilities.fields.NullableCharField(blank=True, help_text='A unique tag used to identify this item', max_length=50, null=True, unique=True, verbose_name='Asset tag'),
),
migrations.AddField(
model_name='inventoryitem',
name='description',
field=models.CharField(blank=True, max_length=100),
),
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-14 17:26
from __future__ import unicode_literals
from django.db import migrations, models
def rpc_client_to_napalm_driver(apps, schema_editor):
"""
Migrate legacy RPC clients to their respective NAPALM drivers
"""
Platform = apps.get_model('dcim', 'Platform')
Platform.objects.filter(rpc_client='juniper-junos').update(napalm_driver='junos')
Platform.objects.filter(rpc_client='cisco-ios').update(napalm_driver='ios')
class Migration(migrations.Migration):
dependencies = [
('dcim', '0040_inventoryitem_add_asset_tag_description'),
]
operations = [
migrations.AlterModelOptions(
name='device',
options={'ordering': ['name'], 'permissions': (('napalm_read', 'Read-only access to devices via NAPALM'), ('napalm_write', 'Read/write access to devices via NAPALM'))},
),
migrations.AddField(
model_name='platform',
name='napalm_driver',
field=models.CharField(blank=True, help_text='The name of the NAPALM driver to use when interacting with devices.', max_length=50, verbose_name='NAPALM driver'),
),
migrations.AlterField(
model_name='platform',
name='rpc_client',
field=models.CharField(blank=True, choices=[['juniper-junos', 'Juniper Junos (NETCONF)'], ['cisco-ios', 'Cisco IOS (SSH)'], ['opengear', 'Opengear (SSH)']], max_length=30, verbose_name='Legacy RPC client'),
),
migrations.RunPython(rpc_client_to_napalm_driver),
]

View File

@@ -24,204 +24,10 @@ from utilities.fields import ColorField, NullableCharField
from utilities.managers import NaturalOrderByManager from utilities.managers import NaturalOrderByManager
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format from utilities.utils import csv_format
from .constants import *
from .fields import ASNField, MACAddressField from .fields import ASNField, MACAddressField
RACK_TYPE_2POST = 100
RACK_TYPE_4POST = 200
RACK_TYPE_CABINET = 300
RACK_TYPE_WALLFRAME = 1000
RACK_TYPE_WALLCABINET = 1100
RACK_TYPE_CHOICES = (
(RACK_TYPE_2POST, '2-post frame'),
(RACK_TYPE_4POST, '4-post frame'),
(RACK_TYPE_CABINET, '4-post cabinet'),
(RACK_TYPE_WALLFRAME, 'Wall-mounted frame'),
(RACK_TYPE_WALLCABINET, 'Wall-mounted cabinet'),
)
RACK_WIDTH_19IN = 19
RACK_WIDTH_23IN = 23
RACK_WIDTH_CHOICES = (
(RACK_WIDTH_19IN, '19 inches'),
(RACK_WIDTH_23IN, '23 inches'),
)
RACK_FACE_FRONT = 0
RACK_FACE_REAR = 1
RACK_FACE_CHOICES = [
[RACK_FACE_FRONT, 'Front'],
[RACK_FACE_REAR, 'Rear'],
]
SUBDEVICE_ROLE_PARENT = True
SUBDEVICE_ROLE_CHILD = False
SUBDEVICE_ROLE_CHOICES = (
(None, 'None'),
(SUBDEVICE_ROLE_PARENT, 'Parent'),
(SUBDEVICE_ROLE_CHILD, 'Child'),
)
IFACE_ORDERING_POSITION = 1
IFACE_ORDERING_NAME = 2
IFACE_ORDERING_CHOICES = [
[IFACE_ORDERING_POSITION, 'Slot/position'],
[IFACE_ORDERING_NAME, 'Name (alphabetically)']
]
# Virtual
IFACE_FF_VIRTUAL = 0
IFACE_FF_LAG = 200
# Ethernet
IFACE_FF_100ME_FIXED = 800
IFACE_FF_1GE_FIXED = 1000
IFACE_FF_1GE_GBIC = 1050
IFACE_FF_1GE_SFP = 1100
IFACE_FF_10GE_FIXED = 1150
IFACE_FF_10GE_SFP_PLUS = 1200
IFACE_FF_10GE_XFP = 1300
IFACE_FF_10GE_XENPAK = 1310
IFACE_FF_10GE_X2 = 1320
IFACE_FF_25GE_SFP28 = 1350
IFACE_FF_40GE_QSFP_PLUS = 1400
IFACE_FF_100GE_CFP = 1500
IFACE_FF_100GE_QSFP28 = 1600
# Fibrechannel
IFACE_FF_1GFC_SFP = 3010
IFACE_FF_2GFC_SFP = 3020
IFACE_FF_4GFC_SFP = 3040
IFACE_FF_8GFC_SFP_PLUS = 3080
IFACE_FF_16GFC_SFP_PLUS = 3160
# Serial
IFACE_FF_T1 = 4000
IFACE_FF_E1 = 4010
IFACE_FF_T3 = 4040
IFACE_FF_E3 = 4050
# Stacking
IFACE_FF_STACKWISE = 5000
IFACE_FF_STACKWISE_PLUS = 5050
IFACE_FF_FLEXSTACK = 5100
IFACE_FF_FLEXSTACK_PLUS = 5150
IFACE_FF_JUNIPER_VCP = 5200
# Other
IFACE_FF_OTHER = 32767
IFACE_FF_CHOICES = [
[
'Virtual interfaces',
[
[IFACE_FF_VIRTUAL, 'Virtual'],
[IFACE_FF_LAG, 'Link Aggregation Group (LAG)'],
]
],
[
'Ethernet (fixed)',
[
[IFACE_FF_100ME_FIXED, '100BASE-TX (10/100ME)'],
[IFACE_FF_1GE_FIXED, '1000BASE-T (1GE)'],
[IFACE_FF_10GE_FIXED, '10GBASE-T (10GE)'],
]
],
[
'Ethernet (modular)',
[
[IFACE_FF_1GE_GBIC, 'GBIC (1GE)'],
[IFACE_FF_1GE_SFP, 'SFP (1GE)'],
[IFACE_FF_10GE_SFP_PLUS, 'SFP+ (10GE)'],
[IFACE_FF_10GE_XFP, 'XFP (10GE)'],
[IFACE_FF_10GE_XENPAK, 'XENPAK (10GE)'],
[IFACE_FF_10GE_X2, 'X2 (10GE)'],
[IFACE_FF_25GE_SFP28, 'SFP28 (25GE)'],
[IFACE_FF_40GE_QSFP_PLUS, 'QSFP+ (40GE)'],
[IFACE_FF_100GE_CFP, 'CFP (100GE)'],
[IFACE_FF_100GE_QSFP28, 'QSFP28 (100GE)'],
]
],
[
'FibreChannel',
[
[IFACE_FF_1GFC_SFP, 'SFP (1GFC)'],
[IFACE_FF_2GFC_SFP, 'SFP (2GFC)'],
[IFACE_FF_4GFC_SFP, 'SFP (4GFC)'],
[IFACE_FF_8GFC_SFP_PLUS, 'SFP+ (8GFC)'],
[IFACE_FF_16GFC_SFP_PLUS, 'SFP+ (16GFC)'],
]
],
[
'Serial',
[
[IFACE_FF_T1, 'T1 (1.544 Mbps)'],
[IFACE_FF_E1, 'E1 (2.048 Mbps)'],
[IFACE_FF_T3, 'T3 (45 Mbps)'],
[IFACE_FF_E3, 'E3 (34 Mbps)'],
[IFACE_FF_E3, 'E3 (34 Mbps)'],
]
],
[
'Stacking',
[
[IFACE_FF_STACKWISE, 'Cisco StackWise'],
[IFACE_FF_STACKWISE_PLUS, 'Cisco StackWise Plus'],
[IFACE_FF_FLEXSTACK, 'Cisco FlexStack'],
[IFACE_FF_FLEXSTACK_PLUS, 'Cisco FlexStack Plus'],
[IFACE_FF_JUNIPER_VCP, 'Juniper VCP'],
]
],
[
'Other',
[
[IFACE_FF_OTHER, 'Other'],
]
],
]
VIRTUAL_IFACE_TYPES = [
IFACE_FF_VIRTUAL,
IFACE_FF_LAG,
]
STATUS_OFFLINE = 0
STATUS_ACTIVE = 1
STATUS_PLANNED = 2
STATUS_STAGED = 3
STATUS_FAILED = 4
STATUS_INVENTORY = 5
STATUS_CHOICES = [
[STATUS_ACTIVE, 'Active'],
[STATUS_OFFLINE, 'Offline'],
[STATUS_PLANNED, 'Planned'],
[STATUS_STAGED, 'Staged'],
[STATUS_FAILED, 'Failed'],
[STATUS_INVENTORY, 'Inventory'],
]
DEVICE_STATUS_CLASSES = {
0: 'warning',
1: 'success',
2: 'info',
3: 'primary',
4: 'danger',
5: 'default',
}
CONNECTION_STATUS_PLANNED = False
CONNECTION_STATUS_CONNECTED = True
CONNECTION_STATUS_CHOICES = [
[CONNECTION_STATUS_PLANNED, 'Planned'],
[CONNECTION_STATUS_CONNECTED, 'Connected'],
]
# For mapping platform -> NC client
RPC_CLIENT_JUNIPER_JUNOS = 'juniper-junos'
RPC_CLIENT_CISCO_IOS = 'cisco-ios'
RPC_CLIENT_OPENGEAR = 'opengear'
RPC_CLIENT_CHOICES = [
[RPC_CLIENT_JUNIPER_JUNOS, 'Juniper Junos (NETCONF)'],
[RPC_CLIENT_CISCO_IOS, 'Cisco IOS (SSH)'],
[RPC_CLIENT_OPENGEAR, 'Opengear (SSH)'],
]
# #
# Regions # Regions
# #
@@ -551,6 +357,16 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
return list(reversed(available_units)) return list(reversed(available_units))
def get_reserved_units(self):
"""
Return a dictionary mapping all reserved units within the rack to their reservation.
"""
reserved_units = {}
for r in self.reservations.all():
for u in r.units:
reserved_units[u] = r
return reserved_units
def get_0u_devices(self): def get_0u_devices(self):
return self.devices.filter(position=0) return self.devices.filter(position=0)
@@ -816,7 +632,7 @@ class PowerOutletTemplate(models.Model):
return self.name return self.name
class InterfaceManager(models.Manager): class InterfaceQuerySet(models.QuerySet):
def order_naturally(self, method=IFACE_ORDERING_POSITION): def order_naturally(self, method=IFACE_ORDERING_POSITION):
""" """
@@ -841,13 +657,12 @@ class InterfaceManager(models.Manager):
The original `name` field is taken as a whole to serve as a fallback in the event interfaces do not match any of The original `name` field is taken as a whole to serve as a fallback in the event interfaces do not match any of
the prescribed fields. the prescribed fields.
""" """
queryset = self.get_queryset() sql_col = '{}.name'.format(self.model._meta.db_table)
sql_col = '{}.name'.format(queryset.model._meta.db_table)
ordering = { ordering = {
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'), IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'), IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
}[method] }[method]
return queryset.extra(select={ return self.extra(select={
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col), '_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col), '_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col), '_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
@@ -856,6 +671,13 @@ class InterfaceManager(models.Manager):
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col), '_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
}).order_by(*ordering) }).order_by(*ordering)
def connectable(self):
"""
Return only physical interfaces which are capable of being connected to other interfaces (i.e. not virtual or
wireless).
"""
return self.exclude(form_factor__in=NONCONNECTABLE_IFACE_TYPES)
@python_2_unicode_compatible @python_2_unicode_compatible
class InterfaceTemplate(models.Model): class InterfaceTemplate(models.Model):
@@ -867,7 +689,7 @@ class InterfaceTemplate(models.Model):
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS) form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
mgmt_only = models.BooleanField(default=False, verbose_name='Management only') mgmt_only = models.BooleanField(default=False, verbose_name='Management only')
objects = InterfaceManager() objects = InterfaceQuerySet.as_manager()
class Meta: class Meta:
ordering = ['device_type', 'name'] ordering = ['device_type', 'name']
@@ -926,7 +748,10 @@ class Platform(models.Model):
""" """
name = models.CharField(max_length=50, unique=True) name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True, verbose_name='RPC client') napalm_driver = models.CharField(max_length=50, blank=True, verbose_name='NAPALM driver',
help_text="The name of the NAPALM driver to use when interacting with devices.")
rpc_client = models.CharField(max_length=30, choices=RPC_CLIENT_CHOICES, blank=True,
verbose_name='Legacy RPC client')
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@@ -997,6 +822,10 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
unique_together = ['rack', 'position', 'face'] unique_together = ['rack', 'position', 'face']
permissions = (
('napalm_read', 'Read-only access to devices via NAPALM'),
('napalm_write', 'Read/write access to devices via NAPALM'),
)
def __str__(self): def __str__(self):
return self.display_name or super(Device, self).__str__() return self.display_name or super(Device, self).__str__()
@@ -1308,16 +1137,27 @@ class Interface(models.Model):
of an InterfaceConnection. of an InterfaceConnection.
""" """
device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE) device = models.ForeignKey('Device', related_name='interfaces', on_delete=models.CASCADE)
lag = models.ForeignKey('self', related_name='member_interfaces', null=True, blank=True, on_delete=models.SET_NULL, lag = models.ForeignKey(
verbose_name='Parent LAG') 'self',
models.SET_NULL,
related_name='member_interfaces',
null=True,
blank=True,
verbose_name='Parent LAG'
)
name = models.CharField(max_length=30) name = models.CharField(max_length=30)
form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS) form_factor = models.PositiveSmallIntegerField(choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS)
enabled = models.BooleanField(default=True)
mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address') mac_address = MACAddressField(null=True, blank=True, verbose_name='MAC Address')
mgmt_only = models.BooleanField(default=False, verbose_name='OOB Management', mtu = models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='MTU')
help_text="This interface is used only for out-of-band management") mgmt_only = models.BooleanField(
default=False,
verbose_name='OOB Management',
help_text="This interface is used only for out-of-band management"
)
description = models.CharField(max_length=100, blank=True) description = models.CharField(max_length=100, blank=True)
objects = InterfaceManager() objects = InterfaceQuerySet.as_manager()
class Meta: class Meta:
ordering = ['device', 'name'] ordering = ['device', 'name']
@@ -1329,10 +1169,10 @@ class Interface(models.Model):
def clean(self): def clean(self):
# Virtual interfaces cannot be connected # Virtual interfaces cannot be connected
if self.form_factor in VIRTUAL_IFACE_TYPES and self.is_connected: if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.is_connected:
raise ValidationError({ raise ValidationError({
'form_factor': "Virtual interfaces cannot be connected to another interface or circuit. Disconnect the " 'form_factor': "Virtual and wireless interfaces cannot be connected to another interface or circuit. "
"interface or choose a physical form factor." "Disconnect the interface or choose a suitable form factor."
}) })
# An interface's LAG must belong to the same device # An interface's LAG must belong to the same device
@@ -1344,7 +1184,7 @@ class Interface(models.Model):
}) })
# A virtual interface cannot have a parent LAG # A virtual interface cannot have a parent LAG
if self.form_factor in VIRTUAL_IFACE_TYPES and self.lag is not None: if self.form_factor in NONCONNECTABLE_IFACE_TYPES and self.lag is not None:
raise ValidationError({ raise ValidationError({
'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display()) 'lag': "{} interfaces cannot have a parent LAG interface.".format(self.get_form_factor_display())
}) })
@@ -1361,6 +1201,10 @@ class Interface(models.Model):
def is_virtual(self): def is_virtual(self):
return self.form_factor in VIRTUAL_IFACE_TYPES return self.form_factor in VIRTUAL_IFACE_TYPES
@property
def is_wireless(self):
return self.form_factor in WIRELESS_IFACE_TYPES
@property @property
def is_lag(self): def is_lag(self):
return self.form_factor == IFACE_FF_LAG return self.form_factor == IFACE_FF_LAG
@@ -1479,11 +1323,17 @@ class InventoryItem(models.Model):
device = models.ForeignKey('Device', related_name='inventory_items', on_delete=models.CASCADE) device = models.ForeignKey('Device', related_name='inventory_items', on_delete=models.CASCADE)
parent = models.ForeignKey('self', related_name='child_items', blank=True, null=True, on_delete=models.CASCADE) parent = models.ForeignKey('self', related_name='child_items', blank=True, null=True, on_delete=models.CASCADE)
name = models.CharField(max_length=50, verbose_name='Name') name = models.CharField(max_length=50, verbose_name='Name')
manufacturer = models.ForeignKey('Manufacturer', related_name='inventory_items', blank=True, null=True, manufacturer = models.ForeignKey(
on_delete=models.PROTECT) 'Manufacturer', models.PROTECT, related_name='inventory_items', blank=True, null=True
)
part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True) part_id = models.CharField(max_length=50, verbose_name='Part ID', blank=True)
serial = models.CharField(max_length=50, verbose_name='Serial number', blank=True) serial = models.CharField(max_length=50, verbose_name='Serial number', blank=True)
asset_tag = NullableCharField(
max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
help_text='A unique tag used to identify this item'
)
discovered = models.BooleanField(default=False, verbose_name='Discovered') discovered = models.BooleanField(default=False, verbose_name='Discovered')
description = models.CharField(max_length=100, blank=True)
class Meta: class Meta:
ordering = ['device__id', 'parent__id', 'name'] ordering = ['device__id', 'parent__id', 'name']

View File

@@ -3,11 +3,11 @@ from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from utilities.tables import BaseTable, SearchTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPortTemplate, Device, DeviceBayTemplate, DeviceRole, DeviceType, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, Platform, PowerOutlet,
RackGroup, RackReservation, Region, Site, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
) )
@@ -142,30 +142,26 @@ class SiteTable(BaseTable):
name = tables.LinkColumn() name = tables.LinkColumn()
region = tables.TemplateColumn(template_code=SITE_REGION_LINK) region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
class Meta(BaseTable.Meta):
model = Site
fields = ('pk', 'name', 'facility', 'region', 'tenant', 'asn')
class SiteDetailTable(SiteTable):
rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks') rack_count = tables.Column(accessor=Accessor('count_racks'), orderable=False, verbose_name='Racks')
device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices') device_count = tables.Column(accessor=Accessor('count_devices'), orderable=False, verbose_name='Devices')
prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes') prefix_count = tables.Column(accessor=Accessor('count_prefixes'), orderable=False, verbose_name='Prefixes')
vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs') vlan_count = tables.Column(accessor=Accessor('count_vlans'), orderable=False, verbose_name='VLANs')
circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits') circuit_count = tables.Column(accessor=Accessor('count_circuits'), orderable=False, verbose_name='Circuits')
class Meta(BaseTable.Meta): class Meta(SiteTable.Meta):
model = Site
fields = ( fields = (
'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count', 'pk', 'name', 'facility', 'region', 'tenant', 'asn', 'rack_count', 'device_count', 'prefix_count',
'vlan_count', 'circuit_count', 'vlan_count', 'circuit_count',
) )
class SiteSearchTable(SearchTable):
name = tables.LinkColumn()
region = tables.TemplateColumn(template_code=SITE_REGION_LINK)
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
class Meta(SearchTable.Meta):
model = Site
fields = ('name', 'facility', 'region', 'tenant', 'asn')
# #
# Rack groups # Rack groups
# #
@@ -214,29 +210,22 @@ class RackTable(BaseTable):
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
role = tables.TemplateColumn(RACK_ROLE) role = tables.TemplateColumn(RACK_ROLE)
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height') u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
devices = tables.Column(accessor=Accessor('device_count'))
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Rack model = Rack
fields = ('pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height')
class RackDetailTable(RackTable):
devices = tables.Column(accessor=Accessor('device_count'))
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
class Meta(RackTable.Meta):
fields = ( fields = (
'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization' 'pk', 'name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height', 'devices', 'get_utilization'
) )
class RackSearchTable(SearchTable):
name = tables.LinkColumn()
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
role = tables.TemplateColumn(RACK_ROLE)
u_height = tables.TemplateColumn("{{ record.u_height }}U", verbose_name='Height')
class Meta(SearchTable.Meta):
model = Rack
fields = ('name', 'site', 'group', 'facility_id', 'tenant', 'role', 'u_height')
class RackImportTable(BaseTable): class RackImportTable(BaseTable):
name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name') name = tables.LinkColumn('dcim:rack', args=[Accessor('pk')], verbose_name='Name')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
@@ -302,23 +291,7 @@ class DeviceTypeTable(BaseTable):
model = DeviceType model = DeviceType
fields = ( fields = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu', 'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device', 'subdevice_role', 'instance_count' 'is_network_device', 'subdevice_role', 'instance_count',
)
class DeviceTypeSearchTable(SearchTable):
model = tables.LinkColumn('dcim:devicetype', args=[Accessor('pk')], verbose_name='Device Type')
is_full_depth = tables.BooleanColumn(verbose_name='Full Depth')
is_console_server = tables.BooleanColumn(verbose_name='CS')
is_pdu = tables.BooleanColumn(verbose_name='PDU')
is_network_device = tables.BooleanColumn(verbose_name='Net')
subdevice_role = tables.TemplateColumn(SUBDEVICE_ROLE_TEMPLATE, verbose_name='Subdevice Role')
class Meta(SearchTable.Meta):
model = DeviceType
fields = (
'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'is_console_server', 'is_pdu',
'is_network_device', 'subdevice_role',
) )
@@ -333,7 +306,6 @@ class ConsolePortTemplateTable(BaseTable):
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ('pk', 'name') fields = ('pk', 'name')
empty_text = "None" empty_text = "None"
show_header = False
class ConsoleServerPortTemplateTable(BaseTable): class ConsoleServerPortTemplateTable(BaseTable):
@@ -343,7 +315,6 @@ class ConsoleServerPortTemplateTable(BaseTable):
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ('pk', 'name') fields = ('pk', 'name')
empty_text = "None" empty_text = "None"
show_header = False
class PowerPortTemplateTable(BaseTable): class PowerPortTemplateTable(BaseTable):
@@ -353,7 +324,6 @@ class PowerPortTemplateTable(BaseTable):
model = PowerPortTemplate model = PowerPortTemplate
fields = ('pk', 'name') fields = ('pk', 'name')
empty_text = "None" empty_text = "None"
show_header = False
class PowerOutletTemplateTable(BaseTable): class PowerOutletTemplateTable(BaseTable):
@@ -363,17 +333,16 @@ class PowerOutletTemplateTable(BaseTable):
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ('pk', 'name') fields = ('pk', 'name')
empty_text = "None" empty_text = "None"
show_header = False
class InterfaceTemplateTable(BaseTable): class InterfaceTemplateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
mgmt_only = tables.TemplateColumn("{% if value %}OOB Management{% endif %}")
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = InterfaceTemplate model = InterfaceTemplate
fields = ('pk', 'name', 'form_factor') fields = ('pk', 'name', 'mgmt_only', 'form_factor')
empty_text = "None" empty_text = "None"
show_header = False
class DeviceBayTemplateTable(BaseTable): class DeviceBayTemplateTable(BaseTable):
@@ -383,7 +352,6 @@ class DeviceBayTemplateTable(BaseTable):
model = DeviceBayTemplate model = DeviceBayTemplate
fields = ('pk', 'name') fields = ('pk', 'name')
empty_text = "None" empty_text = "None"
show_header = False
# #
@@ -438,32 +406,22 @@ class DeviceTable(BaseTable):
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type', 'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
text=lambda record: record.device_type.full_name text=lambda record: record.device_type.full_name
) )
class Meta(BaseTable.Meta):
model = Device
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type')
class DeviceDetailTable(DeviceTable):
primary_ip = tables.TemplateColumn( primary_ip = tables.TemplateColumn(
orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP orderable=False, verbose_name='IP Address', template_code=DEVICE_PRIMARY_IP
) )
class Meta(BaseTable.Meta): class Meta(DeviceTable.Meta):
model = Device model = Device
fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip') fields = ('pk', 'name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type', 'primary_ip')
class DeviceSearchTable(SearchTable):
name = tables.TemplateColumn(template_code=DEVICE_LINK)
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
rack = tables.LinkColumn('dcim:rack', args=[Accessor('rack.pk')])
device_role = tables.TemplateColumn(DEVICE_ROLE, verbose_name='Role')
device_type = tables.LinkColumn(
'dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
text=lambda record: record.device_type.full_name
)
class Meta(SearchTable.Meta):
model = Device
fields = ('name', 'status', 'tenant', 'site', 'rack', 'device_role', 'device_type')
class DeviceImportTable(BaseTable): class DeviceImportTable(BaseTable):
name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name') name = tables.TemplateColumn(template_code=DEVICE_LINK, verbose_name='Name')
status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status') status = tables.TemplateColumn(template_code=DEVICE_STATUS, verbose_name='Status')
@@ -480,6 +438,52 @@ class DeviceImportTable(BaseTable):
empty_text = False empty_text = False
#
# Device components
#
class ConsolePortTable(BaseTable):
class Meta(BaseTable.Meta):
model = ConsolePort
fields = ('name',)
class ConsoleServerPortTable(BaseTable):
class Meta(BaseTable.Meta):
model = ConsoleServerPort
fields = ('name',)
class PowerPortTable(BaseTable):
class Meta(BaseTable.Meta):
model = PowerPort
fields = ('name',)
class PowerOutletTable(BaseTable):
class Meta(BaseTable.Meta):
model = PowerOutlet
fields = ('name',)
class InterfaceTable(BaseTable):
class Meta(BaseTable.Meta):
model = Interface
fields = ('name', 'form_factor', 'lag', 'enabled', 'mgmt_only', 'description')
class DeviceBayTable(BaseTable):
class Meta(BaseTable.Meta):
model = DeviceBay
fields = ('name',)
# #
# Device connections # Device connections
# #

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from extras.views import ImageAttachmentEditView from extras.views import ImageAttachmentEditView
from ipam.views import ServiceEditView from ipam.views import ServiceCreateView
from secrets.views import secret_add from secrets.views import secret_add
from .models import Device, Rack, Site from .models import Device, Rack, Site
from . import views from . import views
@@ -14,13 +14,13 @@ urlpatterns = [
# Regions # Regions
url(r'^regions/$', views.RegionListView.as_view(), name='region_list'), 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/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'),
url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'), url(r'^regions/(?P<pk>\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'),
# Sites # Sites
url(r'^sites/$', views.SiteListView.as_view(), name='site_list'), 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/import/$', views.SiteBulkImportView.as_view(), name='site_import'),
url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'),
url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'), url(r'^sites/(?P<slug>[\w-]+)/$', views.SiteView.as_view(), name='site'),
@@ -30,13 +30,13 @@ urlpatterns = [
# Rack groups # Rack groups
url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'), 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/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'),
url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'), url(r'^rack-groups/(?P<pk>\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'),
# Rack roles # Rack roles
url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'), 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/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'),
url(r'^rack-roles/(?P<pk>\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'), 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+)/$', 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+)/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<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}), url(r'^racks/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}),
# Manufacturers # Manufacturers
url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'), 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/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'),
url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), url(r'^manufacturers/(?P<slug>[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'),
# Device types # Device types
url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'), 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/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/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'),
url(r'^device-types/(?P<pk>\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'), 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'), url(r'^device-types/(?P<pk>\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'),
# Console port templates # 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'), url(r'^device-types/(?P<pk>\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'),
# Console server port templates # 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'), url(r'^device-types/(?P<pk>\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'),
# Power port templates # 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'), url(r'^device-types/(?P<pk>\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'),
# Power outlet templates # 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'), url(r'^device-types/(?P<pk>\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'),
# Interface templates # 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/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'), url(r'^device-types/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'),
# Device bay templates # 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'), url(r'^device-types/(?P<pk>\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'),
# Device roles # Device roles
url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'), 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/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'),
url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), url(r'^device-roles/(?P<slug>[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'),
# Platforms # Platforms
url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'), 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/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'),
url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'), url(r'^platforms/(?P<slug>[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'),
# Devices # Devices
url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'), 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/$', views.DeviceBulkImportView.as_view(), name='device_import'),
url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), 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'), url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'),
@@ -122,14 +122,16 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'), url(r'^devices/(?P<pk>\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'),
url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'), url(r'^devices/(?P<pk>\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'),
url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), url(r'^devices/(?P<pk>\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'),
url(r'^devices/(?P<pk>\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'),
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
url(r'^devices/(?P<pk>\d+)/config/$', views.DeviceConfigView.as_view(), name='device_config'),
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'), url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
url(r'^devices/(?P<device>\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'), url(r'^devices/(?P<device>\d+)/services/assign/$', 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}), url(r'^devices/(?P<object_id>\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}),
# Console ports # Console ports
url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), 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'^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+)/connect/$', views.consoleport_connect, name='consoleport_connect'),
url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'), url(r'^console-ports/(?P<pk>\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'),
@@ -138,7 +140,8 @@ urlpatterns = [
# Console server ports # Console server ports
url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), 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'^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+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'),
url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'), url(r'^console-server-ports/(?P<pk>\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'),
@@ -147,7 +150,7 @@ urlpatterns = [
# Power ports # Power ports
url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), 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'^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+)/connect/$', views.powerport_connect, name='powerport_connect'),
url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'), url(r'^power-ports/(?P<pk>\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'),
@@ -156,7 +159,8 @@ urlpatterns = [
# Power outlets # Power outlets
url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), 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'^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+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'),
url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'), url(r'^power-outlets/(?P<pk>\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'),
@@ -165,8 +169,9 @@ urlpatterns = [
# Interfaces # Interfaces
url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), 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/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+)/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'^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'), url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'),
@@ -175,7 +180,7 @@ urlpatterns = [
# Device bays # Device bays
url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), 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'^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+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'),
url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), url(r'^device-bays/(?P<pk>\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'),

View File

@@ -1,6 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from copy import deepcopy from copy import deepcopy
from difflib import SequenceMatcher
import re import re
from natsort import natsorted from natsort import natsorted
from operator import attrgetter from operator import attrgetter
@@ -9,7 +8,7 @@ from django.contrib import messages
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.paginator import EmptyPage, PageNotAnInteger 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.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
@@ -142,6 +141,44 @@ class ComponentDeleteView(ObjectDeleteView):
return obj.device.get_absolute_url() 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 # Regions
# #
@@ -152,8 +189,8 @@ class RegionListView(ObjectListView):
template_name = 'dcim/region_list.html' template_name = 'dcim/region_list.html'
class RegionEditView(PermissionRequiredMixin, ObjectEditView): class RegionCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_region' permission_required = 'dcim.add_region'
model = Region model = Region
form_class = forms.RegionForm form_class = forms.RegionForm
@@ -161,9 +198,15 @@ class RegionEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:region_list') return reverse('dcim:region_list')
class RegionEditView(RegionCreateView):
permission_required = 'dcim.change_region'
class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_region' permission_required = 'dcim.delete_region'
cls = Region cls = Region
queryset = Region.objects.annotate(site_count=Count('sites'))
table = tables.RegionTable
default_return_url = 'dcim:region_list' default_return_url = 'dcim:region_list'
@@ -175,7 +218,7 @@ class SiteListView(ObjectListView):
queryset = Site.objects.select_related('region', 'tenant') queryset = Site.objects.select_related('region', 'tenant')
filter = filters.SiteFilter filter = filters.SiteFilter
filter_form = forms.SiteFilterForm filter_form = forms.SiteFilterForm
table = tables.SiteTable table = tables.SiteDetailTable
template_name = 'dcim/site_list.html' template_name = 'dcim/site_list.html'
@@ -204,14 +247,18 @@ class SiteView(View):
}) })
class SiteEditView(PermissionRequiredMixin, ObjectEditView): class SiteCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_site' permission_required = 'dcim.add_site'
model = Site model = Site
form_class = forms.SiteForm form_class = forms.SiteForm
template_name = 'dcim/site_edit.html' template_name = 'dcim/site_edit.html'
default_return_url = 'dcim:site_list' default_return_url = 'dcim:site_list'
class SiteEditView(SiteCreateView):
permission_required = 'dcim.change_site'
class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView): class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_site' permission_required = 'dcim.delete_site'
model = Site model = Site
@@ -228,9 +275,10 @@ class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
class SiteBulkEditView(PermissionRequiredMixin, BulkEditView): class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_site' permission_required = 'dcim.change_site'
cls = Site cls = Site
queryset = Site.objects.select_related('region', 'tenant')
filter = filters.SiteFilter filter = filters.SiteFilter
table = tables.SiteTable
form = forms.SiteBulkEditForm form = forms.SiteBulkEditForm
template_name = 'dcim/site_bulk_edit.html'
default_return_url = 'dcim:site_list' default_return_url = 'dcim:site_list'
@@ -246,8 +294,8 @@ class RackGroupListView(ObjectListView):
template_name = 'dcim/rackgroup_list.html' template_name = 'dcim/rackgroup_list.html'
class RackGroupEditView(PermissionRequiredMixin, ObjectEditView): class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackgroup' permission_required = 'dcim.add_rackgroup'
model = RackGroup model = RackGroup
form_class = forms.RackGroupForm form_class = forms.RackGroupForm
@@ -255,10 +303,16 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:rackgroup_list') return reverse('dcim:rackgroup_list')
class RackGroupEditView(RackGroupCreateView):
permission_required = 'dcim.change_rackgroup'
class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackgroup' permission_required = 'dcim.delete_rackgroup'
cls = RackGroup cls = RackGroup
queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
filter = filters.RackGroupFilter filter = filters.RackGroupFilter
table = tables.RackGroupTable
default_return_url = 'dcim:rackgroup_list' default_return_url = 'dcim:rackgroup_list'
@@ -272,8 +326,8 @@ class RackRoleListView(ObjectListView):
template_name = 'dcim/rackrole_list.html' template_name = 'dcim/rackrole_list.html'
class RackRoleEditView(PermissionRequiredMixin, ObjectEditView): class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackrole' permission_required = 'dcim.add_rackrole'
model = RackRole model = RackRole
form_class = forms.RackRoleForm form_class = forms.RackRoleForm
@@ -281,9 +335,15 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:rackrole_list') return reverse('dcim:rackrole_list')
class RackRoleEditView(RackRoleCreateView):
permission_required = 'dcim.change_rackrole'
class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackrole' permission_required = 'dcim.delete_rackrole'
cls = RackRole cls = RackRole
queryset = RackRole.objects.annotate(rack_count=Count('racks'))
table = tables.RackRoleTable
default_return_url = 'dcim:rackrole_list' default_return_url = 'dcim:rackrole_list'
@@ -301,7 +361,7 @@ class RackListView(ObjectListView):
) )
filter = filters.RackFilter filter = filters.RackFilter
filter_form = forms.RackFilterForm filter_form = forms.RackFilterForm
table = tables.RackTable table = tables.RackDetailTable
template_name = 'dcim/rack_list.html' template_name = 'dcim/rack_list.html'
@@ -357,15 +417,10 @@ class RackView(View):
prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first() prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
reservations = RackReservation.objects.filter(rack=rack) reservations = RackReservation.objects.filter(rack=rack)
reserved_units = {}
for r in reservations:
for u in r.units:
reserved_units[u] = r
return render(request, 'dcim/rack.html', { return render(request, 'dcim/rack.html', {
'rack': rack, 'rack': rack,
'reservations': reservations, 'reservations': reservations,
'reserved_units': reserved_units,
'nonracked_devices': nonracked_devices, 'nonracked_devices': nonracked_devices,
'next_rack': next_rack, 'next_rack': next_rack,
'prev_rack': prev_rack, 'prev_rack': prev_rack,
@@ -374,14 +429,18 @@ class RackView(View):
}) })
class RackEditView(PermissionRequiredMixin, ObjectEditView): class RackCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rack' permission_required = 'dcim.add_rack'
model = Rack model = Rack
form_class = forms.RackForm form_class = forms.RackForm
template_name = 'dcim/rack_edit.html' template_name = 'dcim/rack_edit.html'
default_return_url = 'dcim:rack_list' default_return_url = 'dcim:rack_list'
class RackEditView(RackCreateView):
permission_required = 'dcim.change_rack'
class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView): class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_rack' permission_required = 'dcim.delete_rack'
model = Rack model = Rack
@@ -398,16 +457,19 @@ class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
class RackBulkEditView(PermissionRequiredMixin, BulkEditView): class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_rack' permission_required = 'dcim.change_rack'
cls = Rack cls = Rack
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role')
filter = filters.RackFilter filter = filters.RackFilter
table = tables.RackTable
form = forms.RackBulkEditForm form = forms.RackBulkEditForm
template_name = 'dcim/rack_bulk_edit.html'
default_return_url = 'dcim:rack_list' default_return_url = 'dcim:rack_list'
class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rack' permission_required = 'dcim.delete_rack'
cls = Rack cls = Rack
queryset = Rack.objects.select_related('site', 'group', 'tenant', 'role')
filter = filters.RackFilter filter = filters.RackFilter
table = tables.RackTable
default_return_url = 'dcim:rack_list' default_return_url = 'dcim:rack_list'
@@ -423,8 +485,8 @@ class RackReservationListView(ObjectListView):
template_name = 'dcim/rackreservation_list.html' template_name = 'dcim/rackreservation_list.html'
class RackReservationEditView(PermissionRequiredMixin, ObjectEditView): class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_rackreservation' permission_required = 'dcim.add_rackreservation'
model = RackReservation model = RackReservation
form_class = forms.RackReservationForm form_class = forms.RackReservationForm
@@ -438,6 +500,10 @@ class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
return obj.rack.get_absolute_url() return obj.rack.get_absolute_url()
class RackReservationEditView(RackReservationCreateView):
permission_required = 'dcim.change_rackreservation'
class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView): class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_rackreservation' permission_required = 'dcim.delete_rackreservation'
model = RackReservation model = RackReservation
@@ -449,6 +515,7 @@ class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_rackreservation' permission_required = 'dcim.delete_rackreservation'
cls = RackReservation cls = RackReservation
table = tables.RackReservationTable
default_return_url = 'dcim:rackreservation_list' default_return_url = 'dcim:rackreservation_list'
@@ -462,8 +529,8 @@ class ManufacturerListView(ObjectListView):
template_name = 'dcim/manufacturer_list.html' template_name = 'dcim/manufacturer_list.html'
class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView): class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_manufacturer' permission_required = 'dcim.add_manufacturer'
model = Manufacturer model = Manufacturer
form_class = forms.ManufacturerForm form_class = forms.ManufacturerForm
@@ -471,9 +538,15 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:manufacturer_list') return reverse('dcim:manufacturer_list')
class ManufacturerEditView(ManufacturerCreateView):
permission_required = 'dcim.change_manufacturer'
class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_manufacturer' permission_required = 'dcim.delete_manufacturer'
cls = Manufacturer cls = Manufacturer
queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
table = tables.ManufacturerTable
default_return_url = 'dcim:manufacturer_list' default_return_url = 'dcim:manufacturer_list'
@@ -497,36 +570,36 @@ class DeviceTypeView(View):
# Component tables # Component tables
consoleport_table = tables.ConsolePortTemplateTable( consoleport_table = tables.ConsolePortTemplateTable(
natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
show_header=False
) )
consoleserverport_table = tables.ConsoleServerPortTemplateTable( consoleserverport_table = tables.ConsoleServerPortTemplateTable(
natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
show_header=False
) )
powerport_table = tables.PowerPortTemplateTable( powerport_table = tables.PowerPortTemplateTable(
natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
show_header=False
) )
poweroutlet_table = tables.PowerOutletTemplateTable( poweroutlet_table = tables.PowerOutletTemplateTable(
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
) show_header=False
mgmt_interface_table = tables.InterfaceTemplateTable(
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
device_type=devicetype, mgmt_only=True
))
) )
interface_table = tables.InterfaceTemplateTable( interface_table = tables.InterfaceTemplateTable(
list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter( list(InterfaceTemplate.objects.order_naturally(
device_type=devicetype, mgmt_only=False devicetype.interface_ordering
)) ).filter(device_type=devicetype)),
show_header=False
) )
devicebay_table = tables.DeviceBayTemplateTable( devicebay_table = tables.DeviceBayTemplateTable(
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')),
show_header=False
) )
if request.user.has_perm('dcim.change_devicetype'): if request.user.has_perm('dcim.change_devicetype'):
consoleport_table.base_columns['pk'].visible = True consoleport_table.base_columns['pk'].visible = True
consoleserverport_table.base_columns['pk'].visible = True consoleserverport_table.base_columns['pk'].visible = True
powerport_table.base_columns['pk'].visible = True powerport_table.base_columns['pk'].visible = True
poweroutlet_table.base_columns['pk'].visible = True poweroutlet_table.base_columns['pk'].visible = True
mgmt_interface_table.base_columns['pk'].visible = True
interface_table.base_columns['pk'].visible = True interface_table.base_columns['pk'].visible = True
devicebay_table.base_columns['pk'].visible = True devicebay_table.base_columns['pk'].visible = True
@@ -536,20 +609,23 @@ class DeviceTypeView(View):
'consoleserverport_table': consoleserverport_table, 'consoleserverport_table': consoleserverport_table,
'powerport_table': powerport_table, 'powerport_table': powerport_table,
'poweroutlet_table': poweroutlet_table, 'poweroutlet_table': poweroutlet_table,
'mgmt_interface_table': mgmt_interface_table,
'interface_table': interface_table, 'interface_table': interface_table,
'devicebay_table': devicebay_table, 'devicebay_table': devicebay_table,
}) })
class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView): class DeviceTypeCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_devicetype' permission_required = 'dcim.add_devicetype'
model = DeviceType model = DeviceType
form_class = forms.DeviceTypeForm form_class = forms.DeviceTypeForm
template_name = 'dcim/devicetype_edit.html' template_name = 'dcim/devicetype_edit.html'
default_return_url = 'dcim:devicetype_list' default_return_url = 'dcim:devicetype_list'
class DeviceTypeEditView(DeviceTypeCreateView):
permission_required = 'dcim.change_devicetype'
class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView): class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_devicetype' permission_required = 'dcim.delete_devicetype'
model = DeviceType model = DeviceType
@@ -559,16 +635,19 @@ class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView): class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_devicetype' permission_required = 'dcim.change_devicetype'
cls = DeviceType cls = DeviceType
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
filter = filters.DeviceTypeFilter filter = filters.DeviceTypeFilter
table = tables.DeviceTypeTable
form = forms.DeviceTypeBulkEditForm form = forms.DeviceTypeBulkEditForm
template_name = 'dcim/devicetype_bulk_edit.html'
default_return_url = 'dcim:devicetype_list' default_return_url = 'dcim:devicetype_list'
class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicetype' permission_required = 'dcim.delete_devicetype'
cls = DeviceType cls = DeviceType
queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
filter = filters.DeviceTypeFilter filter = filters.DeviceTypeFilter
table = tables.DeviceTypeTable
default_return_url = 'dcim:devicetype_list' default_return_url = 'dcim:devicetype_list'
@@ -576,7 +655,7 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Device type components # Device type components
# #
class ConsolePortTemplateAddView(PermissionRequiredMixin, ComponentCreateView): class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleporttemplate' permission_required = 'dcim.add_consoleporttemplate'
parent_model = DeviceType parent_model = DeviceType
parent_field = 'device_type' parent_field = 'device_type'
@@ -591,9 +670,10 @@ class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
parent_field = 'device_type' parent_field = 'device_type'
cls = ConsolePortTemplate cls = ConsolePortTemplate
parent_cls = DeviceType parent_cls = DeviceType
table = tables.ConsolePortTemplateTable
class ConsoleServerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView): class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverporttemplate' permission_required = 'dcim.add_consoleserverporttemplate'
parent_model = DeviceType parent_model = DeviceType
parent_field = 'device_type' parent_field = 'device_type'
@@ -606,9 +686,10 @@ class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDelet
permission_required = 'dcim.delete_consoleserverporttemplate' permission_required = 'dcim.delete_consoleserverporttemplate'
cls = ConsoleServerPortTemplate cls = ConsoleServerPortTemplate
parent_cls = DeviceType parent_cls = DeviceType
table = tables.ConsoleServerPortTemplateTable
class PowerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView): class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerporttemplate' permission_required = 'dcim.add_powerporttemplate'
parent_model = DeviceType parent_model = DeviceType
parent_field = 'device_type' parent_field = 'device_type'
@@ -621,9 +702,10 @@ class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerporttemplate' permission_required = 'dcim.delete_powerporttemplate'
cls = PowerPortTemplate cls = PowerPortTemplate
parent_cls = DeviceType parent_cls = DeviceType
table = tables.PowerPortTemplateTable
class PowerOutletTemplateAddView(PermissionRequiredMixin, ComponentCreateView): class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlettemplate' permission_required = 'dcim.add_poweroutlettemplate'
parent_model = DeviceType parent_model = DeviceType
parent_field = 'device_type' parent_field = 'device_type'
@@ -636,9 +718,10 @@ class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView)
permission_required = 'dcim.delete_poweroutlettemplate' permission_required = 'dcim.delete_poweroutlettemplate'
cls = PowerOutletTemplate cls = PowerOutletTemplate
parent_cls = DeviceType parent_cls = DeviceType
table = tables.PowerOutletTemplateTable
class InterfaceTemplateAddView(PermissionRequiredMixin, ComponentCreateView): class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_interfacetemplate' permission_required = 'dcim.add_interfacetemplate'
parent_model = DeviceType parent_model = DeviceType
parent_field = 'device_type' parent_field = 'device_type'
@@ -651,17 +734,18 @@ class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interfacetemplate' permission_required = 'dcim.change_interfacetemplate'
cls = InterfaceTemplate cls = InterfaceTemplate
parent_cls = DeviceType parent_cls = DeviceType
table = tables.InterfaceTemplateTable
form = forms.InterfaceTemplateBulkEditForm form = forms.InterfaceTemplateBulkEditForm
template_name = 'dcim/interfacetemplate_bulk_edit.html'
class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interfacetemplate' permission_required = 'dcim.delete_interfacetemplate'
cls = InterfaceTemplate cls = InterfaceTemplate
parent_cls = DeviceType parent_cls = DeviceType
table = tables.InterfaceTemplateTable
class DeviceBayTemplateAddView(PermissionRequiredMixin, ComponentCreateView): class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebaytemplate' permission_required = 'dcim.add_devicebaytemplate'
parent_model = DeviceType parent_model = DeviceType
parent_field = 'device_type' parent_field = 'device_type'
@@ -674,6 +758,7 @@ class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebaytemplate' permission_required = 'dcim.delete_devicebaytemplate'
cls = DeviceBayTemplate cls = DeviceBayTemplate
parent_cls = DeviceType parent_cls = DeviceType
table = tables.DeviceBayTemplateTable
# #
@@ -686,8 +771,8 @@ class DeviceRoleListView(ObjectListView):
template_name = 'dcim/devicerole_list.html' template_name = 'dcim/devicerole_list.html'
class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView): class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_devicerole' permission_required = 'dcim.add_devicerole'
model = DeviceRole model = DeviceRole
form_class = forms.DeviceRoleForm form_class = forms.DeviceRoleForm
@@ -695,9 +780,15 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:devicerole_list') return reverse('dcim:devicerole_list')
class DeviceRoleEditView(DeviceRoleCreateView):
permission_required = 'dcim.change_devicerole'
class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicerole' permission_required = 'dcim.delete_devicerole'
cls = DeviceRole cls = DeviceRole
queryset = DeviceRole.objects.annotate(device_count=Count('devices'))
table = tables.DeviceRoleTable
default_return_url = 'dcim:devicerole_list' default_return_url = 'dcim:devicerole_list'
@@ -711,8 +802,8 @@ class PlatformListView(ObjectListView):
template_name = 'dcim/platform_list.html' template_name = 'dcim/platform_list.html'
class PlatformEditView(PermissionRequiredMixin, ObjectEditView): class PlatformCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.change_platform' permission_required = 'dcim.add_platform'
model = Platform model = Platform
form_class = forms.PlatformForm form_class = forms.PlatformForm
@@ -720,9 +811,15 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('dcim:platform_list') return reverse('dcim:platform_list')
class PlatformEditView(PlatformCreateView):
permission_required = 'dcim.change_platform'
class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_platform' permission_required = 'dcim.delete_platform'
cls = Platform cls = Platform
queryset = Platform.objects.annotate(device_count=Count('devices'))
table = tables.PlatformTable
default_return_url = 'dcim:platform_list' default_return_url = 'dcim:platform_list'
@@ -735,7 +832,7 @@ class DeviceListView(ObjectListView):
'primary_ip4', 'primary_ip6') 'primary_ip4', 'primary_ip6')
filter = filters.DeviceFilter filter = filters.DeviceFilter
filter_form = forms.DeviceFilterForm filter_form = forms.DeviceFilterForm
table = tables.DeviceTable table = tables.DeviceDetailTable
template_name = 'dcim/device_list.html' template_name = 'dcim/device_list.html'
@@ -758,14 +855,10 @@ class DeviceView(View):
power_outlets = natsorted( power_outlets = natsorted(
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name') PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
) )
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter( interfaces = Interface.objects.order_naturally(
device=device, mgmt_only=False device.device_type.interface_ordering
).select_related( ).filter(
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', device=device
'circuit_termination__circuit'
).prefetch_related('ip_addresses')
mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
device=device, mgmt_only=True
).select_related( ).select_related(
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', 'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
'circuit_termination__circuit' 'circuit_termination__circuit'
@@ -796,7 +889,6 @@ class DeviceView(View):
'power_ports': power_ports, 'power_ports': power_ports,
'power_outlets': power_outlets, 'power_outlets': power_outlets,
'interfaces': interfaces, 'interfaces': interfaces,
'mgmt_interfaces': mgmt_interfaces,
'device_bays': device_bays, 'device_bays': device_bays,
'services': services, 'services': services,
'secrets': secrets, 'secrets': secrets,
@@ -824,7 +916,20 @@ class DeviceInventoryView(View):
}) })
class DeviceLLDPNeighborsView(View): class DeviceStatusView(PermissionRequiredMixin, View):
permission_required = 'dcim.napalm_read'
def get(self, request, pk):
device = get_object_or_404(Device, pk=pk)
return render(request, 'dcim/device_status.html', {
'device': device,
})
class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
permission_required = 'dcim.napalm_read'
def get(self, request, pk): def get(self, request, pk):
@@ -843,14 +948,30 @@ class DeviceLLDPNeighborsView(View):
}) })
class DeviceEditView(PermissionRequiredMixin, ObjectEditView): class DeviceConfigView(PermissionRequiredMixin, View):
permission_required = 'dcim.change_device' permission_required = 'dcim.napalm_read'
def get(self, request, pk):
device = get_object_or_404(Device, pk=pk)
return render(request, 'dcim/device_config.html', {
'device': device,
})
class DeviceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'dcim.add_device'
model = Device model = Device
form_class = forms.DeviceForm form_class = forms.DeviceForm
template_name = 'dcim/device_edit.html' template_name = 'dcim/device_edit.html'
default_return_url = 'dcim:device_list' default_return_url = 'dcim:device_list'
class DeviceEditView(DeviceCreateView):
permission_required = 'dcim.change_device'
class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView): class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'dcim.delete_device' permission_required = 'dcim.delete_device'
model = Device model = Device
@@ -887,16 +1008,19 @@ class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView): class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_device' permission_required = 'dcim.change_device'
cls = Device cls = Device
queryset = Device.objects.select_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
filter = filters.DeviceFilter filter = filters.DeviceFilter
table = tables.DeviceTable
form = forms.DeviceBulkEditForm form = forms.DeviceBulkEditForm
template_name = 'dcim/device_bulk_edit.html'
default_return_url = 'dcim:device_list' default_return_url = 'dcim:device_list'
class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_device' permission_required = 'dcim.delete_device'
cls = Device cls = Device
queryset = Device.objects.select_related('tenant', 'site', 'rack', 'device_role', 'device_type__manufacturer')
filter = filters.DeviceFilter filter = filters.DeviceFilter
table = tables.DeviceTable
default_return_url = 'dcim:device_list' default_return_url = 'dcim:device_list'
@@ -904,7 +1028,7 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Console ports # Console ports
# #
class ConsolePortAddView(PermissionRequiredMixin, ComponentCreateView): class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleport' permission_required = 'dcim.add_consoleport'
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'
@@ -1004,6 +1128,7 @@ class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleport' permission_required = 'dcim.delete_consoleport'
cls = ConsolePort cls = ConsolePort
parent_cls = Device parent_cls = Device
table = tables.ConsolePortTable
class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView): class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
@@ -1017,7 +1142,7 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
# Console server ports # Console server ports
# #
class ConsoleServerPortAddView(PermissionRequiredMixin, ComponentCreateView): class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_consoleserverport' permission_required = 'dcim.add_consoleserverport'
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'
@@ -1116,17 +1241,27 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
model = ConsoleServerPort 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): class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_consoleserverport' permission_required = 'dcim.delete_consoleserverport'
cls = ConsoleServerPort cls = ConsoleServerPort
parent_cls = Device parent_cls = Device
table = tables.ConsoleServerPortTable
# #
# Power ports # Power ports
# #
class PowerPortAddView(PermissionRequiredMixin, ComponentCreateView): class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_powerport' permission_required = 'dcim.add_powerport'
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'
@@ -1226,6 +1361,7 @@ class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_powerport' permission_required = 'dcim.delete_powerport'
cls = PowerPort cls = PowerPort
parent_cls = Device parent_cls = Device
table = tables.PowerPortTable
class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView): class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
@@ -1239,7 +1375,7 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
# Power outlets # Power outlets
# #
class PowerOutletAddView(PermissionRequiredMixin, ComponentCreateView): class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_poweroutlet' permission_required = 'dcim.add_poweroutlet'
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'
@@ -1338,17 +1474,29 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView):
model = PowerOutlet 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): class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_poweroutlet' permission_required = 'dcim.delete_poweroutlet'
cls = PowerOutlet cls = PowerOutlet
parent_cls = Device parent_cls = Device
table = tables.PowerOutletTable
# #
# Interfaces # Interfaces
# #
class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView): class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_interface' permission_required = 'dcim.add_interface'
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'
@@ -1368,25 +1516,38 @@ class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
model = Interface 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): class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'dcim.change_interface' permission_required = 'dcim.change_interface'
cls = Interface cls = Interface
parent_cls = Device parent_cls = Device
table = tables.InterfaceTable
form = forms.InterfaceBulkEditForm form = forms.InterfaceBulkEditForm
template_name = 'dcim/interface_bulk_edit.html'
class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_interface' permission_required = 'dcim.delete_interface'
cls = Interface cls = Interface
parent_cls = Device parent_cls = Device
table = tables.InterfaceTable
# #
# Device bays # Device bays
# #
class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView): class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView):
permission_required = 'dcim.add_devicebay' permission_required = 'dcim.add_devicebay'
parent_model = Device parent_model = Device
parent_field = 'device' parent_field = 'device'
@@ -1460,6 +1621,7 @@ class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'dcim.delete_devicebay' permission_required = 'dcim.delete_devicebay'
cls = DeviceBay cls = DeviceBay
parent_cls = Device parent_cls = Device
table = tables.DeviceBayTable
# #

View File

@@ -49,6 +49,10 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
# Validate selected choice # Validate selected choice
if cf.type == CF_TYPE_SELECT: 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()] valid_choices = [c.pk for c in cf.choices.all()]
if value not in valid_choices: if value not in valid_choices:
raise ValidationError("Invalid choice for field {}: {}".format(field_name, value)) raise ValidationError("Invalid choice for field {}: {}".format(field_name, value))
@@ -107,6 +111,16 @@ class CustomFieldModelSerializer(serializers.ModelSerializer):
defaults={'serialized_value': custom_field.serialize_value(value)}, defaults={'serialized_value': custom_field.serialize_value(value)},
) )
def validate(self, data):
"""
Enforce model validation (see utilities.api.ModelValidationMixin)
"""
model_data = data.copy()
model_data.pop('custom_fields', None)
instance = self.Meta.model(**model_data)
instance.clean()
return data
def create(self, validated_data): def create(self, validated_data):
custom_fields = validated_data.pop('custom_fields', None) custom_fields = validated_data.pop('custom_fields', None)

View File

@@ -10,7 +10,7 @@ from extras.models import (
ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction, ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, TopologyMap, UserAction,
) )
from users.api.serializers import NestedUserSerializer from users.api.serializers import NestedUserSerializer
from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ModelValidationMixin
# #
@@ -104,7 +104,7 @@ class ImageAttachmentSerializer(serializers.ModelSerializer):
return serializer(obj.parent, context={'request': self.context['request']}).data return serializer(obj.parent, context={'request': self.context['request']}).data
class WritableImageAttachmentSerializer(serializers.ModelSerializer): class WritableImageAttachmentSerializer(ModelValidationMixin, serializers.ModelSerializer):
content_type = ContentTypeFieldSerializer() content_type = ContentTypeFieldSerializer()
class Meta: class Meta:
@@ -121,6 +121,9 @@ class WritableImageAttachmentSerializer(serializers.ModelSerializer):
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id']) "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
) )
# Enforce model validation
super(WritableImageAttachmentSerializer, self).validate(data)
return data return data

View File

@@ -0,0 +1,62 @@
from __future__ import unicode_literals
# Models which support custom fields
CUSTOMFIELD_MODELS = (
'site', 'rack', 'devicetype', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
)
# Custom field types
CF_TYPE_TEXT = 100
CF_TYPE_INTEGER = 200
CF_TYPE_BOOLEAN = 300
CF_TYPE_DATE = 400
CF_TYPE_URL = 500
CF_TYPE_SELECT = 600
CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_TEXT, 'Text'),
(CF_TYPE_INTEGER, 'Integer'),
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
(CF_TYPE_DATE, 'Date'),
(CF_TYPE_URL, 'URL'),
(CF_TYPE_SELECT, 'Selection'),
)
# Graph types
GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_PROVIDER = 200
GRAPH_TYPE_SITE = 300
GRAPH_TYPE_CHOICES = (
(GRAPH_TYPE_INTERFACE, 'Interface'),
(GRAPH_TYPE_PROVIDER, 'Provider'),
(GRAPH_TYPE_SITE, 'Site'),
)
# Models which support export templates
EXPORTTEMPLATE_MODELS = [
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
]
# User action types
ACTION_CREATE = 1
ACTION_IMPORT = 2
ACTION_EDIT = 3
ACTION_BULK_EDIT = 4
ACTION_DELETE = 5
ACTION_BULK_DELETE = 6
ACTION_BULK_CREATE = 7
ACTION_CHOICES = (
(ACTION_CREATE, 'created'),
(ACTION_BULK_CREATE, 'bulk created'),
(ACTION_IMPORT, 'imported'),
(ACTION_EDIT, 'modified'),
(ACTION_BULK_EDIT, 'bulk edited'),
(ACTION_DELETE, 'deleted'),
(ACTION_BULK_DELETE, 'bulk deleted'),
)

View File

@@ -37,19 +37,29 @@ class Command(BaseCommand):
def get_namespace(self): def get_namespace(self):
namespace = {} namespace = {}
# Gather Django models from each app # Gather Django models and constants from each app
for app in APPS: for app in APPS:
self.django_models[app] = [] self.django_models[app] = []
# Models
app_models = sys.modules['{}.models'.format(app)] app_models = sys.modules['{}.models'.format(app)]
for name in dir(app_models): for name in dir(app_models):
model = getattr(app_models, name) model = getattr(app_models, name)
try: try:
if issubclass(model, Model): if issubclass(model, Model) and model._meta.app_label == app:
namespace[name] = model namespace[name] = model
self.django_models[app].append(name) self.django_models[app].append(name)
except TypeError: except TypeError:
pass pass
# Constants
try:
app_constants = sys.modules['{}.constants'.format(app)]
for name in dir(app_constants):
namespace[name] = getattr(app_constants, name)
except KeyError:
pass
# Load convenience commands # Load convenience commands
namespace.update({ namespace.update({
'lsmodels': self._lsmodels, 'lsmodels': self._lsmodels,

View File

@@ -13,8 +13,8 @@ from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
class Command(BaseCommand): class Command(BaseCommand):
help = "Update inventory information for specified devices" help = "Update inventory information for specified devices"
username = settings.NETBOX_USERNAME username = settings.NAPALM_USERNAME
password = settings.NETBOX_PASSWORD password = settings.NAPALM_PASSWORD
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('-u', '--username', dest='username', help="Specify the username to use") parser.add_argument('-u', '--username', dest='username', help="Specify the username to use")

View File

@@ -15,62 +15,7 @@ from django.utils.encoding import python_2_unicode_compatible
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from utilities.utils import foreground_color from utilities.utils import foreground_color
from .constants import *
CUSTOMFIELD_MODELS = (
'site', 'rack', 'devicetype', 'device', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
)
CF_TYPE_TEXT = 100
CF_TYPE_INTEGER = 200
CF_TYPE_BOOLEAN = 300
CF_TYPE_DATE = 400
CF_TYPE_URL = 500
CF_TYPE_SELECT = 600
CUSTOMFIELD_TYPE_CHOICES = (
(CF_TYPE_TEXT, 'Text'),
(CF_TYPE_INTEGER, 'Integer'),
(CF_TYPE_BOOLEAN, 'Boolean (true/false)'),
(CF_TYPE_DATE, 'Date'),
(CF_TYPE_URL, 'URL'),
(CF_TYPE_SELECT, 'Selection'),
)
GRAPH_TYPE_INTERFACE = 100
GRAPH_TYPE_PROVIDER = 200
GRAPH_TYPE_SITE = 300
GRAPH_TYPE_CHOICES = (
(GRAPH_TYPE_INTERFACE, 'Interface'),
(GRAPH_TYPE_PROVIDER, 'Provider'),
(GRAPH_TYPE_SITE, 'Site'),
)
EXPORTTEMPLATE_MODELS = [
'site', 'rack', 'device', 'consoleport', 'powerport', 'interfaceconnection', # DCIM
'aggregate', 'prefix', 'ipaddress', 'vlan', # IPAM
'provider', 'circuit', # Circuits
'tenant', # Tenants
]
ACTION_CREATE = 1
ACTION_IMPORT = 2
ACTION_EDIT = 3
ACTION_BULK_EDIT = 4
ACTION_DELETE = 5
ACTION_BULK_DELETE = 6
ACTION_BULK_CREATE = 7
ACTION_CHOICES = (
(ACTION_CREATE, 'created'),
(ACTION_BULK_CREATE, 'bulk created'),
(ACTION_IMPORT, 'imported'),
(ACTION_EDIT, 'modified'),
(ACTION_BULK_EDIT, 'bulk edited'),
(ACTION_DELETE, 'deleted'),
(ACTION_BULK_DELETE, 'bulk deleted'),
)
# #
@@ -371,7 +316,8 @@ class TopologyMap(models.Model):
# Add all circuits to the graph # Add all circuits to the graph
for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices): for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices):
peer_termination = termination.get_peer_termination() 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') graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue')
return graph.pipe(format=img_format) return graph.pipe(format=img_format)
@@ -386,7 +332,7 @@ def image_upload(instance, filename):
path = 'image-attachments/' path = 'image-attachments/'
# Rename the file to the provided name, if any. Attempt to preserve the file extension. # 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']: if instance.name and extension in ['bmp', 'gif', 'jpeg', 'jpg', 'png']:
filename = '.'.join([instance.name, extension]) filename = '.'.join([instance.name, extension])
elif instance.name: elif instance.name:

View File

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

View File

@@ -1,4 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
@@ -6,11 +7,11 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import ( from ipam.models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix,
Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
) )
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceFieldSerializer from utilities.api import ChoiceFieldSerializer, ModelValidationMixin
# #
@@ -22,7 +23,7 @@ class VRFSerializer(CustomFieldModelSerializer):
class Meta: class Meta:
model = VRF model = VRF
fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields'] fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'display_name', 'custom_fields']
class NestedVRFSerializer(serializers.ModelSerializer): class NestedVRFSerializer(serializers.ModelSerializer):
@@ -44,7 +45,7 @@ class WritableVRFSerializer(CustomFieldModelSerializer):
# Roles # Roles
# #
class RoleSerializer(serializers.ModelSerializer): class RoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Role model = Role
@@ -63,7 +64,7 @@ class NestedRoleSerializer(serializers.ModelSerializer):
# RIRs # RIRs
# #
class RIRSerializer(serializers.ModelSerializer): class RIRSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = RIR model = RIR
@@ -141,6 +142,9 @@ class WritableVLANGroupSerializer(serializers.ModelSerializer):
validator.set_context(self) validator.set_context(self)
validator(data) validator(data)
# Enforce model validation
super(WritableVLANGroupSerializer, self).validate(data)
return data return data
@@ -187,6 +191,9 @@ class WritableVLANSerializer(CustomFieldModelSerializer):
validator.set_context(self) validator.set_context(self)
validator(data) validator(data)
# Enforce model validation
super(WritableVLANSerializer, self).validate(data)
return data return data
@@ -236,12 +243,13 @@ class IPAddressSerializer(CustomFieldModelSerializer):
vrf = NestedVRFSerializer() vrf = NestedVRFSerializer()
tenant = NestedTenantSerializer() tenant = NestedTenantSerializer()
status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES) status = ChoiceFieldSerializer(choices=IPADDRESS_STATUS_CHOICES)
role = ChoiceFieldSerializer(choices=IPADDRESS_ROLE_CHOICES)
interface = InterfaceSerializer() interface = InterfaceSerializer()
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = [ fields = [
'id', 'family', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside', 'id', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
'nat_outside', 'custom_fields', 'nat_outside', 'custom_fields',
] ]
@@ -261,7 +269,24 @@ class WritableIPAddressSerializer(CustomFieldModelSerializer):
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = ['id', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside', 'custom_fields'] fields = [
'id', 'address', 'vrf', 'tenant', 'status', 'role', 'interface', 'description', 'nat_inside',
'custom_fields',
]
class AvailableIPSerializer(serializers.Serializer):
def to_representation(self, instance):
if self.context.get('vrf'):
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
else:
vrf = None
return OrderedDict([
('family', self.context['prefix'].version),
('address', '{}/{}'.format(instance, self.context['prefix'].prefixlen)),
('vrf', vrf),
])
# #
@@ -278,6 +303,7 @@ class ServiceSerializer(serializers.ModelSerializer):
fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description'] fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description']
# TODO: Figure out how to use ModelValidationMixin with ManyToManyFields. Calling clean() yields a ValueError.
class WritableServiceSerializer(serializers.ModelSerializer): class WritableServiceSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@@ -1,7 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from rest_framework import status
from rest_framework.decorators import detail_route
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from django.conf import settings
from django.shortcuts import get_object_or_404
from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
from ipam import filters from ipam import filters
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
@@ -20,15 +27,6 @@ class VRFViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
filter_class = filters.VRFFilter filter_class = filters.VRFFilter
#
# Roles
#
class RoleViewSet(ModelViewSet):
queryset = Role.objects.all()
serializer_class = serializers.RoleSerializer
# #
# RIRs # RIRs
# #
@@ -50,6 +48,16 @@ class AggregateViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
filter_class = filters.AggregateFilter filter_class = filters.AggregateFilter
#
# Roles
#
class RoleViewSet(ModelViewSet):
queryset = Role.objects.all()
serializer_class = serializers.RoleSerializer
filter_class = filters.RoleFilter
# #
# Prefixes # Prefixes
# #
@@ -60,6 +68,66 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
write_serializer_class = serializers.WritablePrefixSerializer write_serializer_class = serializers.WritablePrefixSerializer
filter_class = filters.PrefixFilter filter_class = filters.PrefixFilter
@detail_route(url_path='available-ips', methods=['get', 'post'])
def available_ips(self, request, pk=None):
"""
A convenience method for returning available IP addresses within a prefix. By default, the number of IPs
returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed,
however results will not be paginated.
"""
prefix = get_object_or_404(Prefix, pk=pk)
# Create the next available IP within the prefix
if request.method == 'POST':
# Permissions check
if not request.user.has_perm('ipam.add_ipaddress'):
raise PermissionDenied()
# Find the first available IP address in the prefix
try:
ipaddress = list(prefix.get_available_ips())[0]
except IndexError:
return Response(
{
"detail": "There are no available IPs within this prefix ({})".format(prefix)
},
status=status.HTTP_400_BAD_REQUEST
)
# Create the new IP address
data = request.data.copy()
data['address'] = '{}/{}'.format(ipaddress, prefix.prefix.prefixlen)
data['vrf'] = prefix.vrf
serializer = serializers.WritableIPAddressSerializer(data=data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# Determine the maximum amount of IPs to return
else:
try:
limit = int(request.query_params.get('limit', settings.PAGINATE_COUNT))
except ValueError:
limit = settings.PAGINATE_COUNT
if settings.MAX_PAGE_SIZE:
limit = min(limit, settings.MAX_PAGE_SIZE)
# Calculate available IPs within the prefix
ip_list = []
for index, ip in enumerate(prefix.get_available_ips(), start=1):
ip_list.append(ip)
if index == limit:
break
serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={
'request': request,
'prefix': prefix.prefix,
'vrf': prefix.vrf,
})
return Response(serializer.data)
# #
# IP addresses # IP addresses

78
netbox/ipam/constants.py Normal file
View File

@@ -0,0 +1,78 @@
from __future__ import unicode_literals
# IP address families
AF_CHOICES = (
(4, 'IPv4'),
(6, 'IPv6'),
)
# Prefix statuses
PREFIX_STATUS_CONTAINER = 0
PREFIX_STATUS_ACTIVE = 1
PREFIX_STATUS_RESERVED = 2
PREFIX_STATUS_DEPRECATED = 3
PREFIX_STATUS_CHOICES = (
(PREFIX_STATUS_CONTAINER, 'Container'),
(PREFIX_STATUS_ACTIVE, 'Active'),
(PREFIX_STATUS_RESERVED, 'Reserved'),
(PREFIX_STATUS_DEPRECATED, 'Deprecated')
)
# IP address statuses
IPADDRESS_STATUS_ACTIVE = 1
IPADDRESS_STATUS_RESERVED = 2
IPADDRESS_STATUS_DEPRECATED = 3
IPADDRESS_STATUS_DHCP = 5
IPADDRESS_STATUS_CHOICES = (
(IPADDRESS_STATUS_ACTIVE, 'Active'),
(IPADDRESS_STATUS_RESERVED, 'Reserved'),
(IPADDRESS_STATUS_DEPRECATED, 'Deprecated'),
(IPADDRESS_STATUS_DHCP, 'DHCP')
)
# IP address roles
IPADDRESS_ROLE_LOOPBACK = 10
IPADDRESS_ROLE_SECONDARY = 20
IPADDRESS_ROLE_ANYCAST = 30
IPADDRESS_ROLE_VIP = 40
IPADDRESS_ROLE_VRRP = 41
IPADDRESS_ROLE_HSRP = 42
IPADDRESS_ROLE_GLBP = 43
IPADDRESS_ROLE_CHOICES = (
(IPADDRESS_ROLE_LOOPBACK, 'Loopback'),
(IPADDRESS_ROLE_SECONDARY, 'Secondary'),
(IPADDRESS_ROLE_ANYCAST, 'Anycast'),
(IPADDRESS_ROLE_VIP, 'VIP'),
(IPADDRESS_ROLE_VRRP, 'VRRP'),
(IPADDRESS_ROLE_HSRP, 'HSRP'),
(IPADDRESS_ROLE_GLBP, 'GLBP'),
)
# VLAN statuses
VLAN_STATUS_ACTIVE = 1
VLAN_STATUS_RESERVED = 2
VLAN_STATUS_DEPRECATED = 3
VLAN_STATUS_CHOICES = (
(VLAN_STATUS_ACTIVE, 'Active'),
(VLAN_STATUS_RESERVED, 'Reserved'),
(VLAN_STATUS_DEPRECATED, 'Deprecated')
)
# Bootstrap CSS classes for various statuses
STATUS_CHOICE_CLASSES = {
0: 'default',
1: 'primary',
2: 'info',
3: 'danger',
4: 'warning',
5: 'success',
}
# IP protocols (for services)
IP_PROTOCOL_TCP = 6
IP_PROTOCOL_UDP = 17
IP_PROTOCOL_CHOICES = (
(IP_PROTOCOL_TCP, 'TCP'),
(IP_PROTOCOL_UDP, 'UDP'),
)

View File

@@ -11,8 +11,8 @@ from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
from .models import ( from .models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
VLAN_STATUS_CHOICES, VLANGroup, VRF, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
) )
@@ -23,7 +23,6 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
@@ -45,7 +44,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = VRF model = VRF
fields = ['name', 'rd'] fields = ['name', 'rd', 'enforce_unique']
class RIRFilter(django_filters.FilterSet): class RIRFilter(django_filters.FilterSet):
@@ -53,7 +52,7 @@ class RIRFilter(django_filters.FilterSet):
class Meta: class Meta:
model = RIR model = RIR
fields = ['is_private'] fields = ['name', 'slug', 'is_private']
class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet): class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -63,7 +62,6 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
rir_id = django_filters.ModelMultipleChoiceFilter( rir_id = django_filters.ModelMultipleChoiceFilter(
name='rir',
queryset=RIR.objects.all(), queryset=RIR.objects.all(),
label='RIR (ID)', label='RIR (ID)',
) )
@@ -90,6 +88,13 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class RoleFilter(django_filters.FilterSet):
class Meta:
model = Role
fields = ['name', 'slug']
class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet): class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
@@ -105,7 +110,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Mask length', label='Mask length',
) )
vrf_id = NullableModelMultipleChoiceFilter( vrf_id = NullableModelMultipleChoiceFilter(
name='vrf_id',
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
label='VRF', label='VRF',
) )
@@ -116,7 +120,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VRF (RD)', label='VRF (RD)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
@@ -127,7 +130,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)', label='Tenant (slug)',
) )
site_id = NullableModelMultipleChoiceFilter( site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
@@ -138,7 +140,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)', label='Site (slug)',
) )
vlan_id = NullableModelMultipleChoiceFilter( vlan_id = NullableModelMultipleChoiceFilter(
name='vlan',
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
label='VLAN (ID)', label='VLAN (ID)',
) )
@@ -147,7 +148,6 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VLAN number (1-4095)', label='VLAN number (1-4095)',
) )
role_id = NullableModelMultipleChoiceFilter( role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=Role.objects.all(), queryset=Role.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
@@ -163,7 +163,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = Prefix model = Prefix
fields = ['family'] fields = ['family', 'is_pool']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -207,7 +207,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Mask length', label='Mask length',
) )
vrf_id = NullableModelMultipleChoiceFilter( vrf_id = NullableModelMultipleChoiceFilter(
name='vrf_id',
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
label='VRF', label='VRF',
) )
@@ -218,7 +217,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='VRF (RD)', label='VRF (RD)',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
@@ -240,13 +238,15 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Device (name)', label='Device (name)',
) )
interface_id = django_filters.ModelMultipleChoiceFilter( interface_id = django_filters.ModelMultipleChoiceFilter(
name='interface',
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
label='Interface (ID)', label='Interface (ID)',
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_STATUS_CHOICES choices=IPADDRESS_STATUS_CHOICES
) )
role = django_filters.MultipleChoiceFilter(
choices=IPADDRESS_ROLE_CHOICES
)
class Meta: class Meta:
model = IPAddress model = IPAddress
@@ -281,7 +281,6 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
class VLANGroupFilter(django_filters.FilterSet): class VLANGroupFilter(django_filters.FilterSet):
site_id = NullableModelMultipleChoiceFilter( site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
@@ -294,7 +293,7 @@ class VLANGroupFilter(django_filters.FilterSet):
class Meta: class Meta:
model = VLANGroup model = VLANGroup
fields = ['name'] fields = ['name', 'slug']
class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet): class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -304,7 +303,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Search', label='Search',
) )
site_id = NullableModelMultipleChoiceFilter( site_id = NullableModelMultipleChoiceFilter(
name='site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label='Site (ID)', label='Site (ID)',
) )
@@ -315,7 +313,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Site (slug)', label='Site (slug)',
) )
group_id = NullableModelMultipleChoiceFilter( group_id = NullableModelMultipleChoiceFilter(
name='group',
queryset=VLANGroup.objects.all(), queryset=VLANGroup.objects.all(),
label='Group (ID)', label='Group (ID)',
) )
@@ -326,7 +323,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Group', label='Group',
) )
tenant_id = NullableModelMultipleChoiceFilter( tenant_id = NullableModelMultipleChoiceFilter(
name='tenant',
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
label='Tenant (ID)', label='Tenant (ID)',
) )
@@ -337,7 +333,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
label='Tenant (slug)', label='Tenant (slug)',
) )
role_id = NullableModelMultipleChoiceFilter( role_id = NullableModelMultipleChoiceFilter(
name='role',
queryset=Role.objects.all(), queryset=Role.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
@@ -353,7 +348,7 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
class Meta: class Meta:
model = VLAN model = VLAN
fields = ['name', 'vid'] fields = ['vid', 'name']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@@ -368,7 +363,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
class ServiceFilter(django_filters.FilterSet): class ServiceFilter(django_filters.FilterSet):
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device (ID)', label='Device (ID)',
) )

View File

@@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django import forms from django import forms
from django.core.exceptions import MultipleObjectsReturned
from django.db.models import Count from django.db.models import Count
from dcim.models import Site, Rack, Device, Interface from dcim.models import Site, Rack, Device, Interface
@@ -13,8 +14,8 @@ from utilities.forms import (
add_blank_choice, add_blank_choice,
) )
from .models import ( from .models import (
Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
VLANGroup, VLAN_STATUS_CHOICES, VRF, Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
) )
@@ -216,7 +217,7 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
# Initialize helper selectors # Initialize helper selectors
instance = kwargs.get('instance') instance = kwargs.get('instance')
initial = kwargs.get('initial', {}) initial = kwargs.get('initial', {}).copy()
if instance and instance.vlan is not None: if instance and instance.vlan is not None:
initial['vlan_group'] = instance.vlan.group initial['vlan_group'] = instance.vlan.group
kwargs['initial'] = initial kwargs['initial'] = initial
@@ -263,7 +264,7 @@ class PrefixCSVForm(forms.ModelForm):
required=False required=False
) )
status = CSVChoiceField( status = CSVChoiceField(
choices=IPADDRESS_STATUS_CHOICES, choices=PREFIX_STATUS_CHOICES,
help_text='Operational status' help_text='Operational status'
) )
role = forms.ModelChoiceField( role = forms.ModelChoiceField(
@@ -301,6 +302,10 @@ class PrefixCSVForm(forms.ModelForm):
)) ))
else: else:
raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group)) 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: elif vlan_vid:
try: try:
self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid) self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid)
@@ -309,6 +314,8 @@ class PrefixCSVForm(forms.ModelForm):
raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site)) raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site))
else: else:
raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid)) 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): class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -477,7 +484,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = [ fields = [
'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack', 'address', 'vrf', 'status', 'role', 'description', 'interface', 'primary_for_device', 'nat_site', 'nat_rack',
'nat_inside', 'tenant_group', 'tenant', 'nat_inside', 'tenant_group', 'tenant',
] ]
@@ -485,12 +492,12 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm)
# Initialize helper selectors # Initialize helper selectors
instance = kwargs.get('instance') instance = kwargs.get('instance')
initial = kwargs.get('initial', {}) initial = kwargs.get('initial', {}).copy()
if instance and instance.interface is not None: if instance and instance.interface is not None:
initial['interface_site'] = instance.interface.device.site initial['interface_site'] = instance.interface.device.site
initial['interface_rack'] = instance.interface.device.rack initial['interface_rack'] = instance.interface.device.rack
initial['interface_device'] = instance.interface.device 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_site'] = instance.nat_inside.device.site
initial['nat_rack'] = instance.nat_inside.device.rack initial['nat_rack'] = instance.nat_inside.device.rack
initial['nat_device'] = instance.nat_inside.device initial['nat_device'] = instance.nat_inside.device
@@ -555,7 +562,7 @@ class IPAddressBulkAddForm(BootstrapMixin, TenancyForm, CustomFieldForm):
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = ['address', 'status', 'vrf', 'description', 'tenant_group', 'tenant'] fields = ['address', 'vrf', 'status', 'role', 'description', 'tenant_group', 'tenant']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(IPAddressBulkAddForm, self).__init__(*args, **kwargs) super(IPAddressBulkAddForm, self).__init__(*args, **kwargs)
@@ -582,9 +589,14 @@ class IPAddressCSVForm(forms.ModelForm):
} }
) )
status = CSVChoiceField( status = CSVChoiceField(
choices=PREFIX_STATUS_CHOICES, choices=IPADDRESS_STATUS_CHOICES,
help_text='Operational status' help_text='Operational status'
) )
role = CSVChoiceField(
choices=IPADDRESS_ROLE_CHOICES,
required=False,
help_text='Functional role'
)
device = FlexibleModelChoiceField( device = FlexibleModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
@@ -605,7 +617,7 @@ class IPAddressCSVForm(forms.ModelForm):
class Meta: class Meta:
model = IPAddress model = IPAddress
fields = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description'] fields = ['address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description']
def clean(self): def clean(self):
@@ -634,16 +646,23 @@ class IPAddressCSVForm(forms.ModelForm):
# Set interface # Set interface
if self.cleaned_data['device'] and self.cleaned_data['interface_name']: if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
self.instance.interface = Interface.objects.get(device=self.cleaned_data['device'], self.instance.interface = Interface.objects.get(
name=self.cleaned_data['interface_name']) device=self.cleaned_data['device'],
name=self.cleaned_data['interface_name']
)
ipaddress = super(IPAddressCSVForm, self).save(*args, **kwargs)
# Set as primary for device # Set as primary for device
if self.cleaned_data['is_primary']: if self.cleaned_data['is_primary']:
device = self.cleaned_data['device']
if self.instance.address.version == 4: 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: 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): class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
@@ -651,10 +670,11 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF') vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False) tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
status = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), required=False) status = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), required=False)
role = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_ROLE_CHOICES), required=False)
description = forms.CharField(max_length=100, required=False) description = forms.CharField(max_length=100, required=False)
class Meta: class Meta:
nullable_fields = ['vrf', 'tenant', 'description'] nullable_fields = ['vrf', 'role', 'tenant', 'description']
def ipaddress_status_choices(): def ipaddress_status_choices():
@@ -664,6 +684,13 @@ def ipaddress_status_choices():
return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES] return [(s[0], '{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
def ipaddress_role_choices():
role_counts = {}
for role in IPAddress.objects.values('role').annotate(count=Count('role')).order_by('role'):
role_counts[role['role']] = role['count']
return [(r[0], '{} ({})'.format(r[1], role_counts.get(r[0], 0))) for r in IPADDRESS_ROLE_CHOICES]
class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm): class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = IPAddress model = IPAddress
q = forms.CharField(required=False, label='Search') q = forms.CharField(required=False, label='Search')
@@ -684,6 +711,7 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
null_option=(0, 'None') null_option=(0, 'None')
) )
status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False) status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
role = forms.MultipleChoiceField(choices=ipaddress_role_choices, required=False)
# #

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-16 19:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0016_unicode_literals'),
]
operations = [
migrations.AddField(
model_name='ipaddress',
name='role',
field=models.PositiveSmallIntegerField(blank=True, choices=[(10, 'Loopback'), (20, 'Secondary'), (30, 'Anycast'), (40, 'VIP'), (41, 'VRRP'), (42, 'HSRP'), (43, 'GLBP')], help_text='The functional role of this IP', null=True, verbose_name='Role'),
),
migrations.AlterField(
model_name='ipaddress',
name='status',
field=models.PositiveSmallIntegerField(choices=[(1, 'Active'), (2, 'Reserved'), (3, 'Deprecated'), (5, 'DHCP')], default=1, help_text='The operational status of this IP', verbose_name='Status'),
),
]

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-08-03 19:37
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ipam', '0017_ipaddress_roles'),
]
operations = [
migrations.AlterUniqueTogether(
name='service',
unique_together=set([]),
),
]

View File

@@ -1,6 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import netaddr
from netaddr import IPNetwork, cidr_merge
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
@@ -17,63 +16,10 @@ from tenancy.models import Tenant
from utilities.models import CreatedUpdatedModel from utilities.models import CreatedUpdatedModel
from utilities.sql import NullsFirstQuerySet from utilities.sql import NullsFirstQuerySet
from utilities.utils import csv_format from utilities.utils import csv_format
from .constants import *
from .fields import IPNetworkField, IPAddressField from .fields import IPNetworkField, IPAddressField
AF_CHOICES = (
(4, 'IPv4'),
(6, 'IPv6'),
)
PREFIX_STATUS_CONTAINER = 0
PREFIX_STATUS_ACTIVE = 1
PREFIX_STATUS_RESERVED = 2
PREFIX_STATUS_DEPRECATED = 3
PREFIX_STATUS_CHOICES = (
(PREFIX_STATUS_CONTAINER, 'Container'),
(PREFIX_STATUS_ACTIVE, 'Active'),
(PREFIX_STATUS_RESERVED, 'Reserved'),
(PREFIX_STATUS_DEPRECATED, 'Deprecated')
)
IPADDRESS_STATUS_ACTIVE = 1
IPADDRESS_STATUS_RESERVED = 2
IPADDRESS_STATUS_DEPRECATED = 3
IPADDRESS_STATUS_DHCP = 5
IPADDRESS_STATUS_CHOICES = (
(IPADDRESS_STATUS_ACTIVE, 'Active'),
(IPADDRESS_STATUS_RESERVED, 'Reserved'),
(IPADDRESS_STATUS_DEPRECATED, 'Deprecated'),
(IPADDRESS_STATUS_DHCP, 'DHCP')
)
VLAN_STATUS_ACTIVE = 1
VLAN_STATUS_RESERVED = 2
VLAN_STATUS_DEPRECATED = 3
VLAN_STATUS_CHOICES = (
(VLAN_STATUS_ACTIVE, 'Active'),
(VLAN_STATUS_RESERVED, 'Reserved'),
(VLAN_STATUS_DEPRECATED, 'Deprecated')
)
STATUS_CHOICE_CLASSES = {
0: 'default',
1: 'primary',
2: 'info',
3: 'danger',
4: 'warning',
5: 'success',
}
IP_PROTOCOL_TCP = 6
IP_PROTOCOL_UDP = 17
IP_PROTOCOL_CHOICES = (
(IP_PROTOCOL_TCP, 'TCP'),
(IP_PROTOCOL_UDP, 'UDP'),
)
@python_2_unicode_compatible @python_2_unicode_compatible
class VRF(CreatedUpdatedModel, CustomFieldModel): class VRF(CreatedUpdatedModel, CustomFieldModel):
""" """
@@ -97,7 +43,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
verbose_name_plural = 'VRFs' verbose_name_plural = 'VRFs'
def __str__(self): def __str__(self):
return self.name return self.display_name or super(VRF, self).__str__()
def get_absolute_url(self): def get_absolute_url(self):
return reverse('ipam:vrf', args=[self.pk]) return reverse('ipam:vrf', args=[self.pk])
@@ -111,6 +57,12 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
self.description, self.description,
]) ])
@property
def display_name(self):
if self.name and self.rd:
return "{} ({})".format(self.name, self.rd)
return None
@python_2_unicode_compatible @python_2_unicode_compatible
class RIR(models.Model): class RIR(models.Model):
@@ -206,13 +158,9 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
""" """
Determine the prefix utilization of the aggregate 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)) queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
# Remove overlapping prefixes from list of children child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
networks = cidr_merge([c.prefix for c in child_prefixes]) return int(float(child_prefixes.size) / self.prefix.size * 100)
children_size = float(0)
for p in networks:
children_size += p.size
return int(children_size / self.prefix.size * 100)
@python_2_unicode_compatible @python_2_unicode_compatible
@@ -368,11 +316,42 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
def get_duplicates(self): def get_duplicates(self):
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk) return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
def get_child_ips(self):
"""
Return all IPAddresses within this Prefix.
"""
return IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf)
def get_available_ips(self):
"""
Return all available IPs within this prefix as an IPSet.
"""
prefix = netaddr.IPSet(self.prefix)
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
available_ips = prefix - child_ips
# Remove unusable IPs from non-pool prefixes
if not self.is_pool:
available_ips -= netaddr.IPSet([
netaddr.IPAddress(self.prefix.first),
netaddr.IPAddress(self.prefix.last),
])
return available_ips
def get_utilization(self): def get_utilization(self):
""" """
Determine the utilization of the prefix and return it as a percentage. 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.
""" """
child_count = IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf).count() if self.status == PREFIX_STATUS_CONTAINER:
queryset = Prefix.objects.filter(prefix__net_contained=str(self.prefix), vrf=self.vrf)
child_prefixes = netaddr.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 prefix_size = self.prefix.size
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool: if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
prefix_size -= 2 prefix_size -= 2
@@ -382,11 +361,11 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
def new_subnet(self): def new_subnet(self):
if self.family == 4: if self.family == 4:
if self.prefix.prefixlen <= 30: if self.prefix.prefixlen <= 30:
return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return None return None
if self.family == 6: if self.family == 6:
if self.prefix.prefixlen <= 126: if self.prefix.prefixlen <= 126:
return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) return netaddr.IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return None return None
@@ -421,7 +400,13 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True, vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
verbose_name='VRF') verbose_name='VRF')
tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT) tenant = models.ForeignKey(Tenant, related_name='ip_addresses', blank=True, null=True, on_delete=models.PROTECT)
status = models.PositiveSmallIntegerField('Status', choices=IPADDRESS_STATUS_CHOICES, default=1) status = models.PositiveSmallIntegerField(
'Status', choices=IPADDRESS_STATUS_CHOICES, default=IPADDRESS_STATUS_ACTIVE,
help_text='The operational status of this IP'
)
role = models.PositiveSmallIntegerField(
'Role', choices=IPADDRESS_ROLE_CHOICES, blank=True, null=True, help_text='The functional role of this IP'
)
interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True, interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
null=True) null=True)
nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True, nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
@@ -432,7 +417,9 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
objects = IPAddressManager() objects = IPAddressManager()
csv_headers = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description'] csv_headers = [
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'interface_name', 'is_primary', 'description',
]
class Meta: class Meta:
ordering = ['family', 'address'] ordering = ['family', 'address']
@@ -484,6 +471,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
self.vrf.rd if self.vrf else None, self.vrf.rd if self.vrf else None,
self.tenant.name if self.tenant else None, self.tenant.name if self.tenant else None,
self.get_status_display(), self.get_status_display(),
self.get_role_display(),
self.device.identifier if self.device else None, self.device.identifier if self.device else None,
self.interface.name if self.interface else None, self.interface.name if self.interface else None,
is_primary, is_primary,
@@ -612,7 +600,6 @@ class Service(CreatedUpdatedModel):
class Meta: class Meta:
ordering = ['device', 'protocol', 'port'] ordering = ['device', 'protocol', 'port']
unique_together = ['device', 'protocol', 'port']
def __str__(self): def __str__(self):
return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display()) return '{} ({}/{})'.format(self.name, self.port, self.get_protocol_display())

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor from django_tables2.utils import Accessor
from utilities.tables import BaseTable, SearchTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
@@ -152,16 +152,6 @@ class VRFTable(BaseTable):
fields = ('pk', 'name', 'rd', 'tenant', 'description') fields = ('pk', 'name', 'rd', 'tenant', 'description')
class VRFSearchTable(SearchTable):
name = tables.LinkColumn()
rd = tables.Column(verbose_name='RD')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
class Meta(SearchTable.Meta):
model = VRF
fields = ('name', 'rd', 'tenant', 'description')
# #
# RIRs # RIRs
# #
@@ -171,6 +161,14 @@ class RIRTable(BaseTable):
name = tables.LinkColumn(verbose_name='Name') name = tables.LinkColumn(verbose_name='Name')
is_private = tables.BooleanColumn(verbose_name='Private') is_private = tables.BooleanColumn(verbose_name='Private')
aggregate_count = tables.Column(verbose_name='Aggregates') aggregate_count = tables.Column(verbose_name='Aggregates')
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta):
model = RIR
fields = ('pk', 'name', 'is_private', 'aggregate_count', 'actions')
class RIRDetailTable(RIRTable):
stats_total = tables.Column(accessor='stats.total', verbose_name='Total', stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
footer=lambda table: sum(r.stats['total'] for r in table.data)) footer=lambda table: sum(r.stats['total'] for r in table.data))
stats_active = tables.Column(accessor='stats.active', verbose_name='Active', stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
@@ -182,12 +180,12 @@ class RIRTable(BaseTable):
stats_available = tables.Column(accessor='stats.available', verbose_name='Available', stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
footer=lambda table: sum(r.stats['available'] for r in table.data)) footer=lambda table: sum(r.stats['available'] for r in table.data))
utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization') utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization')
actions = tables.TemplateColumn(template_code=RIR_ACTIONS, attrs={'td': {'class': 'text-right'}}, verbose_name='')
class Meta(BaseTable.Meta): class Meta(RIRTable.Meta):
model = RIR fields = (
fields = ('pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved', 'pk', 'name', 'is_private', 'aggregate_count', 'stats_total', 'stats_active', 'stats_reserved',
'stats_deprecated', 'stats_available', 'utilization', 'actions') 'stats_deprecated', 'stats_available', 'utilization', 'actions',
)
# #
@@ -197,24 +195,21 @@ class RIRTable(BaseTable):
class AggregateTable(BaseTable): class AggregateTable(BaseTable):
pk = ToggleColumn() pk = ToggleColumn()
prefix = tables.LinkColumn(verbose_name='Aggregate') prefix = tables.LinkColumn(verbose_name='Aggregate')
child_count = tables.Column(verbose_name='Prefixes')
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added') date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Aggregate model = Aggregate
fields = ('pk', 'prefix', 'rir', 'date_added', 'description')
class AggregateDetailTable(AggregateTable):
child_count = tables.Column(verbose_name='Prefixes')
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
class Meta(AggregateTable.Meta):
fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description') fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description')
class AggregateSearchTable(SearchTable):
prefix = tables.LinkColumn(verbose_name='Aggregate')
date_added = tables.DateColumn(format="Y-m-d", verbose_name='Added')
class Meta(SearchTable.Meta):
model = Aggregate
fields = ('prefix', 'rir', 'date_added', 'description')
# #
# Roles # Roles
# #
@@ -241,7 +236,6 @@ class PrefixTable(BaseTable):
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}}) prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
status = tables.TemplateColumn(STATUS_LABEL) status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='IP Usage')
tenant = tables.TemplateColumn(TENANT_LINK) tenant = tables.TemplateColumn(TENANT_LINK)
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
@@ -249,37 +243,17 @@ class PrefixTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Prefix model = Prefix
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description') fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
row_attrs = { row_attrs = {
'class': lambda record: 'success' if not record.pk else '', 'class': lambda record: 'success' if not record.pk else '',
} }
class PrefixBriefTable(BaseTable): class PrefixDetailTable(PrefixTable):
prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF) get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
status = tables.TemplateColumn(STATUS_LABEL)
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')])
class Meta(BaseTable.Meta): class Meta(PrefixTable.Meta):
model = Prefix fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
fields = ('prefix', 'vrf', 'status', 'site', 'vlan', 'role')
orderable = False
class PrefixSearchTable(SearchTable):
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
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')
role = tables.TemplateColumn(PREFIX_ROLE_LINK)
class Meta(SearchTable.Meta):
model = Prefix
fields = ('prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description')
# #
@@ -292,43 +266,26 @@ class IPAddressTable(BaseTable):
status = tables.TemplateColumn(STATUS_LABEL) status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK) tenant = tables.TemplateColumn(TENANT_LINK)
nat_inside = tables.LinkColumn(
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
)
device = tables.TemplateColumn(IPADDRESS_DEVICE, orderable=False) device = tables.TemplateColumn(IPADDRESS_DEVICE, orderable=False)
interface = tables.Column(orderable=False)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = IPAddress model = IPAddress
fields = ('pk', 'address', 'status', 'vrf', 'tenant', 'nat_inside', 'device', 'description') fields = ('pk', 'address', 'vrf', 'status', 'role', 'tenant', 'device', 'interface', 'description')
row_attrs = { row_attrs = {
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '', 'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
} }
class IPAddressBriefTable(BaseTable): class IPAddressDetailTable(IPAddressTable):
address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address')
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False)
interface = tables.Column(orderable=False)
nat_inside = tables.LinkColumn( nat_inside = tables.LinkColumn(
'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)' 'ipam:ipaddress', args=[Accessor('nat_inside.pk')], orderable=False, verbose_name='NAT (Inside)'
) )
class Meta(BaseTable.Meta): class Meta(IPAddressTable.Meta):
model = IPAddress fields = (
fields = ('address', 'device', 'interface', 'nat_inside') 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'device', 'interface', 'description',
)
class IPAddressSearchTable(SearchTable):
address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
tenant = tables.TemplateColumn(TENANT_LINK)
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False)
interface = tables.Column(orderable=False)
class Meta(SearchTable.Meta):
model = IPAddress
fields = ('address', 'status', 'vrf', 'tenant', 'device', 'interface', 'description')
# #
@@ -358,24 +315,17 @@ class VLANTable(BaseTable):
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID') vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')]) tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
status = tables.TemplateColumn(STATUS_LABEL) status = tables.TemplateColumn(STATUS_LABEL)
role = tables.TemplateColumn(VLAN_ROLE_LINK) role = tables.TemplateColumn(VLAN_ROLE_LINK)
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = VLAN model = VLAN
fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')
class VLANDetailTable(VLANTable):
prefixes = tables.TemplateColumn(VLAN_PREFIXES, orderable=False, verbose_name='Prefixes')
class Meta(VLANTable.Meta):
fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description') fields = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
class VLANSearchTable(SearchTable):
vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID')
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group')
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')])
status = tables.TemplateColumn(STATUS_LABEL)
role = tables.TemplateColumn(VLAN_ROLE_LINK)
class Meta(SearchTable.Meta):
model = VLAN
fields = ('vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description')

View File

@@ -367,6 +367,35 @@ class PrefixTest(HttpStatusMixin, APITestCase):
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
self.assertEqual(Prefix.objects.count(), 2) self.assertEqual(Prefix.objects.count(), 2)
def test_available_ips(self):
prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/29'), is_pool=True)
url = reverse('ipam-api:prefix-available-ips', kwargs={'pk': prefix.pk})
# Retrieve all available IPs
response = self.client.get(url, **self.header)
self.assertEqual(len(response.data), 8) # 8 because prefix.is_pool = True
# Change the prefix to not be a pool and try again
prefix.is_pool = False
prefix.save()
response = self.client.get(url, **self.header)
self.assertEqual(len(response.data), 6) # 8 - 2 because prefix.is_pool = False
# Create all six available IPs
for i in range(6):
data = {
'description': 'Test IP {}'.format(i)
}
response = self.client.post(url, data, **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(response.data['description'], data['description'])
# Try to create one more IP
response = self.client.post(url, {}, **self.header)
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
self.assertIn('detail', response.data)
class IPAddressTest(HttpStatusMixin, APITestCase): class IPAddressTest(HttpStatusMixin, APITestCase):

View File

@@ -10,7 +10,7 @@ urlpatterns = [
# VRFs # VRFs
url(r'^vrfs/$', views.VRFListView.as_view(), name='vrf_list'), 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/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'),
url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'), url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'),
url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'), url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'),
@@ -20,13 +20,13 @@ urlpatterns = [
# RIRs # RIRs
url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'), 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/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'),
url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'), url(r'^rirs/(?P<slug>[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'),
# Aggregates # Aggregates
url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'), 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/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'),
url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
@@ -36,13 +36,13 @@ urlpatterns = [
# Roles # Roles
url(r'^roles/$', views.RoleListView.as_view(), name='role_list'), 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/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'),
url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'), url(r'^roles/(?P<slug>[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'),
# Prefixes # Prefixes
url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'), 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/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'),
url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'), url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'),
url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'), url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'),
@@ -53,8 +53,8 @@ urlpatterns = [
# IP addresses # IP addresses
url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'), 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/add/$', views.IPAddressCreateView.as_view(), name='ipaddress_add'),
url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkAddView.as_view(), name='ipaddress_bulk_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/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/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
@@ -64,13 +64,13 @@ urlpatterns = [
# VLAN groups # VLAN groups
url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'), 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/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'),
url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), url(r'^vlan-groups/(?P<pk>\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'),
# VLANs # VLANs
url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'), 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/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'),
url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'), url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'), 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 dcim.models import Device
from utilities.paginator import EnhancedPaginator from utilities.paginator import EnhancedPaginator
from utilities.views import ( from utilities.views import (
BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
) )
from . import filters, forms, tables from . import filters, forms, tables
from .models import ( from .models import (
@@ -103,8 +103,8 @@ class VRFView(View):
def get(self, request, pk): def get(self, request, pk):
vrf = get_object_or_404(VRF.objects.all(), pk=pk) vrf = get_object_or_404(VRF.objects.all(), pk=pk)
prefix_table = tables.PrefixBriefTable( prefix_table = tables.PrefixTable(
list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')) list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role')), orderable=False
) )
prefix_table.exclude = ('vrf',) prefix_table.exclude = ('vrf',)
@@ -114,14 +114,18 @@ class VRFView(View):
}) })
class VRFEditView(PermissionRequiredMixin, ObjectEditView): class VRFCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vrf' permission_required = 'ipam.add_vrf'
model = VRF model = VRF
form_class = forms.VRFForm form_class = forms.VRFForm
template_name = 'ipam/vrf_edit.html' template_name = 'ipam/vrf_edit.html'
default_return_url = 'ipam:vrf_list' default_return_url = 'ipam:vrf_list'
class VRFEditView(VRFCreateView):
permission_required = 'ipam.change_vrf'
class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView): class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_vrf' permission_required = 'ipam.delete_vrf'
model = VRF model = VRF
@@ -138,16 +142,19 @@ class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
class VRFBulkEditView(PermissionRequiredMixin, BulkEditView): class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_vrf' permission_required = 'ipam.change_vrf'
cls = VRF cls = VRF
queryset = VRF.objects.select_related('tenant')
filter = filters.VRFFilter filter = filters.VRFFilter
table = tables.VRFTable
form = forms.VRFBulkEditForm form = forms.VRFBulkEditForm
template_name = 'ipam/vrf_bulk_edit.html'
default_return_url = 'ipam:vrf_list' default_return_url = 'ipam:vrf_list'
class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vrf' permission_required = 'ipam.delete_vrf'
cls = VRF cls = VRF
queryset = VRF.objects.select_related('tenant')
filter = filters.VRFFilter filter = filters.VRFFilter
table = tables.VRFTable
default_return_url = 'ipam:vrf_list' default_return_url = 'ipam:vrf_list'
@@ -159,7 +166,7 @@ class RIRListView(ObjectListView):
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates')) queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
filter = filters.RIRFilter filter = filters.RIRFilter
filter_form = forms.RIRFilterForm filter_form = forms.RIRFilterForm
table = tables.RIRTable table = tables.RIRDetailTable
template_name = 'ipam/rir_list.html' template_name = 'ipam/rir_list.html'
def alter_queryset(self, request): def alter_queryset(self, request):
@@ -239,8 +246,8 @@ class RIRListView(ObjectListView):
} }
class RIREditView(PermissionRequiredMixin, ObjectEditView): class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_rir' permission_required = 'ipam.add_rir'
model = RIR model = RIR
form_class = forms.RIRForm form_class = forms.RIRForm
@@ -248,10 +255,16 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView):
return reverse('ipam:rir_list') return reverse('ipam:rir_list')
class RIREditView(RIRCreateView):
permission_required = 'ipam.change_rir'
class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_rir' permission_required = 'ipam.delete_rir'
cls = RIR cls = RIR
queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
filter = filters.RIRFilter filter = filters.RIRFilter
table = tables.RIRTable
default_return_url = 'ipam:rir_list' default_return_url = 'ipam:rir_list'
@@ -265,7 +278,7 @@ class AggregateListView(ObjectListView):
}) })
filter = filters.AggregateFilter filter = filters.AggregateFilter
filter_form = forms.AggregateFilterForm filter_form = forms.AggregateFilterForm
table = tables.AggregateTable table = tables.AggregateDetailTable
template_name = 'ipam/aggregate_list.html' template_name = 'ipam/aggregate_list.html'
def extra_context(self): def extra_context(self):
@@ -324,14 +337,18 @@ class AggregateView(View):
}) })
class AggregateEditView(PermissionRequiredMixin, ObjectEditView): class AggregateCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_aggregate' permission_required = 'ipam.add_aggregate'
model = Aggregate model = Aggregate
form_class = forms.AggregateForm form_class = forms.AggregateForm
template_name = 'ipam/aggregate_edit.html' template_name = 'ipam/aggregate_edit.html'
default_return_url = 'ipam:aggregate_list' default_return_url = 'ipam:aggregate_list'
class AggregateEditView(AggregateCreateView):
permission_required = 'ipam.change_aggregate'
class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView): class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_aggregate' permission_required = 'ipam.delete_aggregate'
model = Aggregate model = Aggregate
@@ -348,16 +365,19 @@ class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView): class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_aggregate' permission_required = 'ipam.change_aggregate'
cls = Aggregate cls = Aggregate
queryset = Aggregate.objects.select_related('rir')
filter = filters.AggregateFilter filter = filters.AggregateFilter
table = tables.AggregateTable
form = forms.AggregateBulkEditForm form = forms.AggregateBulkEditForm
template_name = 'ipam/aggregate_bulk_edit.html'
default_return_url = 'ipam:aggregate_list' default_return_url = 'ipam:aggregate_list'
class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_aggregate' permission_required = 'ipam.delete_aggregate'
cls = Aggregate cls = Aggregate
queryset = Aggregate.objects.select_related('rir')
filter = filters.AggregateFilter filter = filters.AggregateFilter
table = tables.AggregateTable
default_return_url = 'ipam:aggregate_list' default_return_url = 'ipam:aggregate_list'
@@ -371,8 +391,8 @@ class RoleListView(ObjectListView):
template_name = 'ipam/role_list.html' template_name = 'ipam/role_list.html'
class RoleEditView(PermissionRequiredMixin, ObjectEditView): class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_role' permission_required = 'ipam.add_role'
model = Role model = Role
form_class = forms.RoleForm form_class = forms.RoleForm
@@ -380,9 +400,14 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('ipam:role_list') return reverse('ipam:role_list')
class RoleEditView(RoleCreateView):
permission_required = 'ipam.change_role'
class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_role' permission_required = 'ipam.delete_role'
cls = Role cls = Role
table = tables.RoleTable
default_return_url = 'ipam:role_list' default_return_url = 'ipam:role_list'
@@ -394,7 +419,7 @@ class PrefixListView(ObjectListView):
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
filter = filters.PrefixFilter filter = filters.PrefixFilter
filter_form = forms.PrefixFilterForm filter_form = forms.PrefixFilterForm
table = tables.PrefixTable table = tables.PrefixDetailTable
template_name = 'ipam/prefix_list.html' template_name = 'ipam/prefix_list.html'
def alter_queryset(self, request): def alter_queryset(self, request):
@@ -429,7 +454,7 @@ class PrefixView(View):
).select_related( ).select_related(
'site', 'role' 'site', 'role'
).annotate_depth() ).annotate_depth()
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes) parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
parent_prefix_table.exclude = ('vrf',) parent_prefix_table.exclude = ('vrf',)
# Duplicate prefixes table # Duplicate prefixes table
@@ -440,7 +465,7 @@ class PrefixView(View):
).select_related( ).select_related(
'site', 'role' 'site', 'role'
) )
duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes)) duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
duplicate_prefix_table.exclude = ('vrf',) duplicate_prefix_table.exclude = ('vrf',)
# Child prefixes table # Child prefixes table
@@ -519,14 +544,18 @@ class PrefixIPAddressesView(View):
}) })
class PrefixEditView(PermissionRequiredMixin, ObjectEditView): class PrefixCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_prefix' permission_required = 'ipam.add_prefix'
model = Prefix model = Prefix
form_class = forms.PrefixForm form_class = forms.PrefixForm
template_name = 'ipam/prefix_edit.html' template_name = 'ipam/prefix_edit.html'
default_return_url = 'ipam:prefix_list' default_return_url = 'ipam:prefix_list'
class PrefixEditView(PrefixCreateView):
permission_required = 'ipam.change_prefix'
class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView): class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_prefix' permission_required = 'ipam.delete_prefix'
model = Prefix model = Prefix
@@ -544,16 +573,19 @@ class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView): class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_prefix' permission_required = 'ipam.change_prefix'
cls = Prefix cls = Prefix
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
filter = filters.PrefixFilter filter = filters.PrefixFilter
table = tables.PrefixTable
form = forms.PrefixBulkEditForm form = forms.PrefixBulkEditForm
template_name = 'ipam/prefix_bulk_edit.html'
default_return_url = 'ipam:prefix_list' default_return_url = 'ipam:prefix_list'
class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_prefix' permission_required = 'ipam.delete_prefix'
cls = Prefix cls = Prefix
queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
filter = filters.PrefixFilter filter = filters.PrefixFilter
table = tables.PrefixTable
default_return_url = 'ipam:prefix_list' default_return_url = 'ipam:prefix_list'
@@ -565,7 +597,7 @@ class IPAddressListView(ObjectListView):
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside') queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')
filter = filters.IPAddressFilter filter = filters.IPAddressFilter
filter_form = forms.IPAddressFilterForm filter_form = forms.IPAddressFilterForm
table = tables.IPAddressTable table = tables.IPAddressDetailTable
template_name = 'ipam/ipaddress_list.html' template_name = 'ipam/ipaddress_list.html'
@@ -581,7 +613,7 @@ class IPAddressView(View):
).select_related( ).select_related(
'site', 'role' 'site', 'role'
) )
parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes)) parent_prefixes_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
parent_prefixes_table.exclude = ('vrf',) parent_prefixes_table.exclude = ('vrf',)
# Duplicate IPs table # Duplicate IPs table
@@ -592,7 +624,7 @@ class IPAddressView(View):
).select_related( ).select_related(
'interface__device', 'nat_inside' 'interface__device', 'nat_inside'
) )
duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips)) duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
# Related IP table # Related IP table
related_ips = IPAddress.objects.select_related( related_ips = IPAddress.objects.select_related(
@@ -602,7 +634,7 @@ class IPAddressView(View):
).filter( ).filter(
vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address) vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
) )
related_ips_table = tables.IPAddressBriefTable(list(related_ips)) related_ips_table = tables.IPAddressTable(list(related_ips), orderable=False)
return render(request, 'ipam/ipaddress.html', { return render(request, 'ipam/ipaddress.html', {
'ipaddress': ipaddress, 'ipaddress': ipaddress,
@@ -612,21 +644,25 @@ class IPAddressView(View):
}) })
class IPAddressEditView(PermissionRequiredMixin, ObjectEditView): class IPAddressCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_ipaddress' permission_required = 'ipam.add_ipaddress'
model = IPAddress model = IPAddress
form_class = forms.IPAddressForm form_class = forms.IPAddressForm
template_name = 'ipam/ipaddress_edit.html' template_name = 'ipam/ipaddress_edit.html'
default_return_url = 'ipam:ipaddress_list' default_return_url = 'ipam:ipaddress_list'
class IPAddressEditView(IPAddressCreateView):
permission_required = 'ipam.change_ipaddress'
class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView): class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_ipaddress' permission_required = 'ipam.delete_ipaddress'
model = IPAddress model = IPAddress
default_return_url = 'ipam:ipaddress_list' default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView): class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView):
permission_required = 'ipam.add_ipaddress' permission_required = 'ipam.add_ipaddress'
pattern_form = forms.IPAddressPatternForm pattern_form = forms.IPAddressPatternForm
model_form = forms.IPAddressBulkAddForm model_form = forms.IPAddressBulkAddForm
@@ -641,33 +677,23 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
table = tables.IPAddressTable table = tables.IPAddressTable
default_return_url = 'ipam:ipaddress_list' 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): class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_ipaddress' permission_required = 'ipam.change_ipaddress'
cls = IPAddress cls = IPAddress
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
filter = filters.IPAddressFilter filter = filters.IPAddressFilter
table = tables.IPAddressTable
form = forms.IPAddressBulkEditForm form = forms.IPAddressBulkEditForm
template_name = 'ipam/ipaddress_bulk_edit.html'
default_return_url = 'ipam:ipaddress_list' default_return_url = 'ipam:ipaddress_list'
class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_ipaddress' permission_required = 'ipam.delete_ipaddress'
cls = IPAddress cls = IPAddress
queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
filter = filters.IPAddressFilter filter = filters.IPAddressFilter
table = tables.IPAddressTable
default_return_url = 'ipam:ipaddress_list' default_return_url = 'ipam:ipaddress_list'
@@ -683,8 +709,8 @@ class VLANGroupListView(ObjectListView):
template_name = 'ipam/vlangroup_list.html' template_name = 'ipam/vlangroup_list.html'
class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView): class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlangroup' permission_required = 'ipam.add_vlangroup'
model = VLANGroup model = VLANGroup
form_class = forms.VLANGroupForm form_class = forms.VLANGroupForm
@@ -692,10 +718,16 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('ipam:vlangroup_list') return reverse('ipam:vlangroup_list')
class VLANGroupEditView(VLANGroupCreateView):
permission_required = 'ipam.change_vlangroup'
class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlangroup' permission_required = 'ipam.delete_vlangroup'
cls = VLANGroup cls = VLANGroup
queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
filter = filters.VLANGroupFilter filter = filters.VLANGroupFilter
table = tables.VLANGroupTable
default_return_url = 'ipam:vlangroup_list' default_return_url = 'ipam:vlangroup_list'
@@ -707,7 +739,7 @@ class VLANListView(ObjectListView):
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes') queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
filter = filters.VLANFilter filter = filters.VLANFilter
filter_form = forms.VLANFilterForm filter_form = forms.VLANFilterForm
table = tables.VLANTable table = tables.VLANDetailTable
template_name = 'ipam/vlan_list.html' template_name = 'ipam/vlan_list.html'
@@ -719,7 +751,7 @@ class VLANView(View):
'site__region', 'tenant__group', 'role' 'site__region', 'tenant__group', 'role'
), pk=pk) ), pk=pk)
prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role') prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
prefix_table = tables.PrefixBriefTable(list(prefixes)) prefix_table = tables.PrefixTable(list(prefixes), orderable=False)
prefix_table.exclude = ('vlan',) prefix_table.exclude = ('vlan',)
return render(request, 'ipam/vlan.html', { return render(request, 'ipam/vlan.html', {
@@ -728,14 +760,18 @@ class VLANView(View):
}) })
class VLANEditView(PermissionRequiredMixin, ObjectEditView): class VLANCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_vlan' permission_required = 'ipam.add_vlan'
model = VLAN model = VLAN
form_class = forms.VLANForm form_class = forms.VLANForm
template_name = 'ipam/vlan_edit.html' template_name = 'ipam/vlan_edit.html'
default_return_url = 'ipam:vlan_list' default_return_url = 'ipam:vlan_list'
class VLANEditView(VLANCreateView):
permission_required = 'ipam.change_vlan'
class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView): class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_vlan' permission_required = 'ipam.delete_vlan'
model = VLAN model = VLAN
@@ -752,16 +788,19 @@ class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
class VLANBulkEditView(PermissionRequiredMixin, BulkEditView): class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'ipam.change_vlan' permission_required = 'ipam.change_vlan'
cls = VLAN cls = VLAN
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
filter = filters.VLANFilter filter = filters.VLANFilter
table = tables.VLANTable
form = forms.VLANBulkEditForm form = forms.VLANBulkEditForm
template_name = 'ipam/vlan_bulk_edit.html'
default_return_url = 'ipam:vlan_list' default_return_url = 'ipam:vlan_list'
class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'ipam.delete_vlan' permission_required = 'ipam.delete_vlan'
cls = VLAN cls = VLAN
queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
filter = filters.VLANFilter filter = filters.VLANFilter
table = tables.VLANTable
default_return_url = 'ipam:vlan_list' default_return_url = 'ipam:vlan_list'
@@ -769,8 +808,8 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# Services # Services
# #
class ServiceEditView(PermissionRequiredMixin, ObjectEditView): class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'ipam.change_service' permission_required = 'ipam.add_service'
model = Service model = Service
form_class = forms.ServiceForm form_class = forms.ServiceForm
template_name = 'ipam/service_edit.html' template_name = 'ipam/service_edit.html'
@@ -784,6 +823,10 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
return obj.device.get_absolute_url() return obj.device.get_absolute_url()
class ServiceEditView(ServiceCreateView):
permission_required = 'ipam.change_service'
class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView): class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'ipam.delete_service' permission_required = 'ipam.delete_service'
model = Service model = Service

View File

@@ -60,8 +60,8 @@ BASE_PATH = os.environ.get('BASE_PATH', '')
MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False) MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False)
# Credentials that NetBox will use to access live devices. # Credentials that NetBox will use to access live devices.
NETBOX_USERNAME = os.environ.get('NETBOX_USERNAME', '') NAPALM_USERNAME = os.environ.get('NAPALM_USERNAME', '')
NETBOX_PASSWORD = os.environ.get('NETBOX_PASSWORD', '') NAPALM_PASSWORD = os.environ.get('NAPALM_PASSWORD', '')
# Determine how many objects to display per page within a list. (Default: 50) # Determine how many objects to display per page within a list. (Default: 50)
PAGINATE_COUNT = os.environ.get('PAGINATE_COUNT', 50) PAGINATE_COUNT = os.environ.get('PAGINATE_COUNT', 50)

View File

@@ -93,9 +93,16 @@ MAINTENANCE_MODE = False
# all objects by specifying "?limit=0". # all objects by specifying "?limit=0".
MAX_PAGE_SIZE = 1000 MAX_PAGE_SIZE = 1000
# Credentials that NetBox will use to access live devices (future use). # Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
NETBOX_USERNAME = '' NAPALM_USERNAME = ''
NETBOX_PASSWORD = '' NAPALM_PASSWORD = ''
# NAPALM timeout (in seconds). (Default: 30)
NAPALM_TIMEOUT = 30
# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
# be provided as a dictionary.
NAPALM_ARGS = {}
# Determine how many objects to display per page within a list. (Default: 50) # Determine how many objects to display per page within a list. (Default: 50)
PAGINATE_COUNT = 50 PAGINATE_COUNT = 50

View File

@@ -13,7 +13,7 @@ except ImportError:
) )
VERSION = '2.0.6' VERSION = '2.1.2'
# Import required configuration parameters # Import required configuration parameters
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
@@ -46,8 +46,12 @@ MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000) MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50) PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '') NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '') NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30)
NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {})
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '') # Deprecated
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '') # Deprecated
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s') SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
@@ -56,6 +60,19 @@ TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
# Check for deprecated configuration parameters
config_logger = logging.getLogger('configuration')
config_logger.addHandler(logging.StreamHandler())
config_logger.setLevel(logging.WARNING)
if NETBOX_USERNAME:
config_logger.warning('NETBOX_USERNAME is deprecated and will be removed in v2.2. Please use NAPALM_USERNAME instead.')
if not NAPALM_USERNAME:
NAPALM_USERNAME = NETBOX_USERNAME
if NETBOX_PASSWORD:
config_logger.warning('NETBOX_PASSWORD is deprecated and will be removed in v2.2. Please use NAPALM_PASSWORD instead.')
if not NAPALM_PASSWORD:
NAPALM_PASSWORD = NETBOX_PASSWORD
# Attempt to import LDAP configuration if it has been defined # Attempt to import LDAP configuration if it has been defined
LDAP_IGNORE_CERT_ERRORS = False LDAP_IGNORE_CERT_ERRORS = False
try: try:
@@ -78,9 +95,9 @@ if LDAP_CONFIGURED:
if LDAP_IGNORE_CERT_ERRORS: if LDAP_IGNORE_CERT_ERRORS:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
# Enable logging for django_auth_ldap # Enable logging for django_auth_ldap
logger = logging.getLogger('django_auth_ldap') ldap_logger = logging.getLogger('django_auth_ldap')
logger.addHandler(logging.StreamHandler()) ldap_logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG) ldap_logger.setLevel(logging.DEBUG)
except ImportError: except ImportError:
raise ImproperlyConfigured( raise ImproperlyConfigured(
"LDAP authentication has been configured, but django-auth-ldap is not installed. You can remove " "LDAP authentication has been configured, but django-auth-ldap is not installed. You can remove "

View File

@@ -11,20 +11,20 @@ from django.views.generic import View
from circuits.filters import CircuitFilter, ProviderFilter from circuits.filters import CircuitFilter, ProviderFilter
from circuits.models import Circuit, Provider from circuits.models import Circuit, Provider
from circuits.tables import CircuitSearchTable, ProviderSearchTable from circuits.tables import CircuitTable, ProviderTable
from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter from dcim.filters import DeviceFilter, DeviceTypeFilter, RackFilter, SiteFilter
from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site from dcim.models import ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, Site
from dcim.tables import DeviceSearchTable, DeviceTypeSearchTable, RackSearchTable, SiteSearchTable from dcim.tables import DeviceTable, DeviceTypeTable, RackTable, SiteTable
from extras.models import TopologyMap, UserAction from extras.models import TopologyMap, UserAction
from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter from ipam.filters import AggregateFilter, IPAddressFilter, PrefixFilter, VLANFilter, VRFFilter
from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
from ipam.tables import AggregateSearchTable, IPAddressSearchTable, PrefixSearchTable, VLANSearchTable, VRFSearchTable from ipam.tables import AggregateTable, IPAddressTable, PrefixTable, VLANTable, VRFTable
from secrets.filters import SecretFilter from secrets.filters import SecretFilter
from secrets.models import Secret from secrets.models import Secret
from secrets.tables import SecretSearchTable from secrets.tables import SecretTable
from tenancy.filters import TenantFilter from tenancy.filters import TenantFilter
from tenancy.models import Tenant from tenancy.models import Tenant
from tenancy.tables import TenantSearchTable from tenancy.tables import TenantTable
from .forms import SearchForm from .forms import SearchForm
@@ -34,83 +34,85 @@ SEARCH_TYPES = OrderedDict((
('provider', { ('provider', {
'queryset': Provider.objects.all(), 'queryset': Provider.objects.all(),
'filter': ProviderFilter, 'filter': ProviderFilter,
'table': ProviderSearchTable, 'table': ProviderTable,
'url': 'circuits:provider_list', 'url': 'circuits:provider_list',
}), }),
('circuit', { ('circuit', {
'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'), 'queryset': Circuit.objects.select_related('type', 'provider', 'tenant').prefetch_related('terminations__site'),
'filter': CircuitFilter, 'filter': CircuitFilter,
'table': CircuitSearchTable, 'table': CircuitTable,
'url': 'circuits:circuit_list', 'url': 'circuits:circuit_list',
}), }),
# DCIM # DCIM
('site', { ('site', {
'queryset': Site.objects.select_related('region', 'tenant'), 'queryset': Site.objects.select_related('region', 'tenant'),
'filter': SiteFilter, 'filter': SiteFilter,
'table': SiteSearchTable, 'table': SiteTable,
'url': 'dcim:site_list', 'url': 'dcim:site_list',
}), }),
('rack', { ('rack', {
'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'), 'queryset': Rack.objects.select_related('site', 'group', 'tenant', 'role'),
'filter': RackFilter, 'filter': RackFilter,
'table': RackSearchTable, 'table': RackTable,
'url': 'dcim:rack_list', 'url': 'dcim:rack_list',
}), }),
('devicetype', { ('devicetype', {
'queryset': DeviceType.objects.select_related('manufacturer'), 'queryset': DeviceType.objects.select_related('manufacturer'),
'filter': DeviceTypeFilter, 'filter': DeviceTypeFilter,
'table': DeviceTypeSearchTable, 'table': DeviceTypeTable,
'url': 'dcim:devicetype_list', 'url': 'dcim:devicetype_list',
}), }),
('device', { ('device', {
'queryset': Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack'), 'queryset': Device.objects.select_related(
'device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack'
),
'filter': DeviceFilter, 'filter': DeviceFilter,
'table': DeviceSearchTable, 'table': DeviceTable,
'url': 'dcim:device_list', 'url': 'dcim:device_list',
}), }),
# IPAM # IPAM
('vrf', { ('vrf', {
'queryset': VRF.objects.select_related('tenant'), 'queryset': VRF.objects.select_related('tenant'),
'filter': VRFFilter, 'filter': VRFFilter,
'table': VRFSearchTable, 'table': VRFTable,
'url': 'ipam:vrf_list', 'url': 'ipam:vrf_list',
}), }),
('aggregate', { ('aggregate', {
'queryset': Aggregate.objects.select_related('rir'), 'queryset': Aggregate.objects.select_related('rir'),
'filter': AggregateFilter, 'filter': AggregateFilter,
'table': AggregateSearchTable, 'table': AggregateTable,
'url': 'ipam:aggregate_list', 'url': 'ipam:aggregate_list',
}), }),
('prefix', { ('prefix', {
'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'), 'queryset': Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role'),
'filter': PrefixFilter, 'filter': PrefixFilter,
'table': PrefixSearchTable, 'table': PrefixTable,
'url': 'ipam:prefix_list', 'url': 'ipam:prefix_list',
}), }),
('ipaddress', { ('ipaddress', {
'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'), 'queryset': IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device'),
'filter': IPAddressFilter, 'filter': IPAddressFilter,
'table': IPAddressSearchTable, 'table': IPAddressTable,
'url': 'ipam:ipaddress_list', 'url': 'ipam:ipaddress_list',
}), }),
('vlan', { ('vlan', {
'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'), 'queryset': VLAN.objects.select_related('site', 'group', 'tenant', 'role'),
'filter': VLANFilter, 'filter': VLANFilter,
'table': VLANSearchTable, 'table': VLANTable,
'url': 'ipam:vlan_list', 'url': 'ipam:vlan_list',
}), }),
# Secrets # Secrets
('secret', { ('secret', {
'queryset': Secret.objects.select_related('role', 'device'), 'queryset': Secret.objects.select_related('role', 'device'),
'filter': SecretFilter, 'filter': SecretFilter,
'table': SecretSearchTable, 'table': SecretTable,
'url': 'secrets:secret_list', 'url': 'secrets:secret_list',
}), }),
# Tenancy # Tenancy
('tenant', { ('tenant', {
'queryset': Tenant.objects.select_related('group'), 'queryset': Tenant.objects.select_related('group'),
'filter': TenantFilter, 'filter': TenantFilter,
'table': TenantSearchTable, 'table': TenantTable,
'url': 'tenancy:tenant_list', 'url': 'tenancy:tenant_list',
}), }),
)) ))
@@ -189,7 +191,7 @@ class SearchView(View):
# Construct the results table for this object type # Construct the results table for this object type
filtered_queryset = filter_cls({'q': form.cleaned_data['q']}, queryset=queryset).qs filtered_queryset = filter_cls({'q': form.cleaned_data['q']}, queryset=queryset).qs
table = table(filtered_queryset) table = table(filtered_queryset, orderable=False)
table.paginate(per_page=SEARCH_MAX_RESULTS) table.paginate(per_page=SEARCH_MAX_RESULTS)
if table.page: if table.page:

View File

@@ -81,6 +81,11 @@ footer p {
} }
} }
/* Navigation menu */
li.subnav > a {
padding-left: 30px;
}
/* Forms */ /* Forms */
label { label {
font-weight: normal; font-weight: normal;
@@ -333,6 +338,31 @@ table.component-list tr.ipaddress:hover td {
background-color: #e6f7f7; background-color: #e6f7f7;
} }
/* AJAX loader */
.loading {
position: fixed;
display: none;
z-index: 999;
height: 2em;
width: 2em;
overflow: show;
margin: auto;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.loading:before {
content: '';
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.3);
}
/* Misc */ /* Misc */
.banner-bottom { .banner-bottom {
margin-bottom: 50px; margin-bottom: 50px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -42,13 +42,15 @@ class UserKeyAdmin(admin.ModelAdmin):
if 'activate' in request.POST: if 'activate' in request.POST:
form = ActivateUserKeyForm(request.POST) form = ActivateUserKeyForm(request.POST)
if form.is_valid(): if form.is_valid():
try:
master_key = my_userkey.get_master_key(form.cleaned_data['secret_key']) master_key = my_userkey.get_master_key(form.cleaned_data['secret_key'])
if master_key is not None:
for uk in form.cleaned_data['_selected_action']: for uk in form.cleaned_data['_selected_action']:
uk.activate(master_key) uk.activate(master_key)
return redirect('admin:secrets_userkey_changelist') return redirect('admin:secrets_userkey_changelist')
except ValueError: else:
messages.error(request, "Invalid private key provided. Unable to retrieve master key.") messages.error(
request, "Invalid private key provided. Unable to retrieve master key.", extra_tags='error'
)
else: else:
form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)}) form = ActivateUserKeyForm(initial={'_selected_action': request.POST.getlist(admin.ACTION_CHECKBOX_NAME)})

View File

@@ -5,13 +5,14 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.api.serializers import NestedDeviceSerializer from dcim.api.serializers import NestedDeviceSerializer
from secrets.models import Secret, SecretRole from secrets.models import Secret, SecretRole
from utilities.api import ModelValidationMixin
# #
# SecretRoles # SecretRoles
# #
class SecretRoleSerializer(serializers.ModelSerializer): class SecretRoleSerializer(ModelValidationMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = SecretRole model = SecretRole
@@ -55,4 +56,7 @@ class WritableSecretSerializer(serializers.ModelSerializer):
validator.set_context(self) validator.set_context(self)
validator(data) validator(data)
# Enforce model validation
super(WritableSecretSerializer, self).validate(data)
return data return data

View File

@@ -30,6 +30,7 @@ class SecretRoleViewSet(ModelViewSet):
queryset = SecretRole.objects.all() queryset = SecretRole.objects.all()
serializer_class = serializers.SecretRoleSerializer serializer_class = serializers.SecretRoleSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
filter_class = filters.SecretRoleFilter
# #

View File

@@ -9,6 +9,13 @@ from dcim.models import Device
from utilities.filters import NumericInFilter from utilities.filters import NumericInFilter
class SecretRoleFilter(django_filters.FilterSet):
class Meta:
model = SecretRole
fields = ['name', 'slug']
class SecretFilter(django_filters.FilterSet): class SecretFilter(django_filters.FilterSet):
id__in = NumericInFilter(name='id', lookup_expr='in') id__in = NumericInFilter(name='id', lookup_expr='in')
q = django_filters.CharFilter( q = django_filters.CharFilter(
@@ -16,7 +23,6 @@ class SecretFilter(django_filters.FilterSet):
label='Search', label='Search',
) )
role_id = django_filters.ModelMultipleChoiceFilter( role_id = django_filters.ModelMultipleChoiceFilter(
name='role',
queryset=SecretRole.objects.all(), queryset=SecretRole.objects.all(),
label='Role (ID)', label='Role (ID)',
) )
@@ -27,7 +33,6 @@ class SecretFilter(django_filters.FilterSet):
label='Role (slug)', label='Role (slug)',
) )
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelMultipleChoiceFilter(
name='device',
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device (ID)', label='Device (ID)',
) )

View File

@@ -40,7 +40,7 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm):
class Meta: class Meta:
model = SecretRole model = SecretRole
fields = ['name', 'slug'] fields = ['name', 'slug', 'users', 'groups']
# #

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import django_tables2 as tables import django_tables2 as tables
from utilities.tables import BaseTable, SearchTable, ToggleColumn from utilities.tables import BaseTable, ToggleColumn
from .models import SecretRole, Secret from .models import SecretRole, Secret
@@ -43,11 +43,3 @@ class SecretTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Secret model = Secret
fields = ('pk', 'device', 'role', 'name', 'last_updated') fields = ('pk', 'device', 'role', 'name', 'last_updated')
class SecretSearchTable(SearchTable):
device = tables.LinkColumn()
class Meta(SearchTable.Meta):
model = Secret
fields = ('device', 'role', 'name', 'last_updated')

View File

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

View File

@@ -4,7 +4,6 @@ import base64
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import permission_required, login_required from django.contrib.auth.decorators import permission_required, login_required
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db import transaction, IntegrityError
from django.db.models import Count from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
@@ -40,8 +39,8 @@ class SecretRoleListView(ObjectListView):
template_name = 'secrets/secretrole_list.html' template_name = 'secrets/secretrole_list.html'
class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView): class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'secrets.change_secretrole' permission_required = 'secrets.add_secretrole'
model = SecretRole model = SecretRole
form_class = forms.SecretRoleForm form_class = forms.SecretRoleForm
@@ -49,9 +48,15 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView):
return reverse('secrets:secretrole_list') return reverse('secrets:secretrole_list')
class SecretRoleEditView(SecretRoleCreateView):
permission_required = 'secrets.change_secretrole'
class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'secrets.delete_secretrole' permission_required = 'secrets.delete_secretrole'
cls = SecretRole cls = SecretRole
queryset = SecretRole.objects.annotate(secret_count=Count('secrets'))
table = tables.SecretRoleTable
default_return_url = 'secrets:secretrole_list' default_return_url = 'secrets:secretrole_list'
@@ -236,14 +241,17 @@ class SecretBulkImportView(BulkImportView):
class SecretBulkEditView(PermissionRequiredMixin, BulkEditView): class SecretBulkEditView(PermissionRequiredMixin, BulkEditView):
permission_required = 'secrets.change_secret' permission_required = 'secrets.change_secret'
cls = Secret cls = Secret
queryset = Secret.objects.select_related('role', 'device')
filter = filters.SecretFilter filter = filters.SecretFilter
table = tables.SecretTable
form = forms.SecretBulkEditForm form = forms.SecretBulkEditForm
template_name = 'secrets/secret_bulk_edit.html'
default_return_url = 'secrets:secret_list' default_return_url = 'secrets:secret_list'
class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class SecretBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
permission_required = 'secrets.delete_secret' permission_required = 'secrets.delete_secret'
cls = Secret cls = Secret
queryset = Secret.objects.select_related('role', 'device')
filter = filters.SecretFilter filter = filters.SecretFilter
table = tables.SecretTable
default_return_url = 'secrets:secret_list' default_return_url = 'secrets:secret_list'

View File

@@ -3,7 +3,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>NetBox - {% block title %}Home{% endblock %}</title> <title>{% block title %}Home{% endblock %} - NetBox</title>
<link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}"> <link rel="stylesheet" href="{% static 'bootstrap-3.3.7-dist/css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'font-awesome-4.7.0/css/font-awesome.min.css' %}"> <link rel="stylesheet" href="{% static 'font-awesome-4.7.0/css/font-awesome.min.css' %}">
<link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.css' %}"> <link rel="stylesheet" href="{% static 'jquery-ui-1.12.1/jquery-ui.css' %}">
@@ -31,197 +31,197 @@
<li class="dropdown{% if request.path|contains:'/dcim/sites/,/dcim/regions/,/tenancy/' %} active{% endif %}"> <li class="dropdown{% if request.path|contains:'/dcim/sites/,/dcim/regions/,/tenancy/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Organization <span class="caret"></span></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'dcim:site_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Sites</a></li> <li><a href="{% url 'dcim:site_list' %}"><strong>Sites</strong></a></li>
{% if perms.dcim.add_site %} {% if perms.dcim.add_site %}
<li><a href="{% url 'dcim:site_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Site</a></li> <li class="subnav"><a href="{% url 'dcim:site_add' %}"><i class="fa fa-plus"></i> Add a Site</a></li>
<li><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Sites</a></li> <li class="subnav"><a href="{% url 'dcim:site_import' %}"><i class="fa fa-download"></i> Import Sites</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'dcim:region_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Regions</a></li> <li><a href="{% url 'dcim:region_list' %}"><strong>Regions</strong></a></li>
{% if perms.dcim.add_region %} {% if perms.dcim.add_region %}
<li><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Region</a></li> <li class="subnav"><a href="{% url 'dcim:region_add' %}"><i class="fa fa-plus"></i> Add a Region</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'tenancy:tenant_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenants</a></li> <li><a href="{% url 'tenancy:tenant_list' %}"><strong>Tenants</strong></a></li>
{% if perms.tenancy.add_tenant %} {% if perms.tenancy.add_tenant %}
<li><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant</a></li> <li class="subnav"><a href="{% url 'tenancy:tenant_add' %}"><i class="fa fa-plus"></i> Add a Tenant</a></li>
<li><a href="{% url 'tenancy:tenant_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Tenants</a></li> <li class="subnav"><a href="{% url 'tenancy:tenant_import' %}"><i class="fa fa-download"></i> Import Tenants</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'tenancy:tenantgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Tenant Groups</a></li> <li><a href="{% url 'tenancy:tenantgroup_list' %}"><strong>Tenant Groups</strong></a></li>
{% if perms.tenancy.add_tenantgroup %} {% if perms.tenancy.add_tenantgroup %}
<li><a href="{% url 'tenancy:tenantgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Tenant Group</a></li> <li class="subnav"><a href="{% url 'tenancy:tenantgroup_add' %}"><i class="fa fa-plus"></i> Add a Tenant Group</a></li>
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
<li class="dropdown{% if request.path|contains:'/dcim/rack' %} active{% endif %}"> <li class="dropdown{% if request.path|contains:'/dcim/rack' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Racks <span class="caret"></span></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'dcim:rack_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Racks</a></li> <li><a href="{% url 'dcim:rack_list' %}"><strong>Racks</strong></a></li>
<li><a href="{% url 'dcim:rack_elevation_list' %}"><i class="fa fa-bars" aria-hidden="true"></i> Rack Elevations</a></li>
{% if perms.dcim.add_rack %} {% if perms.dcim.add_rack %}
<li><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack</a></li> <li class="subnav"><a href="{% url 'dcim:rack_add' %}"><i class="fa fa-plus"></i> Add a Rack</a></li>
<li><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Racks</a></li> <li class="subnav"><a href="{% url 'dcim:rack_import' %}"><i class="fa fa-download"></i> Import Racks</a></li>
{% endif %} {% endif %}
<li><a href="{% url 'dcim:rack_elevation_list' %}"><strong>Rack Elevations</strong></a></li>
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'dcim:rackgroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Groups</a></li> <li><a href="{% url 'dcim:rackgroup_list' %}"><strong>Rack Groups</strong></a></li>
{% if perms.dcim.add_rackgroup %} {% if perms.dcim.add_rackgroup %}
<li><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Group</a></li> <li class="subnav"><a href="{% url 'dcim:rackgroup_add' %}"><i class="fa fa-plus"></i> Add a Rack Group</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'dcim:rackrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Roles</a></li> <li><a href="{% url 'dcim:rackrole_list' %}"><strong>Rack Roles</strong></a></li>
{% if perms.dcim.add_rackrole %} {% if perms.dcim.add_rackrole %}
<li><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Rack Role</a></li> <li class="subnav"><a href="{% url 'dcim:rackrole_add' %}"><i class="fa fa-plus"></i> Add a Rack Role</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'dcim:rackreservation_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Rack Reservations</a></li> <li><a href="{% url 'dcim:rackreservation_list' %}"><strong>Rack Reservations</strong></a></li>
</ul> </ul>
</li> </li>
<li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/' %} active{% endif %}"> <li class="dropdown{% if request.path|contains:'/dcim/device,/dcim/manufacturers/,/dcim/platforms/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Devices <span class="caret"></span></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'dcim:device_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Devices</a></li> <li><a href="{% url 'dcim:device_list' %}"><strong>Devices</strong></a></li>
{% if perms.dcim.add_device %} {% if perms.dcim.add_device %}
<li><a href="{% url 'dcim:device_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device</a></li> <li class="subnav"><a href="{% url 'dcim:device_add' %}"><i class="fa fa-plus"></i> Add a Device</a></li>
<li><a href="{% url 'dcim:device_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Devices</a></li> <li class="subnav"><a href="{% url 'dcim:device_import' %}"><i class="fa fa-download"></i> Import Devices</a></li>
{% endif %} {% endif %}
{% if perms.ipam.add_device or perms.ipam.add_devicetype %} {% if perms.ipam.add_device or perms.ipam.add_devicetype %}
<li class="divider"></li> <li class="divider"></li>
{% endif %} {% endif %}
<li><a href="{% url 'dcim:devicetype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Types</a></li> <li><a href="{% url 'dcim:devicetype_list' %}"><strong>Device Types</strong></a></li>
{% if perms.dcim.add_devicetype %} {% if perms.dcim.add_devicetype %}
<li><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Type</a></li> <li class="subnav"><a href="{% url 'dcim:devicetype_add' %}"><i class="fa fa-plus"></i> Add a Device Type</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'dcim:devicerole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Device Roles</a></li> <li><a href="{% url 'dcim:devicerole_list' %}"><strong>Device Roles</strong></a></li>
{% if perms.dcim.add_devicerole %} {% if perms.dcim.add_devicerole %}
<li><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Device Role</a></li> <li class="subnav"><a href="{% url 'dcim:devicerole_add' %}"><i class="fa fa-plus"></i> Add a Device Role</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_devicerole or perms.dcim.add_manufacturer %} {% if perms.dcim.add_devicerole or perms.dcim.add_manufacturer %}
<li class="divider"></li> <li class="divider"></li>
{% endif %} {% endif %}
<li><a href="{% url 'dcim:manufacturer_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Manufacturers</a></li> <li><a href="{% url 'dcim:manufacturer_list' %}"><strong>Manufacturers</strong></a></li>
{% if perms.dcim.add_manufacturer %} {% if perms.dcim.add_manufacturer %}
<li><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Manufacturer</a></li> <li class="subnav"><a href="{% url 'dcim:manufacturer_add' %}"><i class="fa fa-plus"></i> Add a Manufacturer</a></li>
{% endif %} {% endif %}
{% if perms.dcim.add_manufacturer or perms.dcim.add_platform %} {% if perms.dcim.add_manufacturer or perms.dcim.add_platform %}
<li class="divider"></li> <li class="divider"></li>
{% endif %} {% endif %}
<li><a href="{% url 'dcim:platform_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Platforms</a></li> <li><a href="{% url 'dcim:platform_list' %}"><strong>Platforms</strong></a></li>
{% if perms.dcim.add_platform %} {% if perms.dcim.add_platform %}
<li><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Platform</a></li> <li class="subnav"><a href="{% url 'dcim:platform_add' %}"><i class="fa fa-plus"></i> Add a Platform</a></li>
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
<li class="dropdown{% if request.path|contains:'/dcim/console-connections/,/dcim/power-connections/,/dcim/interface-connections/' %} active{% endif %}"> <li class="dropdown{% if request.path|contains:'/dcim/console-connections/,/dcim/power-connections/,/dcim/interface-connections/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Connections <span class="caret"></span></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'dcim:console_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Console Connections</a></li> <li><a href="{% url 'dcim:console_connections_list' %}"><strong>Console Connections</strong></a></li>
{% if perms.dcim.change_consoleport %} {% if perms.dcim.change_consoleport %}
<li><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Console Connections</a></li> <li class="subnav"><a href="{% url 'dcim:console_connections_import' %}"><i class="fa fa-download"></i> Import Console Connections</a></li>
{% endif %} {% endif %}
{% if perms.ipam.change_consoleport or perms.ipam.change_powerport %} {% if perms.ipam.change_consoleport or perms.ipam.change_powerport %}
<li class="divider"></li> <li class="divider"></li>
{% endif %} {% endif %}
<li><a href="{% url 'dcim:power_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Power Connections</a></li> <li><a href="{% url 'dcim:power_connections_list' %}"><strong>Power Connections</strong></a></li>
{% if perms.dcim.change_powerport %} {% if perms.dcim.change_powerport %}
<li><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Power Connections</a></li> <li class="subnav"><a href="{% url 'dcim:power_connections_import' %}"><i class="fa fa-download"></i> Import Power Connections</a></li>
{% endif %} {% endif %}
{% if perms.ipam.change_powerport or perms.ipam.add_interfaceconnection %} {% if perms.ipam.change_powerport or perms.ipam.add_interfaceconnection %}
<li class="divider"></li> <li class="divider"></li>
{% endif %} {% endif %}
<li><a href="{% url 'dcim:interface_connections_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Interface Connections</a></li> <li><a href="{% url 'dcim:interface_connections_list' %}"><strong>Interface Connections</strong></a></li>
{% if perms.dcim.add_interfaceconnection %} {% if perms.dcim.add_interfaceconnection %}
<li><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Interface Connections</a></li> <li class="subnav"><a href="{% url 'dcim:interface_connections_import' %}"><i class="fa fa-download"></i> Import Interface Connections</a></li>
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
<li class="dropdown{% if request.path|contains:'/ipam/' and not request.path|contains:'/ipam/vlan' %} active{% endif %}"> <li class="dropdown{% if request.path|contains:'/ipam/' and not request.path|contains:'/ipam/vlan' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">IP Space <span class="caret"></span></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'ipam:ipaddress_list' %}"><i class="fa fa-search" aria-hidden="true"></i> IP Addresses</a></li> <li><a href="{% url 'ipam:ipaddress_list' %}"><strong>IP Addresses</strong></a></li>
{% if perms.ipam.add_ipaddress %} {% if perms.ipam.add_ipaddress %}
<li><a href="{% url 'ipam:ipaddress_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an IP</a></li> <li class="subnav"><a href="{% url 'ipam:ipaddress_add' %}"><i class="fa fa-plus"></i> Add an IP</a></li>
<li><a href="{% url 'ipam:ipaddress_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import IPs</a></li> <li class="subnav"><a href="{% url 'ipam:ipaddress_import' %}"><i class="fa fa-download"></i> Import IPs</a></li>
{% endif %} {% endif %}
{% if perms.ipam.add_ipaddress or perms.ipam.add_prefix %} {% if perms.ipam.add_ipaddress or perms.ipam.add_prefix %}
<li class="divider"></li> <li class="divider"></li>
{% endif %} {% endif %}
<li><a href="{% url 'ipam:prefix_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefixes</a></li> <li><a href="{% url 'ipam:prefix_list' %}"><strong>Prefixes</strong></a></li>
{% if perms.ipam.add_prefix %} {% if perms.ipam.add_prefix %}
<li><a href="{% url 'ipam:prefix_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Prefix</a></li> <li class="subnav"><a href="{% url 'ipam:prefix_add' %}"><i class="fa fa-plus"></i> Add a Prefix</a></li>
<li><a href="{% url 'ipam:prefix_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Prefixes</a></li> <li class="subnav"><a href="{% url 'ipam:prefix_import' %}"><i class="fa fa-download"></i> Import Prefixes</a></li>
{% endif %} {% endif %}
{% if perms.ipam.add_prefix or perms.ipam.add_aggregate %} {% if perms.ipam.add_prefix or perms.ipam.add_aggregate %}
<li class="divider"></li> <li class="divider"></li>
{% endif %} {% endif %}
<li><a href="{% url 'ipam:aggregate_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Aggregates</a></li> <li><a href="{% url 'ipam:aggregate_list' %}"><strong>Aggregates</strong></a></li>
{% if perms.ipam.add_aggregate %} {% if perms.ipam.add_aggregate %}
<li><a href="{% url 'ipam:aggregate_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add an Aggregate</a></li> <li class="subnav"><a href="{% url 'ipam:aggregate_add' %}"><i class="fa fa-plus"></i> Add an Aggregate</a></li>
<li><a href="{% url 'ipam:aggregate_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Aggregates</a></li> <li class="subnav"><a href="{% url 'ipam:aggregate_import' %}"><i class="fa fa-download"></i> Import Aggregates</a></li>
{% endif %} {% endif %}
{% if perms.ipam.add_aggregate or perms.ipam.add_vrf %} {% if perms.ipam.add_aggregate or perms.ipam.add_vrf %}
<li class="divider"></li> <li class="divider"></li>
{% endif %} {% endif %}
<li><a href="{% url 'ipam:vrf_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VRFs</a></li> <li><a href="{% url 'ipam:vrf_list' %}"><strong>VRFs</strong></a></li>
{% if perms.ipam.add_vrf %} {% if perms.ipam.add_vrf %}
<li><a href="{% url 'ipam:vrf_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VRF</a></li> <li class="subnav"><a href="{% url 'ipam:vrf_add' %}"><i class="fa fa-plus"></i> Add a VRF</a></li>
<li><a href="{% url 'ipam:vrf_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VRFs</a></li> <li class="subnav"><a href="{% url 'ipam:vrf_import' %}"><i class="fa fa-download"></i> Import VRFs</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'ipam:rir_list' %}"><i class="fa fa-search" aria-hidden="true"></i> RIRs</a></li> <li><a href="{% url 'ipam:rir_list' %}"><strong>RIRs</strong></a></li>
{% if perms.ipam.add_rir %} {% if perms.ipam.add_rir %}
<li><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a RIR</a></li> <li class="subnav"><a href="{% url 'ipam:rir_add' %}"><i class="fa fa-plus"></i> Add a RIR</a></li>
{% endif %} {% endif %}
{% if perms.ipam.add_rir or perms.ipam.add_role %} {% if perms.ipam.add_rir or perms.ipam.add_role %}
<li class="divider"></li> <li class="divider"></li>
{% endif %} {% endif %}
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Prefix Roles</a></li> <li><a href="{% url 'ipam:role_list' %}"><strong>Prefix Roles</strong></a></li>
{% if perms.ipam.add_role %} {% if perms.ipam.add_role %}
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li> <li class="subnav"><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus"></i> Add a Role</a></li>
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
<li class="dropdown{% if request.path|contains:'/ipam/vlan' %} active{% endif %}"> <li class="dropdown{% if request.path|contains:'/ipam/vlan' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">VLANs <span class="caret"></span></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'ipam:vlan_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLANs</a></li> <li><a href="{% url 'ipam:vlan_list' %}"><strong>VLANs</strong></a></li>
{% if perms.ipam.add_vlan %} {% if perms.ipam.add_vlan %}
<li><a href="{% url 'ipam:vlan_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN</a></li> <li class="subnav"><a href="{% url 'ipam:vlan_add' %}"><i class="fa fa-plus"></i> Add a VLAN</a></li>
<li><a href="{% url 'ipam:vlan_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import VLANs</a></li> <li class="subnav"><a href="{% url 'ipam:vlan_import' %}"><i class="fa fa-download"></i> Import VLANs</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'ipam:vlangroup_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Groups</a></li> <li><a href="{% url 'ipam:vlangroup_list' %}"><strong>VLAN Groups</strong></a></li>
{% if perms.ipam.add_vlangroup %} {% if perms.ipam.add_vlangroup %}
<li><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a VLAN Group</a></li> <li class="subnav"><a href="{% url 'ipam:vlangroup_add' %}"><i class="fa fa-plus"></i> Add a VLAN Group</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'ipam:role_list' %}"><i class="fa fa-search" aria-hidden="true"></i> VLAN Roles</a></li> <li><a href="{% url 'ipam:role_list' %}"><strong>VLAN Roles</strong></a></li>
{% if perms.ipam.add_role %} {% if perms.ipam.add_role %}
<li><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Role</a></li> <li class="subnav"><a href="{% url 'ipam:role_add' %}"><i class="fa fa-plus"></i> Add a Role</a></li>
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
<li class="dropdown{% if request.path|contains:'/circuits/' %} active{% endif %}"> <li class="dropdown{% if request.path|contains:'/circuits/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Circuits <span class="caret"></span></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'circuits:provider_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Providers</a></li> <li><a href="{% url 'circuits:provider_list' %}"><strong>Providers</strong></a></li>
{% if perms.circuits.add_provider %} {% if perms.circuits.add_provider %}
<li><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Provider</a></li> <li class="subnav"><a href="{% url 'circuits:provider_add' %}"><i class="fa fa-plus"></i> Add a Provider</a></li>
<li><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Providers</a></li> <li class="subnav"><a href="{% url 'circuits:provider_import' %}"><i class="fa fa-download"></i> Import Providers</a></li>
{% endif %} {% endif %}
{% if perms.circuits.add_circuit or perms.circuits.add_provider %} {% if perms.circuits.add_circuit or perms.circuits.add_provider %}
<li class="divider"></li> <li class="divider"></li>
{% endif %} {% endif %}
<li><a href="{% url 'circuits:circuit_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuits</a></li> <li><a href="{% url 'circuits:circuit_list' %}"><strong>Circuits</strong></a></li>
{% if perms.circuits.add_circuit %} {% if perms.circuits.add_circuit %}
<li><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit</a></li> <li class="subnav"><a href="{% url 'circuits:circuit_add' %}"><i class="fa fa-plus"></i> Add a Circuit</a></li>
<li><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Circuits</a></li> <li class="subnav"><a href="{% url 'circuits:circuit_import' %}"><i class="fa fa-download"></i> Import Circuits</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'circuits:circuittype_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Circuit Types</a></li> <li><a href="{% url 'circuits:circuittype_list' %}"><strong>Circuit Types</strong></a></li>
{% if perms.circuits.add_circuittype %} {% if perms.circuits.add_circuittype %}
<li><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Circuit Type</a></li> <li class="subnav"><a href="{% url 'circuits:circuittype_add' %}"><i class="fa fa-plus"></i> Add a Circuit Type</a></li>
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
@@ -229,14 +229,14 @@
<li class="dropdown{% if request.path|contains:'/secrets/' %} active{% endif %}"> <li class="dropdown{% if request.path|contains:'/secrets/' %} active{% endif %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Secrets <span class="caret"></span></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'secrets:secret_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secrets</a></li> <li><a href="{% url 'secrets:secret_list' %}"><strong>Secrets</strong></a></li>
{% if perms.secrets.add_secret %} {% if perms.secrets.add_secret %}
<li><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download" aria-hidden="true"></i> Import Secrets</a></li> <li class="subnav"><a href="{% url 'secrets:secret_import' %}"><i class="fa fa-download"></i> Import Secrets</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'secrets:secretrole_list' %}"><i class="fa fa-search" aria-hidden="true"></i> Secret Roles</a></li> <li><a href="{% url 'secrets:secretrole_list' %}"><strong>Secret Roles</strong></a></li>
{% if perms.secrets.add_secretrole %} {% if perms.secrets.add_secretrole %}
<li><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus" aria-hidden="true"></i> Add a Secret Role</a></li> <li class="subnav"><a href="{% url 'secrets:secretrole_add' %}"><i class="fa fa-plus"></i> Add a Secret Role</a></li>
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
@@ -250,16 +250,16 @@
{{ request.user|truncatechars:"30" }} <span class="caret"></span> {{ request.user|truncatechars:"30" }} <span class="caret"></span>
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'user:profile' %}"><i class="fa fa-user" aria-hidden="true"></i> Profile</a></li> <li><a href="{% url 'user:profile' %}"><i class="fa fa-user"></i> Profile</a></li>
{% if request.user.is_staff %} {% if request.user.is_staff %}
<li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs" aria-hidden="true"></i> Admin</a></li> <li><a href="{% url 'admin:index' %}"><i class="fa fa-cogs"></i> Admin</a></li>
{% endif %} {% endif %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'logout' %}"><i class="fa fa-sign-out" aria-hidden="true"></i> Log out</a></li> <li><a href="{% url 'logout' %}"><i class="fa fa-sign-out"></i> Log out</a></li>
</ul> </ul>
</li> </li>
{% else %} {% else %}
<li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in" aria-hidden="true"></i> Log in</a></li> <li><a href="{% url 'login' %}?next={{ request.path }}"><i class="fa fa-sign-in"></i> Log in</a></li>
{% endif %} {% endif %}
</ul> </ul>
<form action="{% url 'search' %}" method="get" class="navbar-form navbar-right" id="navbar_search" role="search"> <form action="{% url 'search' %}" method="get" class="navbar-form navbar-right" id="navbar_search" role="search">
@@ -267,7 +267,7 @@
<input type="text" name="q" class="form-control" placeholder="Search"> <input type="text" name="q" class="form-control" placeholder="Search">
<span class="input-group-btn"> <span class="input-group-btn">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="fa fa-search" aria-hidden="true"></i> <i class="fa fa-search"></i>
</button> </button>
</span> </span>
</div> </div>
@@ -290,7 +290,7 @@
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissable" role="alert"> <div class="alert alert-{{ message.tags }} alert-dismissable" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span> <span>&times;</span>
</button> </button>
{{ message }} {{ message }}
</div> </div>
@@ -323,13 +323,19 @@
</div> </div>
</div> </div>
</footer> </footer>
<script type="text/javascript"> <script src="{% static 'js/jquery-3.2.1.min.js' %}"></script>
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
</script>
<script src="{% static 'js/jquery-3.2.0.min.js' %}"></script>
<script src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script> <script src="{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"></script>
<script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script> <script src="{% static 'bootstrap-3.3.7-dist/js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/forms.js' %}?v{{ settings.VERSION }}"></script> <script src="{% static 'js/forms.js' %}?v{{ settings.VERSION }}"></script>
<script type="text/javascript">
var netbox_api_path = "/{{ settings.BASE_PATH }}api/";
var loading = $(".loading");
$(document).ajaxStart(function() {
loading.show();
}).ajaxStop(function() {
loading.hide();
});
</script>
{% block javascript %}{% endblock %} {% block javascript %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load helpers %} {% load helpers %}
{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
@@ -39,7 +37,7 @@
</a> </a>
{% endif %} {% endif %}
</div> </div>
<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1> <h1>{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=circuit %} {% include 'inc/created_updated.html' with obj=circuit %}
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">

View File

@@ -1,23 +0,0 @@
{% extends 'utilities/bulk_edit_form.html' %}
{% load form_helpers %}
{% block title %}Circuit Bulk Edit{% endblock %}
{% block selected_objects_table %}
<tr>
<th>Circuit</th>
<th>Type</th>
<th>Provider</th>
<th>Port speed</th>
<th>Commit rate</th>
</tr>
{% for circuit in selected_objects %}
<tr>
<td><a href="{% url 'circuits:circuit' pk=circuit.pk %}">{{ circuit }}</a></td>
<td>{{ circuit.type }}</td>
<td>{{ circuit.provider }}</td>
<td>{{ circuit.port_speed_human }}</td>
<td>{{ circuit.commit_rate_human }}</td>
</tr>
{% endfor %}
{% endblock %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load helpers %} {% load helpers %}
{% block title %}Circuits{% endblock %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.circuits.add_circuit %} {% if perms.circuits.add_circuit %}
@@ -17,7 +15,7 @@
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='circuits' %} {% include 'inc/export_button.html' with obj_type='circuits' %}
</div> </div>
<h1>Circuits</h1> <h1>{% block title %}Circuits{% endblock %}</h1>
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}

View File

@@ -2,10 +2,6 @@
{% load staticfiles %} {% load staticfiles %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}
Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}
{% endblock %}
{% block content %} {% block content %}
<form action="." method="post" class="form form-horizontal"> <form action="." method="post" class="form form-horizontal">
{% csrf_token %} {% csrf_token %}
@@ -14,7 +10,7 @@
{% endfor %} {% endfor %}
<div class="row"> <div class="row">
<div class="col-md-6 col-md-offset-3"> <div class="col-md-6 col-md-offset-3">
<h3>Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}</h3> <h3>{% block title %}Circuit {{ obj.circuit }} - {{ form.term_side.value }} Side{% endblock %}</h3>
{% if form.non_field_errors %} {% if form.non_field_errors %}
<div class="panel panel-danger"> <div class="panel panel-danger">
<div class="panel-heading"><strong>Errors</strong></div> <div class="panel-heading"><strong>Errors</strong></div>

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load helpers %} {% load helpers %}
{% block title %}Circuit Types{% endblock %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.circuits.add_circuittype %} {% if perms.circuits.add_circuittype %}
@@ -12,7 +10,7 @@
</a> </a>
{% endif %} {% endif %}
</div> </div>
<h1>Circuit Types</h1> <h1>{% block title %}Circuit Types{% endblock %}</h1>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='circuits:circuittype_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_delete_url='circuits:circuittype_bulk_delete' %}

View File

@@ -2,8 +2,6 @@
{% load static from staticfiles %} {% load static from staticfiles %}
{% load helpers %} {% load helpers %}
{% block title %}{{ provider }}{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-sm-8 col-md-9"> <div class="col-sm-8 col-md-9">
@@ -45,7 +43,7 @@
</a> </a>
{% endif %} {% endif %}
</div> </div>
<h1>{{ provider }}</h1> <h1>{% block title %}{{ provider }}{% endblock %}</h1>
{% include 'inc/created_updated.html' with obj=provider %} {% include 'inc/created_updated.html' with obj=provider %}
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">

View File

@@ -1,19 +0,0 @@
{% extends 'utilities/bulk_edit_form.html' %}
{% load form_helpers %}
{% block title %}Provider Bulk Edit{% endblock %}
{% block selected_objects_table %}
<tr>
<th>Provider</th>
<th>Account</th>
<th>ASN</th>
</tr>
{% for provider in selected_objects %}
<tr>
<td><a href="{% url 'circuits:provider' slug=provider.slug %}">{{ provider }}</a></td>
<td>{{ provider.account }}</td>
<td>{{ provider.asn }}</td>
</tr>
{% endfor %}
{% endblock %}

View File

@@ -1,7 +1,5 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% block title %}Providers{% endblock %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.circuits.add_provider %} {% if perms.circuits.add_provider %}
@@ -16,7 +14,7 @@
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='providers' %} {% include 'inc/export_button.html' with obj_type='providers' %}
</div> </div>
<h1>Providers</h1> <h1>{% block title %}Providers{% endblock %}</h1>
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}

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

@@ -1,7 +1,5 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% block title %}Console Connections{% endblock %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.dcim.change_consoleport %} {% if perms.dcim.change_consoleport %}
@@ -12,7 +10,7 @@
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='connections' %} {% include 'inc/export_button.html' with obj_type='connections' %}
</div> </div>
<h1>Console Connections</h1> <h1>{% block title %}Console Connections{% endblock %}</h1>
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
{% include 'responsive_table.html' %} {% include 'responsive_table.html' %}

View File

@@ -2,8 +2,6 @@
{% load static from staticfiles %} {% load static from staticfiles %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Connect {{ consoleport.device }} {{ consoleport }}{% endblock %}
{% block content %} {% block content %}
<form action="." method="post" class="form form-horizontal"> <form action="." method="post" class="form form-horizontal">
{% csrf_token %} {% csrf_token %}
@@ -21,7 +19,7 @@
</div> </div>
{% endif %} {% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Connect {{ consoleport.device }} {{ consoleport }}</div> <div class="panel-heading">{% block title %}Connect {{ consoleport.device }} {{ consoleport }}{% endblock %}</div>
<div class="panel-body"> <div class="panel-body">
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li> <li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

View File

@@ -2,8 +2,6 @@
{% load static from staticfiles %} {% load static from staticfiles %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Connect {{ consoleserverport.device }} {{ consoleserverport }}{% endblock %}
{% block content %} {% block content %}
<form action="." method="post" class="form form-horizontal"> <form action="." method="post" class="form form-horizontal">
{% csrf_token %} {% csrf_token %}
@@ -21,7 +19,7 @@
</div> </div>
{% endif %} {% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Connect {{ consoleserverport.device }} {{ consoleserverport }}</div> <div class="panel-heading">{% block title %}Connect {{ consoleserverport.device }} {{ consoleserverport }}{% endblock %}</div>
<div class="panel-body"> <div class="panel-body">
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li> <li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

View File

@@ -7,7 +7,7 @@
{% block content %} {% block content %}
{% include 'dcim/inc/device_header.html' with active_tab='info' %} {% include 'dcim/inc/device_header.html' with active_tab='info' %}
<div class="row"> <div class="row">
<div class="col-md-5 col-lg-6"> <div class="col-md-5">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Device</strong> <strong>Device</strong>
@@ -204,7 +204,7 @@
None None
</div> </div>
{% endif %} {% endif %}
{% if perms.dcim.add_service %} {% if perms.ipam.add_service %}
<div class="panel-footer text-right"> <div class="panel-footer text-right">
<a href="{% url 'dcim:service_assign' device=device.pk %}" class="btn btn-xs btn-primary"> <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 <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
@@ -214,23 +214,9 @@
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Critical Connections</strong> <strong>Console / Power</strong>
</div> </div>
<table class="table table-hover panel-body component-list"> <table class="table table-hover panel-body component-list">
{% for iface in mgmt_interfaces %}
{% include 'dcim/inc/interface.html' with icon='wrench' %}
{% empty %}
{% if device.device_type.interface_templates.exists %}
<tr>
<td colspan="6" class="alert-warning">
<i class="fa fa-fw fa-warning"></i> No management interfaces defined
{% if perms.dcim.add_interface %}
<a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-primary btn-xs pull-right"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></a>
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
{% for cp in console_ports %} {% for cp in console_ports %}
{% include 'dcim/inc/consoleport.html' %} {% include 'dcim/inc/consoleport.html' %}
{% empty %} {% empty %}
@@ -262,11 +248,6 @@
</table> </table>
{% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %} {% if perms.dcim.add_interface or perms.dcim.add_consoleport or perms.dcim.add_powerport %}
<div class="panel-footer text-right"> <div class="panel-footer text-right">
{% if perms.dcim.add_interface %}
<a href="{% url 'dcim:interface_add' pk=device.pk %}?mgmt_only=1" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add interface
</a>
{% endif %}
{% if perms.dcim.add_consoleport %} {% if perms.dcim.add_consoleport %}
<a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary"> <a href="{% url 'dcim:consoleport_add' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Add console port
@@ -333,7 +314,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="col-md-7 col-lg-6"> <div class="col-md-7">
{% if device_bays or device.device_type.is_parent_device %} {% if device_bays or device.device_type.is_parent_device %}
{% if perms.dcim.delete_devicebay %} {% if perms.dcim.delete_devicebay %}
<form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}"> <form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
@@ -424,12 +405,17 @@
<div class="panel-footer"> <div class="panel-footer">
{% if interfaces and perms.dcim.change_interface %} {% 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"> <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> </button>
{% endif %} {% endif %}
{% if interfaces and perms.dcim.delete_interface %} {% 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"> <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> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_interface %} {% if perms.dcim.add_interface %}
@@ -479,9 +465,14 @@
</table> </table>
{% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %} {% if perms.dcim.add_consoleserverport or perms.dcim.delete_consoleserverport %}
<div class="panel-footer"> <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 %} {% if cs_ports and perms.dcim.delete_consoleserverport %}
<button type="submit" class="btn btn-danger btn-xs"> <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> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_consoleserverport %} {% if perms.dcim.add_consoleserverport %}
@@ -531,9 +522,14 @@
</table> </table>
{% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %} {% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
<div class="panel-footer"> <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 %} {% if power_outlets and perms.dcim.delete_poweroutlet %}
<button type="submit" class="btn btn-danger btn-xs"> <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> </button>
{% endif %} {% endif %}
{% if perms.dcim.add_poweroutlet %} {% if perms.dcim.add_poweroutlet %}
@@ -576,7 +572,7 @@ function toggleConnection(elem, api_url) {
success: function() { success: function() {
elem.parents('tr').removeClass('success').addClass('info'); elem.parents('tr').removeClass('success').addClass('info');
elem.removeClass('connected btn-warning').addClass('btn-success'); 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') elem.children('i').removeClass('glyphicon glyphicon-ban-circle').addClass('fa fa-plug')
} }
}); });
@@ -595,7 +591,7 @@ function toggleConnection(elem, api_url) {
success: function() { success: function() {
elem.parents('tr').removeClass('info').addClass('success'); elem.parents('tr').removeClass('info').addClass('success');
elem.removeClass('btn-success').addClass('connected btn-warning'); 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') elem.children('i').removeClass('fa fa-plug').addClass('glyphicon glyphicon-ban-circle')
} }
}); });

View File

@@ -1,23 +0,0 @@
{% extends 'utilities/bulk_edit_form.html' %}
{% load form_helpers %}
{% block title %}Device Bulk Edit{% endblock %}
{% block selected_objects_table %}
<tr>
<th>Device</th>
<th>Type</th>
<th>Role</th>
<th>Tenant</th>
<th>Serial</th>
</tr>
{% for device in selected_objects %}
<tr>
<td><a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a></td>
<td>{{ device.device_type.full_name }}</td>
<td>{{ device.device_role }}</td>
<td>{{ device.tenant }}</td>
<td>{{ device.serial }}</td>
</tr>
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,53 @@
{% extends '_base.html' %}
{% load staticfiles %}
{% block title %}{{ device }} - Config{% endblock %}
{% block content %}
{% include 'inc/ajax_loader.html' %}
{% include 'dcim/inc/device_header.html' with active_tab='config' %}
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-heading"><strong>Device Configuration</strong></div>
<div class="panel-body">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#running" aria-controls="running" role="tab" data-toggle="tab">Running</a></li>
<li role="presentation"><a href="#startup" aria-controls="startup" role="tab" data-toggle="tab">Startup</a></li>
<li role="presentation"><a href="#candidate" aria-controls="candidate" role="tab" data-toggle="tab">Candidate</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="running">
<pre id="running_config"></pre>
</div>
<div role="tabpanel" class="tab-pane" id="startup">
<pre id="startup_config"></pre>
</div>
<div role="tabpanel" class="tab-pane" id="candidate">
<pre id="candidate_config"></pre>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
$.ajax({
url: "{% url 'dcim-api:device-napalm' pk=device.pk %}?method=get_config",
dataType: 'json',
success: function(json) {
$('#running_config').html($.trim(json['get_config']['running']));
$('#startup_config').html($.trim(json['get_config']['startup']));
$('#candidate_config').html($.trim(json['get_config']['candidate']));
},
error: function(xhr) {
alert(xhr.responseText);
}
});
});
</script>
{% endblock %}

View File

@@ -45,7 +45,7 @@
</p> </p>
</div> </div>
</div> </div>
{% elif not obj.device_type.is_child_device %} {% else %}
{% render_field form.face %} {% render_field form.face %}
{% render_field form.position %} {% render_field form.position %}
{% endif %} {% endif %}

View File

@@ -51,6 +51,8 @@
<th>Manufacturer</th> <th>Manufacturer</th>
<th>Part Number</th> <th>Part Number</th>
<th>Serial Number</th> <th>Serial Number</th>
<th>Asset Tag</th>
<th>Description</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load helpers %} {% load helpers %}
{% block title %}Devices{% endblock %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.dcim.add_device %} {% if perms.dcim.add_device %}
@@ -17,7 +15,7 @@
{% endif %} {% endif %}
{% include 'inc/export_button.html' with obj_type='devices' %} {% include 'inc/export_button.html' with obj_type='devices' %}
</div> </div>
<h1>Devices</h1> <h1>{% block title %}Devices{% endblock %}</h1>
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
{% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %} {% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}

View File

@@ -3,6 +3,7 @@
{% block title %}{{ device }} - LLDP Neighbors{% endblock %} {% block title %}{{ device }} - LLDP Neighbors{% endblock %}
{% block content %} {% block content %}
{% include 'inc/ajax_loader.html' %}
{% include 'dcim/inc/device_header.html' with active_tab='lldp-neighbors' %} {% include 'dcim/inc/device_header.html' with active_tab='lldp-neighbors' %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
@@ -20,7 +21,7 @@
</thead> </thead>
<tbody> <tbody>
{% for iface in interfaces %} {% for iface in interfaces %}
<tr id="{{ iface }}"> <tr id="{{ iface.name }}">
<td>{{ iface }}</td> <td>{{ iface }}</td>
{% if iface.connection %} {% if iface.connection %}
{% with iface.connected_interface as connected_iface %} {% with iface.connected_interface as connected_iface %}
@@ -47,20 +48,21 @@
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
$.ajax({ $.ajax({
url: "{% url 'dcim-api:device-lldp-neighbors' pk=device.pk %}", url: "{% url 'dcim-api:device-napalm' pk=device.pk %}?method=get_lldp_neighbors",
dataType: 'json', dataType: 'json',
success: function(json) { success: function(json) {
$.each(json, function(i, neighbor) { $.each(json['get_lldp_neighbors'], function(iface, neighbors) {
var row = $('#' + neighbor['local-interface'].replace(/(\/)/g, "\\$1")); var neighbor = neighbors[0];
var row = $('#' + iface.replace(/(\/)/g, "\\$1"));
var configured_device = row.children('td.configured_device').attr('data'); var configured_device = row.children('td.configured_device').attr('data');
var configured_interface = row.children('td.configured_interface').attr('data'); var configured_interface = row.children('td.configured_interface').attr('data');
// Add LLDP neighbors to table // Add LLDP neighbors to table
row.children('td.device').html(neighbor['name']); row.children('td.device').html(neighbor['hostname']);
row.children('td.interface').html(neighbor['remote-interface']); row.children('td.interface').html(neighbor['port']);
// Apply colors to rows // Apply colors to rows
if (!configured_device && neighbor['name']) { if (!configured_device && neighbor['hostname']) {
row.addClass('info'); row.addClass('info');
} else if (configured_device == neighbor['name'] && configured_interface == neighbor['remote-interface']) { } else if (configured_device == neighbor['hostname'] && configured_interface == neighbor['port']) {
row.addClass('success'); row.addClass('success');
} else { } else {
row.addClass('danger'); row.addClass('danger');

View File

@@ -0,0 +1,125 @@
{% extends '_base.html' %}
{% load staticfiles %}
{% block title %}{{ device }} - Status{% endblock %}
{% block content %}
{% include 'inc/ajax_loader.html' %}
{% include 'dcim/inc/device_header.html' with active_tab='status' %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading"><strong>Device Facts</strong></div>
<table class="table panel-body">
<tr>
<th>Hostname</th>
<td id="hostname"></td>
</tr>
<tr>
<th>FQDN</th>
<td id="fqdn"></td>
</tr>
<tr>
<th>Vendor</th>
<td id="vendor"></td>
</tr>
<tr>
<th>Model</th>
<td id="model"></td>
</tr>
<tr>
<th>Serial Number</th>
<td id="serial_number"></td>
</tr>
<tr>
<th>OS Version</th>
<td id="os_version"></td>
</tr>
<tr>
<th>Uptime</th>
<td id="uptime"></td>
</tr>
</table>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading"><strong>Environment</strong></div>
<table class="table panel-body">
<tr id="cpu">
<th colspan="2"><i class="fa fa-tachometer"></i> CPU</th>
</tr>
<tr id="memory">
<th colspan="2"><i class="fa fa-microchip"></i> Memory</th>
</tr>
<tr id="temperature">
<th colspan="2"><i class="fa fa-thermometer"></i> Temperature</th>
</tr>
<tr id="fans">
<th colspan="2"><i class="fa fa-superpowers"></i> Fans</th>
</tr>
<tr id="power">
<th colspan="2"><i class="fa fa-bolt"></i> Power</th>
</tr>
</table>
</div>
</div>
</div>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function() {
$.ajax({
url: "{% url 'dcim-api:device-napalm' pk=device.pk %}?method=get_facts&method=get_environment",
dataType: 'json',
success: function(json) {
$('#hostname').html(json['get_facts']['hostname']);
$('#fqdn').html(json['get_facts']['fqdn']);
$('#vendor').html(json['get_facts']['vendor']);
$('#model').html(json['get_facts']['model']);
$('#serial_number').html(json['get_facts']['serial_number']);
$('#os_version').html(json['get_facts']['os_version']);
$('#uptime').html(json['get_facts']['uptime']);
$.each(json['get_environment']['cpu'], function(name, obj) {
var row="<tr><td>" + name + "</td><td>" + obj['%usage'] + "%</td></tr>";
$("#cpu").after(row)
});
$('#memory').after("<tr><td>Used</td><td>" + json['get_environment']['memory']['used_ram'] + "MB</td></tr>");
$('#memory').after("<tr><td>Available</td><td>" + json['get_environment']['memory']['available_ram'] + "MB</td></tr>");
$.each(json['get_environment']['temperature'], function(name, obj) {
var style = "success";
if (obj['is_alert']) {
style = "warning";
} else if (obj['is_critical']) {
style = "danger";
}
var row="<tr class=\"" + style +"\"><td>" + name + "</td><td>" + obj['temperature'] + "°C</td></tr>";
$("#temperature").after(row)
});
$.each(json['get_environment']['fans'], function(name, obj) {
var row;
if (obj['status']) {
row="<tr class=\"success\"><td>" + name + "</td><td><i class=\"fa fa-check text-success\"></i></td></tr>";
} else {
row="<tr class=\"error\"><td>" + name + "</td><td><i class=\"fa fa-close text-error\"></i></td></tr>";
}
$("#fans").after(row)
});
$.each(json['get_environment']['power'], function(name, obj) {
var row;
if (obj['status']) {
row="<tr class=\"success\"><td>" + name + "</td><td><i class=\"fa fa-check text-success\"></i></td></tr>";
} else {
row="<tr class=\"danger\"><td>" + name + "</td><td><i class=\"fa fa-close text-danger\"></i></td></tr>";
}
$("#power").after(row)
});
},
error: function(xhr) {
alert(xhr.responseText);
}
});
});
</script>
{% endblock %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load form_helpers %} {% load form_helpers %}
{% block title %}Populate {{ device_bay }}{% endblock %}
{% block content %} {% block content %}
<form action="." method="post" class="form form-horizontal"> <form action="." method="post" class="form form-horizontal">
{% csrf_token %} {% csrf_token %}
@@ -17,7 +15,7 @@
</div> </div>
{% endif %} {% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Populate {{ device_bay }}</div> <div class="panel-heading">{% block title %}Populate {{ device_bay }}{% endblock %}</div>
<div class="panel-body"> <div class="panel-body">
<div class="form-group"> <div class="form-group">
<label class="col-md-3 control-label required">Parent Device</label> <label class="col-md-3 control-label required">Parent Device</label>

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load helpers %} {% load helpers %}
{% block title %}Device Roles{% endblock %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.dcim.add_devicerole %} {% if perms.dcim.add_devicerole %}
@@ -12,7 +10,7 @@
</a> </a>
{% endif %} {% endif %}
</div> </div>
<h1>Device Roles</h1> <h1>{% block title %}Device Roles{% endblock %}</h1>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% include 'utilities/obj_table.html' with bulk_delete_url='dcim:devicerole_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:devicerole_bulk_delete' %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load helpers %} {% load helpers %}
{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@@ -31,9 +29,9 @@
</div> </div>
{% endif %} {% endif %}
<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1> <h1>{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}</h1>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-5">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Chassis</strong> <strong>Chassis</strong>
@@ -163,21 +161,20 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
<div class="col-md-7">
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=consoleport_table title='Console Ports' add_url='dcim:devicetype_add_consoleport' delete_url='dcim:devicetype_delete_consoleport' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=powerport_table title='Power Ports' add_url='dcim:devicetype_add_powerport' delete_url='dcim:devicetype_delete_powerport' %}
{% include 'dcim/inc/devicetype_component_table.html' with table=mgmt_interface_table title='Management Interfaces' add_url='dcim:devicetype_add_interface' add_url_extra='?mgmt_only=1' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %} {% if devicetype.is_parent_device or devicebay_table.rows %}
</div>
<div class="col-md-6">
{% if devicetype.is_parent_device %}
{% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %} {% include 'dcim/inc/devicetype_component_table.html' with table=devicebay_table title='Device Bays' add_url='dcim:devicetype_add_devicebay' delete_url='dcim:devicetype_delete_devicebay' %}
{% endif %} {% endif %}
{% if devicetype.is_network_device %} {% if devicetype.is_network_device or interface_table.rows %}
{% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %} {% include 'dcim/inc/devicetype_component_table.html' with table=interface_table title='Interfaces' add_url='dcim:devicetype_add_interface' edit_url='dcim:devicetype_bulkedit_interface' delete_url='dcim:devicetype_delete_interface' %}
{% endif %} {% endif %}
{% if devicetype.is_console_server %} {% if devicetype.is_console_server or consoleserverport_table.rows %}
{% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %} {% include 'dcim/inc/devicetype_component_table.html' with table=consoleserverport_table title='Console Server Ports' add_url='dcim:devicetype_add_consoleserverport' delete_url='dcim:devicetype_delete_consoleserverport' %}
{% endif %} {% endif %}
{% if devicetype.is_pdu %} {% if devicetype.is_pdu or poweroutlet_table.rows %}
{% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:devicetype_add_poweroutlet' delete_url='dcim:devicetype_delete_poweroutlet' %} {% include 'dcim/inc/devicetype_component_table.html' with table=poweroutlet_table title='Power Outlets' add_url='dcim:devicetype_add_poweroutlet' delete_url='dcim:devicetype_delete_poweroutlet' %}
{% endif %} {% endif %}
</div> </div>

View File

@@ -1,19 +0,0 @@
{% extends 'utilities/bulk_edit_form.html' %}
{% load form_helpers %}
{% block title %}Device Type Bulk Edit{% endblock %}
{% block selected_objects_table %}
<tr>
<th>Device type</th>
<th>Manufacturer</th>
<th>Height</th>
</tr>
{% for devicetype in selected_objects %}
<tr>
<td><a href="{% url 'dcim:devicetype' pk=devicetype.pk %}">{{ devicetype.model }}</a></td>
<td>{{ devicetype.manufacturer }}</td>
<td>{{ devicetype.u_height }}U</td>
</tr>
{% endfor %}
{% endblock %}

View File

@@ -1,8 +1,6 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load helpers %} {% load helpers %}
{% block title %}Device Types{% endblock %}
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.dcim.add_devicetype %} {% if perms.dcim.add_devicetype %}
@@ -12,7 +10,7 @@
</a> </a>
{% endif %} {% endif %}
</div> </div>
<h1>Device Types</h1> <h1>{% block title %}Device Types{% endblock %}</h1>
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
{% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %} {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %}

View File

@@ -1,13 +1,7 @@
<tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}"> <tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_consoleport or perms.dcim.delete_consoleport %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ cp.pk }}" />
</td>
{% endif %}
<td> <td>
<i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }} <i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
</td> </td>
<td></td>
{% if cp.cs_port %} {% if cp.cs_port %}
<td> <td>
<a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a> <a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
@@ -20,28 +14,28 @@
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td colspan="2" class="text-right"> <td class="text-right">
{% if perms.dcim.change_consoleport %} {% if perms.dcim.change_consoleport %}
{% if cp.cs_port %} {% if cp.cs_port %}
{% if cp.connection_status %} {% if cp.connection_status %}
<a href="#" class="btn btn-warning btn-xs consoleport-toggle connected" data="{{ cp.pk }}"> <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" title="Mark planned"></i> <i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</a> </a>
{% else %} {% else %}
<a href="#" class="btn btn-success btn-xs consoleport-toggle" data="{{ cp.pk }}"> <a href="#" class="btn btn-success btn-xs consoleport-toggle" title="Mark installed" data="{{ cp.pk }}">
<i class="fa fa-plug" aria-hidden="true" title="Mark connected"></i> <i class="fa fa-plug" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:consoleport_disconnect' pk=cp.pk %}" class="btn btn-danger btn-xs"> <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" title="Delete connection"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a> </a>
{% else %} {% else %}
<a href="{% url 'dcim:consoleport_connect' pk=cp.pk %}" class="btn btn-success btn-xs"> <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" title="Connect"></i> <i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" class="btn btn-info btn-xs"> <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" title="Edit port"></i> <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
{% if perms.dcim.delete_consoleport %} {% if perms.dcim.delete_consoleport %}
@@ -50,8 +44,8 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button> </button>
{% else %} {% else %}
<a href="{% url 'dcim:consoleport_delete' pk=cp.pk %}" class="btn btn-danger btn-xs"> <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" title="Delete port"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

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

View File

@@ -45,7 +45,15 @@
<ul class="nav nav-tabs" style="margin-bottom: 20px"> <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 == '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> <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 perms.dcim.napalm_read %}
{% if device.status == 1 and device.platform.napalm_driver and device.primary_ip %}
<li role="presentation"{% if active_tab == 'status' %} class="active"{% endif %}><a href="{% url 'dcim:device_status' pk=device.pk %}">Status</a></li>
<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> <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>
<li role="presentation"{% if active_tab == 'config' %} class="active"{% endif %}><a href="{% url 'dcim:device_config' pk=device.pk %}">Configuration</a></li>
{% else %}
<li role="presentation" class="disabled"><a href="#">Status</a></li>
<li role="presentation" class="disabled"><a href="#">LLDP Neighbors</a></li>
<li role="presentation" class="disabled"><a href="#">Configuration</a></li>
{% endif %}
{% endif %} {% endif %}
</ul> </ul>

View File

@@ -1,11 +1,12 @@
<tr class="interface{% if iface.connection and not iface.connection.connection_status %} info{% endif %}"> <tr class="interface{% if not iface.enabled %} danger{% elif 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 %} {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ iface.pk }}" /> <input name="pk" type="checkbox" value="{{ iface.pk }}" />
</td> </td>
{% endif %} {% endif %}
<td> <td>
<i class="fa fa-fw fa-{{ icon|default:"exchange" }}"></i> <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span> <i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_virtual %}square{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
<span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
{% if iface.lag %} {% if iface.lag %}
<span class="label label-primary">{{ iface.lag.name }}</span> <span class="label label-primary">{{ iface.lag.name }}</span>
{% endif %} {% endif %}
@@ -13,6 +14,7 @@
<i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i> <i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
{% endif %} {% endif %}
</td> </td>
<td>{{ iface.mtu|default:"" }}</td>
<td>{{ iface.mac_address|default:"" }}</td> <td>{{ iface.mac_address|default:"" }}</td>
{% if iface.is_lag %} {% if iface.is_lag %}
<td colspan="2" class="text-muted"> <td colspan="2" class="text-muted">
@@ -21,6 +23,8 @@
</td> </td>
{% elif iface.is_virtual %} {% elif iface.is_virtual %}
<td colspan="2" class="text-muted">Virtual interface</td> <td colspan="2" class="text-muted">Virtual interface</td>
{% elif iface.is_wireless %}
<td colspan="2" class="text-muted">Wireless interface</td>
{% elif iface.connection %} {% elif iface.connection %}
{% with iface.connected_interface as connected_iface %} {% with iface.connected_interface as connected_iface %}
<td> <td>
@@ -72,7 +76,7 @@
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i> <i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</a> </a>
{% else %} {% 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> <i class="fa fa-plug" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
@@ -114,7 +118,7 @@
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
<td></td> <td></td>
{% endif %} {% endif %}
<td colspan="2"> <td colspan="3">
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> <a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
{% if ip.description %} {% if ip.description %}
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i> <i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>

View File

@@ -1,9 +1,11 @@
<tr> <tr>
<td style="padding-left: {{ indent|add:5 }}px">{{ item.name }}</td> <td style="padding-left: {{ indent|add:5 }}px">{{ item.name }}</td>
<td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td> <td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
<td>{{ item.manufacturer|default:'' }}</td> <td>{{ item.manufacturer|default:"" }}</td>
<td>{{ item.part_id }}</td> <td>{{ item.part_id }}</td>
<td>{{ item.serial }}</td> <td>{{ item.serial }}</td>
<td>{{ item.asset_tag|default:"" }}</td>
<td>{{ item.description }}</td>
<td class="text-right"> <td class="text-right">
{% if perms.dcim.change_inventoryitem %} {% if perms.dcim.change_inventoryitem %}
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a> <a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}" class="btn btn-xs btn-warning"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></a>

View File

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

View File

@@ -1,13 +1,7 @@
<tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}"> <tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_powerport or perms.dcim.delete_powerport %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ pp.pk }}" />
</td>
{% endif %}
<td> <td>
<i class="fa fa-fw fa-bolt"></i> {{ pp.name }} <i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
</td> </td>
<td></td>
{% if pp.power_outlet %} {% if pp.power_outlet %}
<td> <td>
<a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a> <a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
@@ -20,28 +14,28 @@
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td colspan="2" class="text-right"> <td class="text-right">
{% if perms.dcim.change_powerport %} {% if perms.dcim.change_powerport %}
{% if pp.power_outlet %} {% if pp.power_outlet %}
{% if pp.connection_status %} {% if pp.connection_status %}
<a href="#" class="btn btn-warning btn-xs powerport-toggle connected" data="{{ pp.pk }}"> <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" title="Mark planned"></i> <i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</a> </a>
{% else %} {% else %}
<a href="#" class="btn btn-success btn-xs powerport-toggle" data="{{ pp.pk }}"> <a href="#" class="btn btn-success btn-xs powerport-toggle" title="Mark installed" data="{{ pp.pk }}">
<i class="fa fa-plug" aria-hidden="true" title="Mark connected"></i> <i class="fa fa-plug" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:powerport_disconnect' pk=pp.pk %}" class="btn btn-danger btn-xs"> <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" title="Delete connection"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a> </a>
{% else %} {% else %}
<a href="{% url 'dcim:powerport_connect' pk=pp.pk %}" class="btn btn-success btn-xs"> <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" title="Connect"></i> <i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" class="btn btn-info btn-xs"> <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" title="Edit port"></i> <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
{% if perms.dcim.delete_powerport %} {% if perms.dcim.delete_powerport %}
@@ -50,8 +44,8 @@
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</button> </button>
{% else %} {% else %}
<a href="{% url 'dcim:powerport_delete' pk=pp.pk %}" class="btn btn-danger btn-xs"> <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" title="Delete port"></i> <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@@ -1,17 +0,0 @@
{% extends 'utilities/bulk_edit_form.html' %}
{% load form_helpers %}
{% block title %}Interface Bulk Edit{% endblock %}
{% block selected_objects_table %}
<tr>
<th>Name</th>
<th>Form Factor</th>
</tr>
{% for iface in selected_objects %}
<tr>
<td>{{ iface.name }}</td>
<td>{{ iface.get_form_factor_display }}</td>
</tr>
{% endfor %}
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More